semantic update

This commit is contained in:
2026-02-24 21:08:12 +03:00
parent 7a12ed0931
commit 95ae9c6af1
32 changed files with 60376 additions and 59911 deletions

View File

@@ -32,27 +32,28 @@ router = APIRouter(prefix="/api/reports", tags=["Reports"])
# @PARAM: field_name (str) - Query field name for diagnostics.
# @RETURN: List - Parsed enum values.
def _parse_csv_enum_list(raw: Optional[str], enum_cls, field_name: str) -> List:
if raw is None or not raw.strip():
return []
values = [item.strip() for item in raw.split(",") if item.strip()]
parsed = []
invalid = []
for value in values:
try:
parsed.append(enum_cls(value))
except ValueError:
invalid.append(value)
if invalid:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail={
"message": f"Invalid values for '{field_name}'",
"field": field_name,
"invalid_values": invalid,
"allowed_values": [item.value for item in enum_cls],
},
)
return parsed
with belief_scope("_parse_csv_enum_list"):
if raw is None or not raw.strip():
return []
values = [item.strip() for item in raw.split(",") if item.strip()]
parsed = []
invalid = []
for value in values:
try:
parsed.append(enum_cls(value))
except ValueError:
invalid.append(value)
if invalid:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail={
"message": f"Invalid values for '{field_name}'",
"field": field_name,
"invalid_values": invalid,
"allowed_values": [item.value for item in enum_cls],
},
)
return parsed
# [/DEF:_parse_csv_enum_list:Function]

View File

@@ -21,7 +21,7 @@ import asyncio
from .dependencies import get_task_manager, get_scheduler_service
from .core.utils.network import NetworkError
from .core.logger import logger, belief_scope
from .api.routes import plugins, tasks, settings, environments, mappings, migration, connections, git, storage, admin, llm, dashboards, datasets, reports, assistant
from .api.routes import plugins, tasks, settings, environments, mappings, migration, connections, git, storage, admin, llm, dashboards, datasets, reports, assistant
from .api import auth
# [DEF:App:Global]
@@ -72,12 +72,12 @@ app.add_middleware(
)
# [DEF:log_requests:Function]
# @PURPOSE: Middleware to log incoming HTTP requests and their response status.
# [DEF:network_error_handler:Function]
# @PURPOSE: Global exception handler for NetworkError.
# @PRE: request is a FastAPI Request object.
# @POST: Logs request and response details.
# @POST: Returns 503 HTTP Exception.
# @PARAM: request (Request) - The incoming request object.
# @PARAM: call_next (Callable) - The next middleware or route handler.
# @PARAM: exc (NetworkError) - The exception instance.
@app.exception_handler(NetworkError)
async def network_error_handler(request: Request, exc: NetworkError):
with belief_scope("network_error_handler"):
@@ -86,26 +86,34 @@ async def network_error_handler(request: Request, exc: NetworkError):
status_code=503,
detail="Environment unavailable. Please check if the Superset instance is running."
)
# [/DEF:network_error_handler:Function]
# [DEF:log_requests:Function]
# @PURPOSE: Middleware to log incoming HTTP requests and their response status.
# @PRE: request is a FastAPI Request object.
# @POST: Logs request and response details.
# @PARAM: request (Request) - The incoming request object.
# @PARAM: call_next (Callable) - The next middleware or route handler.
@app.middleware("http")
async def log_requests(request: Request, call_next):
# Avoid spamming logs for polling endpoints
is_polling = request.url.path.endswith("/api/tasks") and request.method == "GET"
if not is_polling:
logger.info(f"Incoming request: {request.method} {request.url.path}")
try:
response = await call_next(request)
with belief_scope("log_requests"):
# Avoid spamming logs for polling endpoints
is_polling = request.url.path.endswith("/api/tasks") and request.method == "GET"
if not is_polling:
logger.info(f"Response status: {response.status_code} for {request.url.path}")
return response
except NetworkError as e:
logger.error(f"Network error caught in middleware: {e}")
raise HTTPException(
status_code=503,
detail="Environment unavailable. Please check if the Superset instance is running."
)
logger.info(f"Incoming request: {request.method} {request.url.path}")
try:
response = await call_next(request)
if not is_polling:
logger.info(f"Response status: {response.status_code} for {request.url.path}")
return response
except NetworkError as e:
logger.error(f"Network error caught in middleware: {e}")
raise HTTPException(
status_code=503,
detail="Environment unavailable. Please check if the Superset instance is running."
)
# [/DEF:log_requests:Function]
# Include API routes
@@ -119,12 +127,12 @@ app.include_router(environments.router, tags=["Environments"])
app.include_router(mappings.router, prefix="/api/mappings", tags=["Mappings"])
app.include_router(migration.router)
app.include_router(git.router, prefix="/api/git", tags=["Git"])
app.include_router(llm.router, prefix="/api/llm", tags=["LLM"])
app.include_router(storage.router, prefix="/api/storage", tags=["Storage"])
app.include_router(dashboards.router)
app.include_router(datasets.router)
app.include_router(reports.router)
app.include_router(assistant.router, prefix="/api/assistant", tags=["Assistant"])
app.include_router(llm.router, prefix="/api/llm", tags=["LLM"])
app.include_router(storage.router, prefix="/api/storage", tags=["Storage"])
app.include_router(dashboards.router)
app.include_router(datasets.router)
app.include_router(reports.router)
app.include_router(assistant.router, prefix="/api/assistant", tags=["Assistant"])
# [DEF:api.include_routers:Action]
@@ -249,12 +257,13 @@ if frontend_path.exists():
# @POST: Returns the requested file or index.html.
@app.get("/{file_path:path}", include_in_schema=False)
async def serve_spa(file_path: str):
# Only serve SPA for non-API paths
# API routes are registered separately and should be matched by FastAPI first
if file_path and (file_path.startswith("api/") or file_path.startswith("/api/") or file_path == "api"):
# This should not happen if API routers are properly registered
# Return 404 instead of serving HTML
raise HTTPException(status_code=404, detail=f"API endpoint not found: {file_path}")
with belief_scope("serve_spa"):
# Only serve SPA for non-API paths
# API routes are registered separately and should be matched by FastAPI first
if file_path and (file_path.startswith("api/") or file_path.startswith("/api/") or file_path == "api"):
# This should not happen if API routers are properly registered
# Return 404 instead of serving HTML
raise HTTPException(status_code=404, detail=f"API endpoint not found: {file_path}")
full_path = frontend_path / file_path
if file_path and full_path.is_file():

View File

@@ -35,7 +35,19 @@ class BeliefFormatter(logging.Formatter):
def format(self, record):
anchor_id = getattr(_belief_state, 'anchor_id', None)
if anchor_id:
record.msg = f"[{anchor_id}][Action] {record.msg}"
msg = str(record.msg)
# Supported molecular topology markers
markers = ("[EXPLORE]", "[REASON]", "[REFLECT]", "[COHERENCE:", "[Action]", "[Entry]", "[Exit]")
# Avoid duplicating anchor or overriding explicit markers
if msg.startswith(f"[{anchor_id}]"):
pass
elif any(msg.startswith(m) for m in markers):
record.msg = f"[{anchor_id}]{msg}"
else:
# Default covalent bond
record.msg = f"[{anchor_id}][Action] {msg}"
return super().format(record)
# [/DEF:format:Function]
# [/DEF:BeliefFormatter:Class]
@@ -75,12 +87,12 @@ def belief_scope(anchor_id: str, message: str = ""):
try:
yield
# Log Coherence OK and Exit (DEBUG level to reduce noise)
logger.debug(f"[{anchor_id}][Coherence:OK]")
logger.debug("[COHERENCE:OK]")
if _enable_belief_state:
logger.debug(f"[{anchor_id}][Exit]")
logger.debug("[Exit]")
except Exception as e:
# Log Coherence Failed (DEBUG level to reduce noise)
logger.debug(f"[{anchor_id}][Coherence:Failed] {str(e)}")
logger.debug(f"[COHERENCE:FAILED] {str(e)}")
raise
finally:
# Restore old anchor
@@ -275,5 +287,33 @@ logger.addHandler(websocket_log_handler)
# Example usage:
# logger.info("Application started", extra={"context_key": "context_value"})
# logger.error("An error occurred", exc_info=True)
import types
# [DEF:explore:Function]
# @PURPOSE: Logs an EXPLORE message (Van der Waals force) for searching, alternatives, and hypotheses.
# @SEMANTICS: log, explore, molecule
def explore(self, msg, *args, **kwargs):
self.warning(f"[EXPLORE] {msg}", *args, **kwargs)
# [/DEF:explore:Function]
# [DEF:reason:Function]
# @PURPOSE: Logs a REASON message (Covalent bond) for strict deduction and core logic.
# @SEMANTICS: log, reason, molecule
def reason(self, msg, *args, **kwargs):
self.info(f"[REASON] {msg}", *args, **kwargs)
# [/DEF:reason:Function]
# [DEF:reflect:Function]
# @PURPOSE: Logs a REFLECT message (Hydrogen bond) for self-check and structural validation.
# @SEMANTICS: log, reflect, molecule
def reflect(self, msg, *args, **kwargs):
self.debug(f"[REFLECT] {msg}", *args, **kwargs)
# [/DEF:reflect:Function]
logger.explore = types.MethodType(explore, logger)
logger.reason = types.MethodType(reason, logger)
logger.reflect = types.MethodType(reflect, logger)
# [/DEF:Logger:Global]
# [/DEF:LoggerModule:Module]

View File

@@ -6,9 +6,11 @@
# @TIER: CRITICAL
# @INVARIANT: Each TaskContext is bound to a single task execution.
# [SECTION: IMPORTS]
# [SECTION: IMPORTS]
from typing import Dict, Any, Callable
from .task_logger import TaskLogger
from ..logger import belief_scope
# [/SECTION]
# [DEF:TaskContext:Class]
@@ -44,13 +46,14 @@ class TaskContext:
params: Dict[str, Any],
default_source: str = "plugin"
):
self._task_id = task_id
self._params = params
self._logger = TaskLogger(
task_id=task_id,
add_log_fn=add_log_fn,
source=default_source
)
with belief_scope("__init__"):
self._task_id = task_id
self._params = params
self._logger = TaskLogger(
task_id=task_id,
add_log_fn=add_log_fn,
source=default_source
)
# [/DEF:__init__:Function]
# [DEF:task_id:Function]
@@ -60,7 +63,8 @@ class TaskContext:
# @RETURN: str - The task ID.
@property
def task_id(self) -> str:
return self._task_id
with belief_scope("task_id"):
return self._task_id
# [/DEF:task_id:Function]
# [DEF:logger:Function]
@@ -70,7 +74,8 @@ class TaskContext:
# @RETURN: TaskLogger - The logger instance.
@property
def logger(self) -> TaskLogger:
return self._logger
with belief_scope("logger"):
return self._logger
# [/DEF:logger:Function]
# [DEF:params:Function]
@@ -80,7 +85,8 @@ class TaskContext:
# @RETURN: Dict[str, Any] - The task parameters.
@property
def params(self) -> Dict[str, Any]:
return self._params
with belief_scope("params"):
return self._params
# [/DEF:params:Function]
# [DEF:get_param:Function]
@@ -91,7 +97,8 @@ class TaskContext:
# @PARAM: default (Any) - Default value if key not found.
# @RETURN: Any - Parameter value or default.
def get_param(self, key: str, default: Any = None) -> Any:
return self._params.get(key, default)
with belief_scope("get_param"):
return self._params.get(key, default)
# [/DEF:get_param:Function]
# [DEF:create_sub_context:Function]
@@ -102,12 +109,13 @@ class TaskContext:
# @RETURN: TaskContext - New context with different source.
def create_sub_context(self, source: str) -> "TaskContext":
"""Create a sub-context with a different default source for logging."""
return TaskContext(
task_id=self._task_id,
add_log_fn=self._logger._add_log,
params=self._params,
default_source=source
)
with belief_scope("create_sub_context"):
return TaskContext(
task_id=self._task_id,
add_log_fn=self._logger._add_log,
params=self._params,
default_source=source
)
# [/DEF:create_sub_context:Function]
# [/DEF:TaskContext:Class]

View File

@@ -1,4 +1,5 @@
# [DEF:TaskManagerModule:Module]
# @TIER: CRITICAL
# @SEMANTICS: task, manager, lifecycle, execution, state
# @PURPOSE: Manages the lifecycle of tasks, including their creation, execution, and state tracking. It uses a thread pool to run plugins asynchronously.
# @LAYER: Core
@@ -74,9 +75,10 @@ class TaskManager:
# @POST: Logs are batch-written to database every LOG_FLUSH_INTERVAL seconds.
def _flusher_loop(self):
"""Background thread that flushes log buffer to database."""
while not self._flusher_stop_event.is_set():
self._flush_logs()
self._flusher_stop_event.wait(self.LOG_FLUSH_INTERVAL)
with belief_scope("_flusher_loop"):
while not self._flusher_stop_event.is_set():
self._flush_logs()
self._flusher_stop_event.wait(self.LOG_FLUSH_INTERVAL)
# [/DEF:_flusher_loop:Function]
# [DEF:_flush_logs:Function]
@@ -85,23 +87,24 @@ class TaskManager:
# @POST: All buffered logs are written to task_logs table.
def _flush_logs(self):
"""Flush all buffered logs to the database."""
with self._log_buffer_lock:
task_ids = list(self._log_buffer.keys())
for task_id in task_ids:
with belief_scope("_flush_logs"):
with self._log_buffer_lock:
logs = self._log_buffer.pop(task_id, [])
task_ids = list(self._log_buffer.keys())
if logs:
try:
self.log_persistence_service.add_logs(task_id, logs)
except Exception as e:
logger.error(f"Failed to flush logs for task {task_id}: {e}")
# Re-add logs to buffer on failure
with self._log_buffer_lock:
if task_id not in self._log_buffer:
self._log_buffer[task_id] = []
self._log_buffer[task_id].extend(logs)
for task_id in task_ids:
with self._log_buffer_lock:
logs = self._log_buffer.pop(task_id, [])
if logs:
try:
self.log_persistence_service.add_logs(task_id, logs)
except Exception as e:
logger.error(f"Failed to flush logs for task {task_id}: {e}")
# Re-add logs to buffer on failure
with self._log_buffer_lock:
if task_id not in self._log_buffer:
self._log_buffer[task_id] = []
self._log_buffer[task_id].extend(logs)
# [/DEF:_flush_logs:Function]
# [DEF:_flush_task_logs:Function]
@@ -111,14 +114,15 @@ class TaskManager:
# @PARAM: task_id (str) - The task ID.
def _flush_task_logs(self, task_id: str):
"""Flush logs for a specific task immediately."""
with self._log_buffer_lock:
logs = self._log_buffer.pop(task_id, [])
if logs:
try:
self.log_persistence_service.add_logs(task_id, logs)
except Exception as e:
logger.error(f"Failed to flush logs for task {task_id}: {e}")
with belief_scope("_flush_task_logs"):
with self._log_buffer_lock:
logs = self._log_buffer.pop(task_id, [])
if logs:
try:
self.log_persistence_service.add_logs(task_id, logs)
except Exception as e:
logger.error(f"Failed to flush logs for task {task_id}: {e}")
# [/DEF:_flush_task_logs:Function]
# [DEF:create_task:Function]

View File

@@ -1,4 +1,5 @@
# [DEF:TaskPersistenceModule:Module]
# @TIER: CRITICAL
# @SEMANTICS: persistence, sqlite, sqlalchemy, task, storage
# @PURPOSE: Handles the persistence of tasks using SQLAlchemy and the tasks.db database.
# @LAYER: Core
@@ -19,42 +20,65 @@ from ..logger import logger, belief_scope
# [/SECTION]
# [DEF:TaskPersistenceService:Class]
# @TIER: CRITICAL
# @SEMANTICS: persistence, service, database, sqlalchemy
# @PURPOSE: Provides methods to save and load tasks from the tasks.db database using SQLAlchemy.
# @INVARIANT: Persistence must handle potentially missing task fields natively.
class TaskPersistenceService:
# [DEF:_json_load_if_needed:Function]
# @PURPOSE: Safely load JSON strings from DB if necessary
# @PRE: value is an arbitrary database value
# @POST: Returns parsed JSON object, list, string, or primitive
@staticmethod
def _json_load_if_needed(value):
if value is None:
return None
if isinstance(value, (dict, list)):
return value
if isinstance(value, str):
stripped = value.strip()
if stripped == "" or stripped.lower() == "null":
with belief_scope("TaskPersistenceService._json_load_if_needed"):
if value is None:
return None
try:
return json.loads(stripped)
except json.JSONDecodeError:
if isinstance(value, (dict, list)):
return value
return value
if isinstance(value, str):
stripped = value.strip()
if stripped == "" or stripped.lower() == "null":
return None
try:
return json.loads(stripped)
except json.JSONDecodeError:
return value
return value
# [/DEF:_json_load_if_needed:Function]
# [DEF:_parse_datetime:Function]
# @PURPOSE: Safely parse a datetime string from the database
# @PRE: value is an ISO string or datetime object
# @POST: Returns datetime object or None
@staticmethod
def _parse_datetime(value):
if value is None or isinstance(value, datetime):
return value
if isinstance(value, str):
try:
return datetime.fromisoformat(value)
except ValueError:
return None
return None
@staticmethod
def _resolve_environment_id(session: Session, env_id: Optional[str]) -> Optional[str]:
if not env_id:
with belief_scope("TaskPersistenceService._parse_datetime"):
if value is None or isinstance(value, datetime):
return value
if isinstance(value, str):
try:
return datetime.fromisoformat(value)
except ValueError:
return None
return None
exists = session.query(Environment.id).filter(Environment.id == env_id).first()
return env_id if exists else None
# [/DEF:_parse_datetime:Function]
# [DEF:_resolve_environment_id:Function]
# @TIER: STANDARD
# @PURPOSE: Resolve environment id based on provided value or fallback to default
# @PRE: Session is active
# @POST: Environment ID is returned
@staticmethod
def _resolve_environment_id(session: Session, env_id: Optional[str]) -> str:
with belief_scope("_resolve_environment_id"):
if env_id:
return env_id
repo_env = session.query(Environment).filter_by(name="default").first()
if repo_env:
return str(repo_env.id)
return "default"
# [/DEF:_resolve_environment_id:Function]
# [DEF:__init__:Function]
# @PURPOSE: Initializes the persistence service.
@@ -90,13 +114,14 @@ class TaskPersistenceService:
# Ensure params and result are JSON serializable
def json_serializable(obj):
if isinstance(obj, dict):
return {k: json_serializable(v) for k, v in obj.items()}
elif isinstance(obj, list):
return [json_serializable(v) for v in obj]
elif isinstance(obj, datetime):
return obj.isoformat()
return obj
with belief_scope("TaskPersistenceService.json_serializable"):
if isinstance(obj, dict):
return {k: json_serializable(v) for k, v in obj.items()}
elif isinstance(obj, list):
return [json_serializable(v) for v in obj]
elif isinstance(obj, datetime):
return obj.isoformat()
return obj
record.params = json_serializable(task.params)
record.result = json_serializable(task.result)
@@ -227,9 +252,11 @@ class TaskLogPersistenceService:
"""
# [DEF:__init__:Function]
# @PURPOSE: Initialize the log persistence service.
# @POST: Service is ready.
def __init__(self):
# @TIER: STANDARD
# @PURPOSE: Initializes the TaskLogPersistenceService
# @PRE: config is provided or defaults are used
# @POST: Service is ready for log persistence
def __init__(self, config=None):
pass
# [/DEF:__init__:Function]

View File

@@ -15,6 +15,7 @@ from typing import Dict, Any, Optional, Callable
# @PURPOSE: A wrapper around TaskManager._add_log that carries task_id and source context.
# @TIER: CRITICAL
# @INVARIANT: All log calls include the task_id and source.
# @TEST_DATA: task_logger -> {"task_id": "test_123", "source": "test_plugin"}
# @UX_STATE: Idle -> Logging -> (system records log)
class TaskLogger:
"""
@@ -71,6 +72,7 @@ class TaskLogger:
# @PARAM: message (str) - Log message.
# @PARAM: source (Optional[str]) - Override source for this log entry.
# @PARAM: metadata (Optional[Dict]) - Additional structured data.
# @UX_STATE: Logging -> (writing internal log)
def _log(
self,
level: str,
@@ -90,6 +92,8 @@ class TaskLogger:
# [DEF:debug:Function]
# @PURPOSE: Log a DEBUG level message.
# @PRE: message is a string.
# @POST: Log entry added via internally with DEBUG level.
# @PARAM: message (str) - Log message.
# @PARAM: source (Optional[str]) - Override source.
# @PARAM: metadata (Optional[Dict]) - Additional data.
@@ -104,6 +108,8 @@ class TaskLogger:
# [DEF:info:Function]
# @PURPOSE: Log an INFO level message.
# @PRE: message is a string.
# @POST: Log entry added internally with INFO level.
# @PARAM: message (str) - Log message.
# @PARAM: source (Optional[str]) - Override source.
# @PARAM: metadata (Optional[Dict]) - Additional data.
@@ -118,6 +124,8 @@ class TaskLogger:
# [DEF:warning:Function]
# @PURPOSE: Log a WARNING level message.
# @PRE: message is a string.
# @POST: Log entry added internally with WARNING level.
# @PARAM: message (str) - Log message.
# @PARAM: source (Optional[str]) - Override source.
# @PARAM: metadata (Optional[Dict]) - Additional data.
@@ -132,6 +140,8 @@ class TaskLogger:
# [DEF:error:Function]
# @PURPOSE: Log an ERROR level message.
# @PRE: message is a string.
# @POST: Log entry added internally with ERROR level.
# @PARAM: message (str) - Log message.
# @PARAM: source (Optional[str]) - Override source.
# @PARAM: metadata (Optional[Dict]) - Additional data.

View File

@@ -16,6 +16,9 @@ from pydantic import BaseModel, Field, field_validator, model_validator
# [DEF:TaskType:Class]
# @TIER: CRITICAL
# @INVARIANT: Must contain valid generic task type mappings.
# @SEMANTICS: enum, type, task
# @PURPOSE: Supported normalized task report types.
class TaskType(str, Enum):
LLM_VERIFICATION = "llm_verification"
@@ -27,6 +30,9 @@ class TaskType(str, Enum):
# [DEF:ReportStatus:Class]
# @TIER: CRITICAL
# @INVARIANT: TaskStatus enum mapping logic holds.
# @SEMANTICS: enum, status, task
# @PURPOSE: Supported normalized report status values.
class ReportStatus(str, Enum):
SUCCESS = "success"
@@ -37,6 +43,9 @@ class ReportStatus(str, Enum):
# [DEF:ErrorContext:Class]
# @TIER: CRITICAL
# @INVARIANT: The properties accurately describe error state.
# @SEMANTICS: error, context, payload
# @PURPOSE: Error and recovery context for failed/partial reports.
class ErrorContext(BaseModel):
code: Optional[str] = None
@@ -46,6 +55,9 @@ class ErrorContext(BaseModel):
# [DEF:TaskReport:Class]
# @TIER: CRITICAL
# @INVARIANT: Must represent canonical task record attributes.
# @SEMANTICS: report, model, summary
# @PURPOSE: Canonical normalized report envelope for one task execution.
class TaskReport(BaseModel):
report_id: str
@@ -69,6 +81,9 @@ class TaskReport(BaseModel):
# [DEF:ReportQuery:Class]
# @TIER: CRITICAL
# @INVARIANT: Time and pagination queries are mutually consistent.
# @SEMANTICS: query, filter, search
# @PURPOSE: Query object for server-side report filtering, sorting, and pagination.
class ReportQuery(BaseModel):
page: int = Field(default=1, ge=1)
@@ -105,6 +120,9 @@ class ReportQuery(BaseModel):
# [DEF:ReportCollection:Class]
# @TIER: CRITICAL
# @INVARIANT: Represents paginated data correctly.
# @SEMANTICS: collection, pagination
# @PURPOSE: Paginated collection of normalized task reports.
class ReportCollection(BaseModel):
items: List[TaskReport]
@@ -117,6 +135,9 @@ class ReportCollection(BaseModel):
# [DEF:ReportDetailView:Class]
# @TIER: CRITICAL
# @INVARIANT: Incorporates a report and logs correctly.
# @SEMANTICS: view, detail, logs
# @PURPOSE: Detailed report representation including diagnostics and recovery actions.
class ReportDetailView(BaseModel):
report: TaskReport

View File

@@ -33,7 +33,8 @@ class EncryptionManager:
# @PRE: data must be a non-empty string.
# @POST: Returns encrypted string.
def encrypt(self, data: str) -> str:
return self.fernet.encrypt(data.encode()).decode()
with belief_scope("encrypt"):
return self.fernet.encrypt(data.encode()).decode()
# [/DEF:EncryptionManager.encrypt:Function]
# [DEF:EncryptionManager.decrypt:Function]
@@ -41,7 +42,8 @@ class EncryptionManager:
# @PRE: encrypted_data must be a valid Fernet-encrypted string.
# @POST: Returns original plaintext string.
def decrypt(self, encrypted_data: str) -> str:
return self.fernet.decrypt(encrypted_data.encode()).decode()
with belief_scope("decrypt"):
return self.fernet.decrypt(encrypted_data.encode()).decode()
# [/DEF:EncryptionManager.decrypt:Function]
# [/DEF:EncryptionManager:Class]

View File

@@ -12,6 +12,7 @@
from datetime import datetime
from typing import Any, Dict, Optional
from ...core.logger import belief_scope
from ...core.task_manager.models import Task, TaskStatus
from ...models.report import ErrorContext, ReportStatus, TaskReport
from .type_profiles import get_type_profile, resolve_task_type
@@ -25,14 +26,15 @@ from .type_profiles import get_type_profile, resolve_task_type
# @PARAM: status (Any) - Internal task status value.
# @RETURN: ReportStatus - Canonical report status.
def status_to_report_status(status: Any) -> ReportStatus:
raw = str(status.value if isinstance(status, TaskStatus) else status).upper()
if raw == TaskStatus.SUCCESS.value:
return ReportStatus.SUCCESS
if raw == TaskStatus.FAILED.value:
return ReportStatus.FAILED
if raw in {TaskStatus.PENDING.value, TaskStatus.RUNNING.value, TaskStatus.AWAITING_INPUT.value, TaskStatus.AWAITING_MAPPING.value}:
return ReportStatus.IN_PROGRESS
return ReportStatus.PARTIAL
with belief_scope("status_to_report_status"):
raw = str(status.value if isinstance(status, TaskStatus) else status).upper()
if raw == TaskStatus.SUCCESS.value:
return ReportStatus.SUCCESS
if raw == TaskStatus.FAILED.value:
return ReportStatus.FAILED
if raw in {TaskStatus.PENDING.value, TaskStatus.RUNNING.value, TaskStatus.AWAITING_INPUT.value, TaskStatus.AWAITING_MAPPING.value}:
return ReportStatus.IN_PROGRESS
return ReportStatus.PARTIAL
# [/DEF:status_to_report_status:Function]
@@ -44,19 +46,20 @@ def status_to_report_status(status: Any) -> ReportStatus:
# @PARAM: report_status (ReportStatus) - Canonical status.
# @RETURN: str - Normalized summary.
def build_summary(task: Task, report_status: ReportStatus) -> str:
result = task.result
if isinstance(result, dict):
for key in ("summary", "message", "status_message", "description"):
value = result.get(key)
if isinstance(value, str) and value.strip():
return value.strip()
if report_status == ReportStatus.SUCCESS:
return "Task completed successfully"
if report_status == ReportStatus.FAILED:
return "Task failed"
if report_status == ReportStatus.IN_PROGRESS:
return "Task is in progress"
return "Task completed with partial data"
with belief_scope("build_summary"):
result = task.result
if isinstance(result, dict):
for key in ("summary", "message", "status_message", "description"):
value = result.get(key)
if isinstance(value, str) and value.strip():
return value.strip()
if report_status == ReportStatus.SUCCESS:
return "Task completed successfully"
if report_status == ReportStatus.FAILED:
return "Task failed"
if report_status == ReportStatus.IN_PROGRESS:
return "Task is in progress"
return "Task completed with partial data"
# [/DEF:build_summary:Function]
@@ -68,38 +71,39 @@ def build_summary(task: Task, report_status: ReportStatus) -> str:
# @PARAM: report_status (ReportStatus) - Canonical status.
# @RETURN: Optional[ErrorContext] - Error context block.
def extract_error_context(task: Task, report_status: ReportStatus) -> Optional[ErrorContext]:
if report_status not in {ReportStatus.FAILED, ReportStatus.PARTIAL}:
return None
with belief_scope("extract_error_context"):
if report_status not in {ReportStatus.FAILED, ReportStatus.PARTIAL}:
return None
result = task.result if isinstance(task.result, dict) else {}
message = None
code = None
next_actions = []
result = task.result if isinstance(task.result, dict) else {}
message = None
code = None
next_actions = []
if isinstance(result.get("error"), dict):
error_obj = result.get("error", {})
message = error_obj.get("message") or message
code = error_obj.get("code") or code
actions = error_obj.get("next_actions")
if isinstance(actions, list):
next_actions = [str(action) for action in actions if str(action).strip()]
if isinstance(result.get("error"), dict):
error_obj = result.get("error", {})
message = error_obj.get("message") or message
code = error_obj.get("code") or code
actions = error_obj.get("next_actions")
if isinstance(actions, list):
next_actions = [str(action) for action in actions if str(action).strip()]
if not message:
message = result.get("error_message") if isinstance(result.get("error_message"), str) else None
if not message:
message = result.get("error_message") if isinstance(result.get("error_message"), str) else None
if not message:
for log in reversed(task.logs):
if str(log.level).upper() == "ERROR" and log.message:
message = log.message
break
if not message:
for log in reversed(task.logs):
if str(log.level).upper() == "ERROR" and log.message:
message = log.message
break
if not message:
message = "Not provided"
if not message:
message = "Not provided"
if not next_actions:
next_actions = ["Review task diagnostics", "Retry the operation"]
if not next_actions:
next_actions = ["Review task diagnostics", "Retry the operation"]
return ErrorContext(code=code, message=message, next_actions=next_actions)
return ErrorContext(code=code, message=message, next_actions=next_actions)
# [/DEF:extract_error_context:Function]
@@ -110,43 +114,44 @@ def extract_error_context(task: Task, report_status: ReportStatus) -> Optional[E
# @PARAM: task (Task) - Source task.
# @RETURN: TaskReport - Canonical normalized report.
def normalize_task_report(task: Task) -> TaskReport:
task_type = resolve_task_type(task.plugin_id)
report_status = status_to_report_status(task.status)
profile = get_type_profile(task_type)
with belief_scope("normalize_task_report"):
task_type = resolve_task_type(task.plugin_id)
report_status = status_to_report_status(task.status)
profile = get_type_profile(task_type)
started_at = task.started_at if isinstance(task.started_at, datetime) else None
updated_at = task.finished_at if isinstance(task.finished_at, datetime) else None
if not updated_at:
updated_at = started_at or datetime.utcnow()
started_at = task.started_at if isinstance(task.started_at, datetime) else None
updated_at = task.finished_at if isinstance(task.finished_at, datetime) else None
if not updated_at:
updated_at = started_at or datetime.utcnow()
details: Dict[str, Any] = {
"profile": {
"display_label": profile.get("display_label"),
"visual_variant": profile.get("visual_variant"),
"icon_token": profile.get("icon_token"),
"emphasis_rules": profile.get("emphasis_rules", []),
},
"result": task.result if task.result is not None else {"note": "Not provided"},
}
details: Dict[str, Any] = {
"profile": {
"display_label": profile.get("display_label"),
"visual_variant": profile.get("visual_variant"),
"icon_token": profile.get("icon_token"),
"emphasis_rules": profile.get("emphasis_rules", []),
},
"result": task.result if task.result is not None else {"note": "Not provided"},
}
source_ref: Dict[str, Any] = {}
if isinstance(task.params, dict):
for key in ("environment_id", "source_env_id", "target_env_id", "dashboard_id", "dataset_id", "resource_id"):
if key in task.params:
source_ref[key] = task.params.get(key)
source_ref: Dict[str, Any] = {}
if isinstance(task.params, dict):
for key in ("environment_id", "source_env_id", "target_env_id", "dashboard_id", "dataset_id", "resource_id"):
if key in task.params:
source_ref[key] = task.params.get(key)
return TaskReport(
report_id=task.id,
task_id=task.id,
task_type=task_type,
status=report_status,
started_at=started_at,
updated_at=updated_at,
summary=build_summary(task, report_status),
details=details,
error_context=extract_error_context(task, report_status),
source_ref=source_ref or None,
)
return TaskReport(
report_id=task.id,
task_id=task.id,
task_type=task_type,
status=report_status,
started_at=started_at,
updated_at=updated_at,
summary=build_summary(task, report_status),
details=details,
error_context=extract_error_context(task, report_status),
source_ref=source_ref or None,
)
# [/DEF:normalize_task_report:Function]
# [/DEF:backend.src.services.reports.normalizer:Module]

View File

@@ -12,6 +12,8 @@
from datetime import datetime, timezone
from typing import List, Optional
from ...core.logger import belief_scope
from ...core.task_manager import TaskManager
from ...models.report import ReportCollection, ReportDetailView, ReportQuery, ReportStatus, TaskReport, TaskType
from .normalizer import normalize_task_report
@@ -33,7 +35,8 @@ class ReportsService:
# @INVARIANT: Constructor performs no task mutations.
# @PARAM: task_manager (TaskManager) - Task manager providing source task history.
def __init__(self, task_manager: TaskManager):
self.task_manager = task_manager
with belief_scope("__init__"):
self.task_manager = task_manager
# [/DEF:__init__:Function]
# [DEF:_load_normalized_reports:Function]
@@ -43,9 +46,10 @@ class ReportsService:
# @INVARIANT: Every returned item is a TaskReport.
# @RETURN: List[TaskReport] - Reports sorted later by list logic.
def _load_normalized_reports(self) -> List[TaskReport]:
tasks = self.task_manager.get_all_tasks()
reports = [normalize_task_report(task) for task in tasks]
return reports
with belief_scope("_load_normalized_reports"):
tasks = self.task_manager.get_all_tasks()
reports = [normalize_task_report(task) for task in tasks]
return reports
# [/DEF:_load_normalized_reports:Function]
# [DEF:_to_utc_datetime:Function]
@@ -56,11 +60,12 @@ class ReportsService:
# @PARAM: value (Optional[datetime]) - Source datetime value.
# @RETURN: Optional[datetime] - UTC-aware datetime or None.
def _to_utc_datetime(self, value: Optional[datetime]) -> Optional[datetime]:
if value is None:
return None
if value.tzinfo is None:
return value.replace(tzinfo=timezone.utc)
return value.astimezone(timezone.utc)
with belief_scope("_to_utc_datetime"):
if value is None:
return None
if value.tzinfo is None:
return value.replace(tzinfo=timezone.utc)
return value.astimezone(timezone.utc)
# [/DEF:_to_utc_datetime:Function]
# [DEF:_datetime_sort_key:Function]
@@ -71,10 +76,11 @@ class ReportsService:
# @PARAM: report (TaskReport) - Report item.
# @RETURN: float - UTC timestamp key.
def _datetime_sort_key(self, report: TaskReport) -> float:
updated = self._to_utc_datetime(report.updated_at)
if updated is None:
return 0.0
return updated.timestamp()
with belief_scope("_datetime_sort_key"):
updated = self._to_utc_datetime(report.updated_at)
if updated is None:
return 0.0
return updated.timestamp()
# [/DEF:_datetime_sort_key:Function]
# [DEF:_matches_query:Function]
@@ -86,24 +92,25 @@ class ReportsService:
# @PARAM: query (ReportQuery) - Applied query.
# @RETURN: bool - True if report matches all filters.
def _matches_query(self, report: TaskReport, query: ReportQuery) -> bool:
if query.task_types and report.task_type not in query.task_types:
return False
if query.statuses and report.status not in query.statuses:
return False
report_updated_at = self._to_utc_datetime(report.updated_at)
query_time_from = self._to_utc_datetime(query.time_from)
query_time_to = self._to_utc_datetime(query.time_to)
if query_time_from and report_updated_at and report_updated_at < query_time_from:
return False
if query_time_to and report_updated_at and report_updated_at > query_time_to:
return False
if query.search:
needle = query.search.lower()
haystack = f"{report.summary} {report.task_type.value} {report.status.value}".lower()
if needle not in haystack:
with belief_scope("_matches_query"):
if query.task_types and report.task_type not in query.task_types:
return False
return True
if query.statuses and report.status not in query.statuses:
return False
report_updated_at = self._to_utc_datetime(report.updated_at)
query_time_from = self._to_utc_datetime(query.time_from)
query_time_to = self._to_utc_datetime(query.time_to)
if query_time_from and report_updated_at and report_updated_at < query_time_from:
return False
if query_time_to and report_updated_at and report_updated_at > query_time_to:
return False
if query.search:
needle = query.search.lower()
haystack = f"{report.summary} {report.task_type.value} {report.status.value}".lower()
if needle not in haystack:
return False
return True
# [/DEF:_matches_query:Function]
# [DEF:_sort_reports:Function]
@@ -115,16 +122,17 @@ class ReportsService:
# @PARAM: query (ReportQuery) - Sort config.
# @RETURN: List[TaskReport] - Sorted reports.
def _sort_reports(self, reports: List[TaskReport], query: ReportQuery) -> List[TaskReport]:
reverse = query.sort_order == "desc"
with belief_scope("_sort_reports"):
reverse = query.sort_order == "desc"
if query.sort_by == "status":
reports.sort(key=lambda item: item.status.value, reverse=reverse)
elif query.sort_by == "task_type":
reports.sort(key=lambda item: item.task_type.value, reverse=reverse)
else:
reports.sort(key=self._datetime_sort_key, reverse=reverse)
if query.sort_by == "status":
reports.sort(key=lambda item: item.status.value, reverse=reverse)
elif query.sort_by == "task_type":
reports.sort(key=lambda item: item.task_type.value, reverse=reverse)
else:
reports.sort(key=self._datetime_sort_key, reverse=reverse)
return reports
return reports
# [/DEF:_sort_reports:Function]
# [DEF:list_reports:Function]
@@ -134,24 +142,25 @@ class ReportsService:
# @PARAM: query (ReportQuery) - List filters and pagination.
# @RETURN: ReportCollection - Paginated unified reports payload.
def list_reports(self, query: ReportQuery) -> ReportCollection:
reports = self._load_normalized_reports()
filtered = [report for report in reports if self._matches_query(report, query)]
sorted_reports = self._sort_reports(filtered, query)
with belief_scope("list_reports"):
reports = self._load_normalized_reports()
filtered = [report for report in reports if self._matches_query(report, query)]
sorted_reports = self._sort_reports(filtered, query)
total = len(sorted_reports)
start = (query.page - 1) * query.page_size
end = start + query.page_size
items = sorted_reports[start:end]
has_next = end < total
total = len(sorted_reports)
start = (query.page - 1) * query.page_size
end = start + query.page_size
items = sorted_reports[start:end]
has_next = end < total
return ReportCollection(
items=items,
total=total,
page=query.page,
page_size=query.page_size,
has_next=has_next,
applied_filters=query,
)
return ReportCollection(
items=items,
total=total,
page=query.page,
page_size=query.page_size,
has_next=has_next,
applied_filters=query,
)
# [/DEF:list_reports:Function]
# [DEF:get_report_detail:Function]
@@ -161,34 +170,35 @@ class ReportsService:
# @PARAM: report_id (str) - Stable report identifier.
# @RETURN: Optional[ReportDetailView] - Detailed report or None if not found.
def get_report_detail(self, report_id: str) -> Optional[ReportDetailView]:
reports = self._load_normalized_reports()
target = next((report for report in reports if report.report_id == report_id), None)
if not target:
return None
with belief_scope("get_report_detail"):
reports = self._load_normalized_reports()
target = next((report for report in reports if report.report_id == report_id), None)
if not target:
return None
timeline = []
if target.started_at:
timeline.append({"event": "started", "at": target.started_at.isoformat()})
timeline.append({"event": "updated", "at": target.updated_at.isoformat()})
timeline = []
if target.started_at:
timeline.append({"event": "started", "at": target.started_at.isoformat()})
timeline.append({"event": "updated", "at": target.updated_at.isoformat()})
diagnostics = target.details or {}
if not diagnostics:
diagnostics = {"note": "Not provided"}
if target.error_context:
diagnostics["error_context"] = target.error_context.model_dump()
diagnostics = target.details or {}
if not diagnostics:
diagnostics = {"note": "Not provided"}
if target.error_context:
diagnostics["error_context"] = target.error_context.model_dump()
next_actions = []
if target.error_context and target.error_context.next_actions:
next_actions = target.error_context.next_actions
elif target.status in {ReportStatus.FAILED, ReportStatus.PARTIAL}:
next_actions = ["Review diagnostics", "Retry task if applicable"]
next_actions = []
if target.error_context and target.error_context.next_actions:
next_actions = target.error_context.next_actions
elif target.status in {ReportStatus.FAILED, ReportStatus.PARTIAL}:
next_actions = ["Review diagnostics", "Retry task if applicable"]
return ReportDetailView(
report=target,
timeline=timeline,
diagnostics=diagnostics,
next_actions=next_actions,
)
return ReportDetailView(
report=target,
timeline=timeline,
diagnostics=diagnostics,
next_actions=next_actions,
)
# [/DEF:get_report_detail:Function]
# [/DEF:ReportsService:Class]

View File

@@ -9,6 +9,7 @@
# [SECTION: IMPORTS]
from typing import Any, Dict, Optional
from ...core.logger import belief_scope
from ...models.report import TaskType
# [/SECTION]
@@ -71,10 +72,11 @@ TASK_TYPE_PROFILES: Dict[TaskType, Dict[str, Any]] = {
# @PARAM: plugin_id (Optional[str]) - Source plugin/task identifier from task record.
# @RETURN: TaskType - Resolved canonical type or UNKNOWN fallback.
def resolve_task_type(plugin_id: Optional[str]) -> TaskType:
normalized = (plugin_id or "").strip()
if not normalized:
return TaskType.UNKNOWN
return PLUGIN_TO_TASK_TYPE.get(normalized, TaskType.UNKNOWN)
with belief_scope("resolve_task_type"):
normalized = (plugin_id or "").strip()
if not normalized:
return TaskType.UNKNOWN
return PLUGIN_TO_TASK_TYPE.get(normalized, TaskType.UNKNOWN)
# [/DEF:resolve_task_type:Function]
@@ -85,7 +87,8 @@ def resolve_task_type(plugin_id: Optional[str]) -> TaskType:
# @PARAM: task_type (TaskType) - Canonical task type.
# @RETURN: Dict[str, Any] - Profile metadata used by normalization and UI contracts.
def get_type_profile(task_type: TaskType) -> Dict[str, Any]:
return TASK_TYPE_PROFILES.get(task_type, TASK_TYPE_PROFILES[TaskType.UNKNOWN])
with belief_scope("get_type_profile"):
return TASK_TYPE_PROFILES.get(task_type, TASK_TYPE_PROFILES[TaskType.UNKNOWN])
# [/DEF:get_type_profile:Function]
# [/DEF:backend.src.services.reports.type_profiles:Module]