Files
claudetools/api/routers/context_snippets.py
Mike Swanson 390b10b32c Complete Phase 6: MSP Work Tracking with Context Recall System
Implements production-ready MSP platform with cross-machine persistent memory for Claude.

API Implementation:
- 130 REST API endpoints across 21 entities
- JWT authentication on all endpoints
- AES-256-GCM encryption for credentials
- Automatic audit logging
- Complete OpenAPI documentation

Database:
- 43 tables in MariaDB (172.16.3.20:3306)
- 42 SQLAlchemy models with modern 2.0 syntax
- Full Alembic migration system
- 99.1% CRUD test pass rate

Context Recall System (Phase 6):
- Cross-machine persistent memory via database
- Automatic context injection via Claude Code hooks
- Automatic context saving after task completion
- 90-95% token reduction with compression utilities
- Relevance scoring with time decay
- Tag-based semantic search
- One-command setup script

Security Features:
- JWT tokens with Argon2 password hashing
- AES-256-GCM encryption for all sensitive data
- Comprehensive audit trail for credentials
- HMAC tamper detection
- Secure configuration management

Test Results:
- Phase 3: 38/38 CRUD tests passing (100%)
- Phase 4: 34/35 core API tests passing (97.1%)
- Phase 5: 62/62 extended API tests passing (100%)
- Phase 6: 10/10 compression tests passing (100%)
- Overall: 144/145 tests passing (99.3%)

Documentation:
- Comprehensive architecture guides
- Setup automation scripts
- API documentation at /api/docs
- Complete test reports
- Troubleshooting guides

Project Status: 95% Complete (Production-Ready)
Phase 7 (optional work context APIs) remains for future enhancement.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-17 06:00:26 -07:00

313 lines
9.0 KiB
Python

"""
ContextSnippet API router for ClaudeTools.
Defines all REST API endpoints for managing context snippets,
reusable pieces of knowledge for quick retrieval.
"""
from typing import List
from uuid import UUID
from fastapi import APIRouter, Depends, HTTPException, Query, status
from sqlalchemy.orm import Session
from api.database import get_db
from api.middleware.auth import get_current_user
from api.schemas.context_snippet import (
ContextSnippetCreate,
ContextSnippetResponse,
ContextSnippetUpdate,
)
from api.services import context_snippet_service
# Create router with prefix and tags
router = APIRouter()
@router.get(
"",
response_model=dict,
summary="List all context snippets",
description="Retrieve a paginated list of all context snippets with optional filtering",
status_code=status.HTTP_200_OK,
)
def list_context_snippets(
skip: int = Query(
default=0,
ge=0,
description="Number of records to skip for pagination"
),
limit: int = Query(
default=100,
ge=1,
le=1000,
description="Maximum number of records to return (max 1000)"
),
db: Session = Depends(get_db),
current_user: dict = Depends(get_current_user),
):
"""
List all context snippets with pagination.
Returns snippets ordered by relevance score and usage count.
"""
try:
snippets, total = context_snippet_service.get_context_snippets(db, skip, limit)
return {
"total": total,
"skip": skip,
"limit": limit,
"snippets": [ContextSnippetResponse.model_validate(snippet) for snippet in snippets]
}
except Exception as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to retrieve context snippets: {str(e)}"
)
@router.get(
"/by-tags",
response_model=dict,
summary="Get context snippets by tags",
description="Retrieve context snippets filtered by tags",
status_code=status.HTTP_200_OK,
)
def get_context_snippets_by_tags(
tags: List[str] = Query(..., description="Tags to filter by (OR logic - any match)"),
skip: int = Query(default=0, ge=0),
limit: int = Query(default=100, ge=1, le=1000),
db: Session = Depends(get_db),
current_user: dict = Depends(get_current_user),
):
"""
Get context snippets filtered by tags.
Uses OR logic - snippets matching any of the provided tags will be returned.
"""
try:
snippets, total = context_snippet_service.get_context_snippets_by_tags(
db, tags, skip, limit
)
return {
"total": total,
"skip": skip,
"limit": limit,
"tags": tags,
"snippets": [ContextSnippetResponse.model_validate(snippet) for snippet in snippets]
}
except Exception as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to retrieve context snippets: {str(e)}"
)
@router.get(
"/top-relevant",
response_model=dict,
summary="Get top relevant context snippets",
description="Retrieve the most relevant context snippets by relevance score",
status_code=status.HTTP_200_OK,
)
def get_top_relevant_snippets(
limit: int = Query(
default=10,
ge=1,
le=50,
description="Maximum number of snippets to retrieve (max 50)"
),
min_relevance_score: float = Query(
default=7.0,
ge=0.0,
le=10.0,
description="Minimum relevance score threshold (0.0-10.0)"
),
db: Session = Depends(get_db),
current_user: dict = Depends(get_current_user),
):
"""
Get the top most relevant context snippets.
Returns snippets ordered by relevance score (highest first).
"""
try:
snippets = context_snippet_service.get_top_relevant_snippets(
db, limit, min_relevance_score
)
return {
"total": len(snippets),
"limit": limit,
"min_relevance_score": min_relevance_score,
"snippets": [ContextSnippetResponse.model_validate(snippet) for snippet in snippets]
}
except Exception as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to retrieve top relevant snippets: {str(e)}"
)
@router.get(
"/by-project/{project_id}",
response_model=dict,
summary="Get context snippets by project",
description="Retrieve all context snippets for a specific project",
status_code=status.HTTP_200_OK,
)
def get_context_snippets_by_project(
project_id: UUID,
skip: int = Query(default=0, ge=0),
limit: int = Query(default=100, ge=1, le=1000),
db: Session = Depends(get_db),
current_user: dict = Depends(get_current_user),
):
"""
Get all context snippets for a specific project.
"""
try:
snippets, total = context_snippet_service.get_context_snippets_by_project(
db, project_id, skip, limit
)
return {
"total": total,
"skip": skip,
"limit": limit,
"project_id": str(project_id),
"snippets": [ContextSnippetResponse.model_validate(snippet) for snippet in snippets]
}
except Exception as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to retrieve context snippets: {str(e)}"
)
@router.get(
"/by-client/{client_id}",
response_model=dict,
summary="Get context snippets by client",
description="Retrieve all context snippets for a specific client",
status_code=status.HTTP_200_OK,
)
def get_context_snippets_by_client(
client_id: UUID,
skip: int = Query(default=0, ge=0),
limit: int = Query(default=100, ge=1, le=1000),
db: Session = Depends(get_db),
current_user: dict = Depends(get_current_user),
):
"""
Get all context snippets for a specific client.
"""
try:
snippets, total = context_snippet_service.get_context_snippets_by_client(
db, client_id, skip, limit
)
return {
"total": total,
"skip": skip,
"limit": limit,
"client_id": str(client_id),
"snippets": [ContextSnippetResponse.model_validate(snippet) for snippet in snippets]
}
except Exception as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to retrieve context snippets: {str(e)}"
)
@router.get(
"/{snippet_id}",
response_model=ContextSnippetResponse,
summary="Get context snippet by ID",
description="Retrieve a single context snippet by its unique identifier (increments usage_count)",
status_code=status.HTTP_200_OK,
)
def get_context_snippet(
snippet_id: UUID,
db: Session = Depends(get_db),
current_user: dict = Depends(get_current_user),
):
"""
Get a specific context snippet by ID.
Note: This automatically increments the usage_count for tracking.
"""
snippet = context_snippet_service.get_context_snippet_by_id(db, snippet_id)
return ContextSnippetResponse.model_validate(snippet)
@router.post(
"",
response_model=ContextSnippetResponse,
summary="Create new context snippet",
description="Create a new context snippet with the provided details",
status_code=status.HTTP_201_CREATED,
)
def create_context_snippet(
snippet_data: ContextSnippetCreate,
db: Session = Depends(get_db),
current_user: dict = Depends(get_current_user),
):
"""
Create a new context snippet.
Requires a valid JWT token with appropriate permissions.
"""
snippet = context_snippet_service.create_context_snippet(db, snippet_data)
return ContextSnippetResponse.model_validate(snippet)
@router.put(
"/{snippet_id}",
response_model=ContextSnippetResponse,
summary="Update context snippet",
description="Update an existing context snippet's details",
status_code=status.HTTP_200_OK,
)
def update_context_snippet(
snippet_id: UUID,
snippet_data: ContextSnippetUpdate,
db: Session = Depends(get_db),
current_user: dict = Depends(get_current_user),
):
"""
Update an existing context snippet.
Only provided fields will be updated. All fields are optional.
"""
snippet = context_snippet_service.update_context_snippet(db, snippet_id, snippet_data)
return ContextSnippetResponse.model_validate(snippet)
@router.delete(
"/{snippet_id}",
response_model=dict,
summary="Delete context snippet",
description="Delete a context snippet by its ID",
status_code=status.HTTP_200_OK,
)
def delete_context_snippet(
snippet_id: UUID,
db: Session = Depends(get_db),
current_user: dict = Depends(get_current_user),
):
"""
Delete a context snippet.
This is a permanent operation and cannot be undone.
"""
return context_snippet_service.delete_context_snippet(db, snippet_id)