semantics
This commit is contained in:
@@ -4,9 +4,41 @@
|
||||
# @PURPOSE: Provide lazy route module loading to avoid heavyweight imports during tests.
|
||||
# @LAYER: API
|
||||
# @RELATION: [CALLS] ->[ApiRoutesGetAttr]
|
||||
# @RELATION: [BINDS_TO] ->[Route_Group_Contracts]
|
||||
# @INVARIANT: Only names listed in __all__ are importable via __getattr__.
|
||||
|
||||
__all__ = ['plugins', 'tasks', 'settings', 'connections', 'environments', 'mappings', 'migration', 'git', 'storage', 'admin', 'reports', 'assistant', 'clean_release', 'profile', 'dataset_review']
|
||||
# [DEF:Route_Group_Contracts:Block]
|
||||
# @COMPLEXITY: 3
|
||||
# @PURPOSE: Declare the canonical route-module registry used by lazy imports and app router inclusion.
|
||||
# @RELATION: DEPENDS_ON -> [PluginsRouter]
|
||||
# @RELATION: DEPENDS_ON -> [TasksRouter]
|
||||
# @RELATION: DEPENDS_ON -> [SettingsRouter]
|
||||
# @RELATION: DEPENDS_ON -> [ConnectionsRouter]
|
||||
# @RELATION: DEPENDS_ON -> [ReportsRouter]
|
||||
# @RELATION: DEPENDS_ON -> [LlmRoutes]
|
||||
__all__ = [
|
||||
"plugins",
|
||||
"tasks",
|
||||
"settings",
|
||||
"connections",
|
||||
"environments",
|
||||
"mappings",
|
||||
"migration",
|
||||
"git",
|
||||
"storage",
|
||||
"admin",
|
||||
"reports",
|
||||
"assistant",
|
||||
"clean_release",
|
||||
"clean_release_v2",
|
||||
"profile",
|
||||
"dataset_review",
|
||||
"llm",
|
||||
"dashboards",
|
||||
"datasets",
|
||||
"health",
|
||||
]
|
||||
# [/DEF:Route_Group_Contracts:Block]
|
||||
|
||||
|
||||
# [DEF:ApiRoutesGetAttr:Function]
|
||||
@@ -18,7 +50,10 @@ __all__ = ['plugins', 'tasks', 'settings', 'connections', 'environments', 'mappi
|
||||
def __getattr__(name):
|
||||
if name in __all__:
|
||||
import importlib
|
||||
|
||||
return importlib.import_module(f".{name}", __name__)
|
||||
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
||||
|
||||
|
||||
# [/DEF:ApiRoutesGetAttr:Function]
|
||||
# [/DEF:ApiRoutesModule:Module]
|
||||
|
||||
@@ -559,6 +559,4 @@ def test_dataset_review_scoped_command_routes_field_semantics_update():
|
||||
# [/DEF:test_dataset_review_scoped_command_routes_field_semantics_update:Function]
|
||||
|
||||
|
||||
# [/DEF:test_capabilities_question_returns_successful_help:Function]
|
||||
|
||||
# [/DEF:AssistantApiTests:Module]
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
# [DEF:CleanReleaseV2Api:Module]
|
||||
# @COMPLEXITY: 4
|
||||
# @PURPOSE: Redesigned clean release API for headless candidate lifecycle.
|
||||
# @LAYER: UI (API)
|
||||
# @RELATION: DEPENDS_ON -> [CleanReleaseRepository]
|
||||
# @RELATION: CALLS -> [approve_candidate]
|
||||
# @RELATION: CALLS -> [publish_candidate]
|
||||
# @PRE: Clean release repository dependency is available for candidate lifecycle endpoints.
|
||||
# @POST: Candidate registration, approval, publication, and revocation routes are registered without behavior changes.
|
||||
# @SIDE_EFFECT: Persists candidate lifecycle state through clean release services and repository adapters.
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from typing import List, Dict, Any
|
||||
@@ -29,7 +36,6 @@ router = APIRouter(prefix="/api/v2/clean-release", tags=["Clean Release V2"])
|
||||
# [DEF:ApprovalRequest:Class]
|
||||
# @COMPLEXITY: 1
|
||||
# @PURPOSE: Schema for approval request payload.
|
||||
# @RELATION: USES -> [CandidateDTO]
|
||||
class ApprovalRequest(dict):
|
||||
pass
|
||||
|
||||
@@ -40,7 +46,6 @@ class ApprovalRequest(dict):
|
||||
# [DEF:PublishRequest:Class]
|
||||
# @COMPLEXITY: 1
|
||||
# @PURPOSE: Schema for publication request payload.
|
||||
# @RELATION: USES -> [CandidateDTO]
|
||||
class PublishRequest(dict):
|
||||
pass
|
||||
|
||||
@@ -51,7 +56,6 @@ class PublishRequest(dict):
|
||||
# [DEF:RevokeRequest:Class]
|
||||
# @COMPLEXITY: 1
|
||||
# @PURPOSE: Schema for revocation request payload.
|
||||
# @RELATION: USES -> [CandidateDTO]
|
||||
class RevokeRequest(dict):
|
||||
pass
|
||||
|
||||
@@ -66,7 +70,7 @@ class RevokeRequest(dict):
|
||||
# @POST: Candidate is saved in repository.
|
||||
# @RETURN: CandidateDTO
|
||||
# @RELATION: CALLS -> [CleanReleaseRepository.save_candidate]
|
||||
# @RELATION: USES -> [CandidateDTO]
|
||||
# @RELATION: DEPENDS_ON -> [CandidateDTO]
|
||||
@router.post(
|
||||
"/candidates", response_model=CandidateDTO, status_code=status.HTTP_201_CREATED
|
||||
)
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
# [DEF:backend/src/api/routes/llm.py:Module]
|
||||
# @COMPLEXITY: 2
|
||||
# [DEF:LlmRoutes:Module]
|
||||
# @COMPLEXITY: 3
|
||||
# @SEMANTICS: api, routes, llm
|
||||
# @PURPOSE: API routes for LLM provider configuration and management.
|
||||
# @LAYER: UI (API)
|
||||
# @RELATION: DEPENDS_ON -> [LLMProviderService]
|
||||
# @RELATION: DEPENDS_ON -> [LLMProviderConfig]
|
||||
# @RELATION: DEPENDS_ON -> [get_current_user]
|
||||
# @RELATION: DEPENDS_ON -> [get_db]
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from typing import List, Optional
|
||||
@@ -24,6 +28,7 @@ router = APIRouter(tags=["LLM"])
|
||||
# @PURPOSE: Validate decrypted runtime API key presence/shape.
|
||||
# @PRE: value can be None.
|
||||
# @POST: Returns True only for non-placeholder key.
|
||||
# @RELATION: BINDS_TO -> [LlmRoutes]
|
||||
def _is_valid_runtime_api_key(value: Optional[str]) -> bool:
|
||||
key = (value or "").strip()
|
||||
if not key:
|
||||
@@ -40,6 +45,8 @@ def _is_valid_runtime_api_key(value: Optional[str]) -> bool:
|
||||
# @PURPOSE: Retrieve all LLM provider configurations.
|
||||
# @PRE: User is authenticated.
|
||||
# @POST: Returns list of LLMProviderConfig.
|
||||
# @RELATION: CALLS -> [LLMProviderService]
|
||||
# @RELATION: DEPENDS_ON -> [LLMProviderConfig]
|
||||
@router.get("/providers", response_model=List[LLMProviderConfig])
|
||||
async def get_providers(
|
||||
current_user: User = Depends(get_current_active_user), db: Session = Depends(get_db)
|
||||
@@ -73,6 +80,8 @@ async def get_providers(
|
||||
# @PURPOSE: Returns whether LLM runtime is configured for dashboard validation.
|
||||
# @PRE: User is authenticated.
|
||||
# @POST: configured=true only when an active provider with valid decrypted key exists.
|
||||
# @RELATION: CALLS -> [LLMProviderService]
|
||||
# @RELATION: CALLS -> [_is_valid_runtime_api_key]
|
||||
@router.get("/status")
|
||||
async def get_llm_status(
|
||||
current_user: User = Depends(get_current_active_user), db: Session = Depends(get_db)
|
||||
@@ -112,7 +121,9 @@ async def get_llm_status(
|
||||
"configured": False,
|
||||
"reason": "invalid_api_key",
|
||||
"provider_count": len(providers),
|
||||
"active_provider_count": len([provider for provider in providers if provider.is_active]),
|
||||
"active_provider_count": len(
|
||||
[provider for provider in providers if provider.is_active]
|
||||
),
|
||||
"provider_id": active_provider.id,
|
||||
"provider_name": active_provider.name,
|
||||
"provider_type": active_provider.provider_type,
|
||||
@@ -123,7 +134,9 @@ async def get_llm_status(
|
||||
"configured": True,
|
||||
"reason": "ok",
|
||||
"provider_count": len(providers),
|
||||
"active_provider_count": len([provider for provider in providers if provider.is_active]),
|
||||
"active_provider_count": len(
|
||||
[provider for provider in providers if provider.is_active]
|
||||
),
|
||||
"provider_id": active_provider.id,
|
||||
"provider_name": active_provider.name,
|
||||
"provider_type": active_provider.provider_type,
|
||||
@@ -138,6 +151,8 @@ async def get_llm_status(
|
||||
# @PURPOSE: Create a new LLM provider configuration.
|
||||
# @PRE: User is authenticated and has admin permissions.
|
||||
# @POST: Returns the created LLMProviderConfig.
|
||||
# @RELATION: CALLS -> [LLMProviderService]
|
||||
# @RELATION: DEPENDS_ON -> [LLMProviderConfig]
|
||||
@router.post(
|
||||
"/providers", response_model=LLMProviderConfig, status_code=status.HTTP_201_CREATED
|
||||
)
|
||||
@@ -169,6 +184,8 @@ async def create_provider(
|
||||
# @PURPOSE: Update an existing LLM provider configuration.
|
||||
# @PRE: User is authenticated and has admin permissions.
|
||||
# @POST: Returns the updated LLMProviderConfig.
|
||||
# @RELATION: CALLS -> [LLMProviderService]
|
||||
# @RELATION: DEPENDS_ON -> [LLMProviderConfig]
|
||||
@router.put("/providers/{provider_id}", response_model=LLMProviderConfig)
|
||||
async def update_provider(
|
||||
provider_id: str,
|
||||
@@ -202,6 +219,7 @@ async def update_provider(
|
||||
# @PURPOSE: Delete an LLM provider configuration.
|
||||
# @PRE: User is authenticated and has admin permissions.
|
||||
# @POST: Returns success status.
|
||||
# @RELATION: CALLS -> [LLMProviderService]
|
||||
@router.delete("/providers/{provider_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def delete_provider(
|
||||
provider_id: str,
|
||||
@@ -224,6 +242,8 @@ async def delete_provider(
|
||||
# @PURPOSE: Test connection to an LLM provider.
|
||||
# @PRE: User is authenticated.
|
||||
# @POST: Returns success status and message.
|
||||
# @RELATION: CALLS -> [LLMProviderService]
|
||||
# @RELATION: DEPENDS_ON -> [LLMClient]
|
||||
@router.post("/providers/{provider_id}/test")
|
||||
async def test_connection(
|
||||
provider_id: str,
|
||||
@@ -276,6 +296,8 @@ async def test_connection(
|
||||
# @PURPOSE: Test connection with a provided configuration (not yet saved).
|
||||
# @PRE: User is authenticated.
|
||||
# @POST: Returns success status and message.
|
||||
# @RELATION: DEPENDS_ON -> [LLMClient]
|
||||
# @RELATION: DEPENDS_ON -> [LLMProviderConfig]
|
||||
@router.post("/providers/test")
|
||||
async def test_provider_config(
|
||||
config: LLMProviderConfig, current_user: User = Depends(get_current_active_user)
|
||||
@@ -311,4 +333,4 @@ async def test_provider_config(
|
||||
|
||||
# [/DEF:test_provider_config:Function]
|
||||
|
||||
# [/DEF:backend/src/api/routes/llm.py:Module]
|
||||
# [/DEF:LlmRoutes:Module]
|
||||
|
||||
@@ -3,7 +3,9 @@
|
||||
# @SEMANTICS: api, router, plugins, list
|
||||
# @PURPOSE: Defines the FastAPI router for plugin-related endpoints, allowing clients to list available plugins.
|
||||
# @LAYER: UI (API)
|
||||
# @RELATION: Depends on the PluginLoader and PluginConfig. It is included by the main app.
|
||||
# @RELATION: DEPENDS_ON -> [PluginConfig]
|
||||
# @RELATION: DEPENDS_ON -> [get_plugin_loader]
|
||||
# @RELATION: BINDS_TO -> [API_Routes]
|
||||
from typing import List
|
||||
from fastapi import APIRouter, Depends
|
||||
|
||||
@@ -13,20 +15,25 @@ from ...core.logger import belief_scope
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
# [DEF:list_plugins:Function]
|
||||
# @PURPOSE: Retrieve a list of all available plugins.
|
||||
# @PRE: plugin_loader is injected via Depends.
|
||||
# @POST: Returns a list of PluginConfig objects.
|
||||
# @RETURN: List[PluginConfig] - List of registered plugins.
|
||||
# @RELATION: CALLS -> [get_plugin_loader]
|
||||
# @RELATION: DEPENDS_ON -> [PluginConfig]
|
||||
@router.get("", response_model=List[PluginConfig])
|
||||
async def list_plugins(
|
||||
plugin_loader = Depends(get_plugin_loader),
|
||||
_ = Depends(has_permission("plugins", "READ"))
|
||||
plugin_loader=Depends(get_plugin_loader),
|
||||
_=Depends(has_permission("plugins", "READ")),
|
||||
):
|
||||
with belief_scope("list_plugins"):
|
||||
"""
|
||||
Retrieve a list of all available plugins.
|
||||
"""
|
||||
return plugin_loader.get_all_plugin_configs()
|
||||
|
||||
|
||||
# [/DEF:list_plugins:Function]
|
||||
# [/DEF:PluginsRouter:Module]
|
||||
# [/DEF:PluginsRouter:Module]
|
||||
|
||||
@@ -49,6 +49,7 @@ router = APIRouter(prefix="/api/reports", tags=["Reports"])
|
||||
# @PARAM: enum_cls (type) - Enum class for validation.
|
||||
# @PARAM: field_name (str) - Query field name for diagnostics.
|
||||
# @RETURN: List - Parsed enum values.
|
||||
# @RELATION: BINDS_TO -> [ReportsRouter]
|
||||
def _parse_csv_enum_list(raw: Optional[str], enum_cls, field_name: str) -> List:
|
||||
with belief_scope("_parse_csv_enum_list"):
|
||||
if raw is None or not raw.strip():
|
||||
@@ -158,6 +159,7 @@ async def list_reports(
|
||||
# @PURPOSE: Return one normalized report detail with diagnostics and next actions.
|
||||
# @PRE: authenticated/authorized request and existing report_id.
|
||||
# @POST: returns normalized detail envelope or 404 when report is not found.
|
||||
# @RELATION: CALLS -> [ReportsService:Class]
|
||||
@router.get("/{report_id}", response_model=ReportDetailView)
|
||||
async def get_report_detail(
|
||||
report_id: str,
|
||||
|
||||
@@ -15,7 +15,12 @@ from ...core.logger import belief_scope
|
||||
|
||||
from ...core.task_manager import TaskManager, Task, TaskStatus, LogEntry
|
||||
from ...core.task_manager.models import LogFilter, LogStats
|
||||
from ...dependencies import get_task_manager, has_permission, get_current_user, get_config_manager
|
||||
from ...dependencies import (
|
||||
get_task_manager,
|
||||
has_permission,
|
||||
get_current_user,
|
||||
get_config_manager,
|
||||
)
|
||||
from ...core.config_manager import ConfigManager
|
||||
from ...services.llm_prompt_templates import (
|
||||
is_multimodal_model,
|
||||
@@ -32,16 +37,20 @@ TASK_TYPE_PLUGIN_MAP = {
|
||||
"migration": ["superset-migration"],
|
||||
}
|
||||
|
||||
|
||||
class CreateTaskRequest(BaseModel):
|
||||
plugin_id: str
|
||||
params: Dict[str, Any]
|
||||
|
||||
|
||||
class ResolveTaskRequest(BaseModel):
|
||||
resolution_params: Dict[str, Any]
|
||||
|
||||
|
||||
class ResumeTaskRequest(BaseModel):
|
||||
passwords: Dict[str, str]
|
||||
|
||||
|
||||
# [DEF:create_task:Function]
|
||||
# @COMPLEXITY: 3
|
||||
# @PURPOSE: Create and start a new task for a given plugin.
|
||||
@@ -50,11 +59,14 @@ class ResumeTaskRequest(BaseModel):
|
||||
# @PRE: plugin_id must exist and params must be valid for that plugin.
|
||||
# @POST: A new task is created and started.
|
||||
# @RETURN: Task - The created task instance.
|
||||
# @RELATION: CALLS -> [TaskManager]
|
||||
# @RELATION: DEPENDS_ON -> [ConfigManager]
|
||||
# @RELATION: DEPENDS_ON -> [LLMProviderService]
|
||||
@router.post("", response_model=Task, status_code=status.HTTP_201_CREATED)
|
||||
async def create_task(
|
||||
request: CreateTaskRequest,
|
||||
task_manager: TaskManager = Depends(get_task_manager),
|
||||
current_user = Depends(get_current_user),
|
||||
current_user=Depends(get_current_user),
|
||||
config_manager: ConfigManager = Depends(get_config_manager),
|
||||
):
|
||||
# Dynamic permission check based on plugin_id
|
||||
@@ -65,19 +77,30 @@ async def create_task(
|
||||
if request.plugin_id in {"llm_dashboard_validation", "llm_documentation"}:
|
||||
from ...core.database import SessionLocal
|
||||
from ...services.llm_provider import LLMProviderService
|
||||
|
||||
db = SessionLocal()
|
||||
try:
|
||||
llm_service = LLMProviderService(db)
|
||||
provider_id = request.params.get("provider_id")
|
||||
if not provider_id:
|
||||
llm_settings = normalize_llm_settings(config_manager.get_config().settings.llm)
|
||||
binding_key = "dashboard_validation" if request.plugin_id == "llm_dashboard_validation" else "documentation"
|
||||
provider_id = resolve_bound_provider_id(llm_settings, binding_key)
|
||||
llm_settings = normalize_llm_settings(
|
||||
config_manager.get_config().settings.llm
|
||||
)
|
||||
binding_key = (
|
||||
"dashboard_validation"
|
||||
if request.plugin_id == "llm_dashboard_validation"
|
||||
else "documentation"
|
||||
)
|
||||
provider_id = resolve_bound_provider_id(
|
||||
llm_settings, binding_key
|
||||
)
|
||||
if provider_id:
|
||||
request.params["provider_id"] = provider_id
|
||||
if not provider_id:
|
||||
providers = llm_service.get_all_providers()
|
||||
active_provider = next((p for p in providers if p.is_active), None)
|
||||
active_provider = next(
|
||||
(p for p in providers if p.is_active), None
|
||||
)
|
||||
if active_provider:
|
||||
provider_id = active_provider.id
|
||||
request.params["provider_id"] = provider_id
|
||||
@@ -86,9 +109,12 @@ async def create_task(
|
||||
db_provider = llm_service.get_provider(provider_id)
|
||||
if not db_provider:
|
||||
raise ValueError(f"LLM Provider {provider_id} not found")
|
||||
if request.plugin_id == "llm_dashboard_validation" and not is_multimodal_model(
|
||||
db_provider.default_model,
|
||||
db_provider.provider_type,
|
||||
if (
|
||||
request.plugin_id == "llm_dashboard_validation"
|
||||
and not is_multimodal_model(
|
||||
db_provider.default_model,
|
||||
db_provider.provider_type,
|
||||
)
|
||||
):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
||||
@@ -98,14 +124,16 @@ async def create_task(
|
||||
db.close()
|
||||
|
||||
task = await task_manager.create_task(
|
||||
plugin_id=request.plugin_id,
|
||||
params=request.params
|
||||
plugin_id=request.plugin_id, params=request.params
|
||||
)
|
||||
return task
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))
|
||||
|
||||
|
||||
# [/DEF:create_task:Function]
|
||||
|
||||
|
||||
# [DEF:list_tasks:Function]
|
||||
# @COMPLEXITY: 2
|
||||
# @PURPOSE: Retrieve a list of tasks with pagination and optional status filter.
|
||||
@@ -116,16 +144,24 @@ async def create_task(
|
||||
# @PRE: task_manager must be available.
|
||||
# @POST: Returns a list of tasks.
|
||||
# @RETURN: List[Task] - List of tasks.
|
||||
# @RELATION: CALLS -> [TaskManager]
|
||||
# @RELATION: BINDS_TO -> [TASK_TYPE_PLUGIN_MAP]
|
||||
@router.get("", response_model=List[Task])
|
||||
async def list_tasks(
|
||||
limit: int = 10,
|
||||
offset: int = 0,
|
||||
status_filter: Optional[TaskStatus] = Query(None, alias="status"),
|
||||
task_type: Optional[str] = Query(None, description="Task category: llm_validation, backup, migration"),
|
||||
plugin_id: Optional[List[str]] = Query(None, description="Filter by plugin_id (repeatable query param)"),
|
||||
completed_only: bool = Query(False, description="Return only completed tasks (SUCCESS/FAILED)"),
|
||||
task_type: Optional[str] = Query(
|
||||
None, description="Task category: llm_validation, backup, migration"
|
||||
),
|
||||
plugin_id: Optional[List[str]] = Query(
|
||||
None, description="Filter by plugin_id (repeatable query param)"
|
||||
),
|
||||
completed_only: bool = Query(
|
||||
False, description="Return only completed tasks (SUCCESS/FAILED)"
|
||||
),
|
||||
task_manager: TaskManager = Depends(get_task_manager),
|
||||
_ = Depends(has_permission("tasks", "READ"))
|
||||
_=Depends(has_permission("tasks", "READ")),
|
||||
):
|
||||
with belief_scope("list_tasks"):
|
||||
plugin_filters = list(plugin_id) if plugin_id else []
|
||||
@@ -133,7 +169,7 @@ async def list_tasks(
|
||||
if task_type not in TASK_TYPE_PLUGIN_MAP:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"Unsupported task_type '{task_type}'. Allowed: {', '.join(TASK_TYPE_PLUGIN_MAP.keys())}"
|
||||
detail=f"Unsupported task_type '{task_type}'. Allowed: {', '.join(TASK_TYPE_PLUGIN_MAP.keys())}",
|
||||
)
|
||||
plugin_filters.extend(TASK_TYPE_PLUGIN_MAP[task_type])
|
||||
|
||||
@@ -142,10 +178,13 @@ async def list_tasks(
|
||||
offset=offset,
|
||||
status=status_filter,
|
||||
plugin_ids=plugin_filters or None,
|
||||
completed_only=completed_only
|
||||
completed_only=completed_only,
|
||||
)
|
||||
|
||||
|
||||
# [/DEF:list_tasks:Function]
|
||||
|
||||
|
||||
# [DEF:get_task:Function]
|
||||
# @COMPLEXITY: 2
|
||||
# @PURPOSE: Retrieve the details of a specific task.
|
||||
@@ -154,19 +193,25 @@ async def list_tasks(
|
||||
# @PRE: task_id must exist.
|
||||
# @POST: Returns task details or raises 404.
|
||||
# @RETURN: Task - The task details.
|
||||
# @RELATION: CALLS -> [TaskManager]
|
||||
@router.get("/{task_id}", response_model=Task)
|
||||
async def get_task(
|
||||
task_id: str,
|
||||
task_manager: TaskManager = Depends(get_task_manager),
|
||||
_ = Depends(has_permission("tasks", "READ"))
|
||||
_=Depends(has_permission("tasks", "READ")),
|
||||
):
|
||||
with belief_scope("get_task"):
|
||||
task = task_manager.get_task(task_id)
|
||||
if not task:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Task not found")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND, detail="Task not found"
|
||||
)
|
||||
return task
|
||||
|
||||
|
||||
# [/DEF:get_task:Function]
|
||||
|
||||
|
||||
# [DEF:get_task_logs:Function]
|
||||
# @COMPLEXITY: 5
|
||||
# @PURPOSE: Retrieve logs for a specific task with optional filtering.
|
||||
@@ -180,6 +225,8 @@ async def get_task(
|
||||
# @PRE: task_id must exist.
|
||||
# @POST: Returns a list of log entries or raises 404.
|
||||
# @RETURN: List[LogEntry] - List of log entries.
|
||||
# @RELATION: CALLS -> [TaskManager]
|
||||
# @RELATION: DEPENDS_ON -> [LogFilter]
|
||||
# @TEST_CONTRACT: TaskLogQueryInput -> List[LogEntry]
|
||||
# @TEST_SCENARIO: existing_task_logs_filtered -> Returns filtered logs by level/source/search with pagination.
|
||||
# @TEST_FIXTURE: valid_task_with_mixed_logs -> backend/tests/fixtures/task_logs/valid_task_with_mixed_logs.json
|
||||
@@ -190,28 +237,37 @@ async def get_task(
|
||||
@router.get("/{task_id}/logs")
|
||||
async def get_task_logs(
|
||||
task_id: str,
|
||||
level: Optional[str] = Query(None, description="Filter by log level (DEBUG, INFO, WARNING, ERROR)"),
|
||||
level: Optional[str] = Query(
|
||||
None, description="Filter by log level (DEBUG, INFO, WARNING, ERROR)"
|
||||
),
|
||||
source: Optional[str] = Query(None, description="Filter by source component"),
|
||||
search: Optional[str] = Query(None, description="Text search in message"),
|
||||
offset: int = Query(0, ge=0, description="Number of logs to skip"),
|
||||
limit: int = Query(100, ge=1, le=1000, description="Maximum number of logs to return"),
|
||||
limit: int = Query(
|
||||
100, ge=1, le=1000, description="Maximum number of logs to return"
|
||||
),
|
||||
task_manager: TaskManager = Depends(get_task_manager),
|
||||
):
|
||||
with belief_scope("get_task_logs"):
|
||||
task = task_manager.get_task(task_id)
|
||||
if not task:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Task not found")
|
||||
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND, detail="Task not found"
|
||||
)
|
||||
|
||||
log_filter = LogFilter(
|
||||
level=level.upper() if level else None,
|
||||
source=source,
|
||||
search=search,
|
||||
offset=offset,
|
||||
limit=limit
|
||||
limit=limit,
|
||||
)
|
||||
return task_manager.get_task_logs(task_id, log_filter)
|
||||
|
||||
|
||||
# [/DEF:get_task_logs:Function]
|
||||
|
||||
|
||||
# [DEF:get_task_log_stats:Function]
|
||||
# @COMPLEXITY: 2
|
||||
# @PURPOSE: Get statistics about logs for a task (counts by level and source).
|
||||
@@ -220,6 +276,8 @@ async def get_task_logs(
|
||||
# @PRE: task_id must exist.
|
||||
# @POST: Returns log statistics or raises 404.
|
||||
# @RETURN: LogStats - Statistics about task logs.
|
||||
# @RELATION: CALLS -> [TaskManager]
|
||||
# @RELATION: DEPENDS_ON -> [LogStats]
|
||||
@router.get("/{task_id}/logs/stats", response_model=LogStats)
|
||||
async def get_task_log_stats(
|
||||
task_id: str,
|
||||
@@ -228,26 +286,37 @@ async def get_task_log_stats(
|
||||
with belief_scope("get_task_log_stats"):
|
||||
task = task_manager.get_task(task_id)
|
||||
if not task:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Task not found")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND, detail="Task not found"
|
||||
)
|
||||
stats_payload = task_manager.get_task_log_stats(task_id)
|
||||
if isinstance(stats_payload, LogStats):
|
||||
return stats_payload
|
||||
if isinstance(stats_payload, dict) and (
|
||||
"total_count" in stats_payload or "by_level" in stats_payload or "by_source" in stats_payload
|
||||
"total_count" in stats_payload
|
||||
or "by_level" in stats_payload
|
||||
or "by_source" in stats_payload
|
||||
):
|
||||
return LogStats(
|
||||
total_count=int(stats_payload.get("total_count", 0) or 0),
|
||||
by_level=dict(stats_payload.get("by_level") or {}),
|
||||
by_source=dict(stats_payload.get("by_source") or {}),
|
||||
)
|
||||
flat_by_level = dict(stats_payload or {}) if isinstance(stats_payload, dict) else {}
|
||||
flat_by_level = (
|
||||
dict(stats_payload or {}) if isinstance(stats_payload, dict) else {}
|
||||
)
|
||||
return LogStats(
|
||||
total_count=sum(int(value or 0) for value in flat_by_level.values()),
|
||||
by_level={str(key): int(value or 0) for key, value in flat_by_level.items()},
|
||||
by_level={
|
||||
str(key): int(value or 0) for key, value in flat_by_level.items()
|
||||
},
|
||||
by_source={},
|
||||
)
|
||||
|
||||
|
||||
# [/DEF:get_task_log_stats:Function]
|
||||
|
||||
|
||||
# [DEF:get_task_log_sources:Function]
|
||||
# @COMPLEXITY: 2
|
||||
# @PURPOSE: Get unique sources for a task's logs.
|
||||
@@ -256,6 +325,7 @@ async def get_task_log_stats(
|
||||
# @PRE: task_id must exist.
|
||||
# @POST: Returns list of unique source names or raises 404.
|
||||
# @RETURN: List[str] - Unique source names.
|
||||
# @RELATION: CALLS -> [TaskManager]
|
||||
@router.get("/{task_id}/logs/sources", response_model=List[str])
|
||||
async def get_task_log_sources(
|
||||
task_id: str,
|
||||
@@ -264,10 +334,15 @@ async def get_task_log_sources(
|
||||
with belief_scope("get_task_log_sources"):
|
||||
task = task_manager.get_task(task_id)
|
||||
if not task:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Task not found")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND, detail="Task not found"
|
||||
)
|
||||
return task_manager.get_task_log_sources(task_id)
|
||||
|
||||
|
||||
# [/DEF:get_task_log_sources:Function]
|
||||
|
||||
|
||||
# [DEF:resolve_task:Function]
|
||||
# @COMPLEXITY: 2
|
||||
# @PURPOSE: Resolve a task that is awaiting mapping.
|
||||
@@ -277,12 +352,13 @@ async def get_task_log_sources(
|
||||
# @PRE: task must be in AWAITING_MAPPING status.
|
||||
# @POST: Task is resolved and resumes execution.
|
||||
# @RETURN: Task - The updated task object.
|
||||
# @RELATION: CALLS -> [TaskManager]
|
||||
@router.post("/{task_id}/resolve", response_model=Task)
|
||||
async def resolve_task(
|
||||
task_id: str,
|
||||
request: ResolveTaskRequest,
|
||||
task_manager: TaskManager = Depends(get_task_manager),
|
||||
_ = Depends(has_permission("tasks", "WRITE"))
|
||||
_=Depends(has_permission("tasks", "WRITE")),
|
||||
):
|
||||
with belief_scope("resolve_task"):
|
||||
try:
|
||||
@@ -290,8 +366,11 @@ async def resolve_task(
|
||||
return task_manager.get_task(task_id)
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
|
||||
|
||||
|
||||
# [/DEF:resolve_task:Function]
|
||||
|
||||
|
||||
# [DEF:resume_task:Function]
|
||||
# @COMPLEXITY: 2
|
||||
# @PURPOSE: Resume a task that is awaiting input (e.g., passwords).
|
||||
@@ -301,12 +380,13 @@ async def resolve_task(
|
||||
# @PRE: task must be in AWAITING_INPUT status.
|
||||
# @POST: Task resumes execution with provided input.
|
||||
# @RETURN: Task - The updated task object.
|
||||
# @RELATION: CALLS -> [TaskManager]
|
||||
@router.post("/{task_id}/resume", response_model=Task)
|
||||
async def resume_task(
|
||||
task_id: str,
|
||||
request: ResumeTaskRequest,
|
||||
task_manager: TaskManager = Depends(get_task_manager),
|
||||
_ = Depends(has_permission("tasks", "WRITE"))
|
||||
_=Depends(has_permission("tasks", "WRITE")),
|
||||
):
|
||||
with belief_scope("resume_task"):
|
||||
try:
|
||||
@@ -314,8 +394,11 @@ async def resume_task(
|
||||
return task_manager.get_task(task_id)
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
|
||||
|
||||
|
||||
# [/DEF:resume_task:Function]
|
||||
|
||||
|
||||
# [DEF:clear_tasks:Function]
|
||||
# @COMPLEXITY: 2
|
||||
# @PURPOSE: Clear tasks matching the status filter.
|
||||
@@ -323,15 +406,18 @@ async def resume_task(
|
||||
# @PARAM: task_manager (TaskManager) - The task manager instance.
|
||||
# @PRE: task_manager is available.
|
||||
# @POST: Tasks are removed from memory/persistence.
|
||||
# @RELATION: CALLS -> [TaskManager]
|
||||
@router.delete("", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def clear_tasks(
|
||||
status: Optional[TaskStatus] = None,
|
||||
task_manager: TaskManager = Depends(get_task_manager),
|
||||
_ = Depends(has_permission("tasks", "WRITE"))
|
||||
_=Depends(has_permission("tasks", "WRITE")),
|
||||
):
|
||||
with belief_scope("clear_tasks", f"status={status}"):
|
||||
task_manager.clear_tasks(status)
|
||||
return
|
||||
|
||||
|
||||
# [/DEF:clear_tasks:Function]
|
||||
|
||||
# [/DEF:TasksRouter:Module]
|
||||
|
||||
Reference in New Issue
Block a user