diff --git a/authbridge/sparc-service/sparc_service/__main__.py b/authbridge/sparc-service/sparc_service/__main__.py index a0f387a4..f545a7f9 100644 --- a/authbridge/sparc-service/sparc_service/__main__.py +++ b/authbridge/sparc-service/sparc_service/__main__.py @@ -2,12 +2,15 @@ from __future__ import annotations +import logging + import uvicorn from .settings import Settings def main() -> None: + logging.basicConfig(level=logging.INFO) settings = Settings.from_env() uvicorn.run("sparc_service.api:app", host=settings.host, port=settings.port, log_level="info") diff --git a/authbridge/sparc-service/sparc_service/api.py b/authbridge/sparc-service/sparc_service/api.py index 67df2c24..fd0487ca 100644 --- a/authbridge/sparc-service/sparc_service/api.py +++ b/authbridge/sparc-service/sparc_service/api.py @@ -8,6 +8,7 @@ from __future__ import annotations +import json import logging from fastapi import FastAPI, HTTPException @@ -19,6 +20,29 @@ log = logging.getLogger(__name__) +# _LOG_REQUESTS and _STRIP_KEYS are now read from Settings (via Settings.from_env) +# so all config comes from a single place. See settings.py. + + +def _strip_tool_arg_keys(tool_calls: list[dict], keys: frozenset[str]) -> list[dict]: + """Return a copy of tool_calls with the named argument keys removed.""" + result = [] + for tc in tool_calls: + fn = tc.get("function") or {} + if not isinstance(fn, dict): + result.append(tc) + continue + raw_args = fn.get("arguments", "") + try: + args = json.loads(raw_args) if isinstance(raw_args, str) else raw_args + if isinstance(args, dict): + args = {k: v for k, v in args.items() if k not in keys} + new_args = json.dumps(args) if isinstance(args, dict) else raw_args + except (json.JSONDecodeError, TypeError): + new_args = raw_args + result.append({**tc, "function": {**fn, "arguments": new_args}}) + return result + def create_app(engine: ReflectionEngine | None = None) -> FastAPI: """Build the FastAPI app. Inject ``engine`` in tests; defaults to env config.""" @@ -51,6 +75,16 @@ def readyz() -> dict[str, object]: @app.post("/reflect", response_model=ReflectResponse) async def reflect(request: ReflectRequest) -> ReflectResponse: + if settings.log_requests: + log.info("incoming reflect request: %s", request.model_dump_json()) + + if settings.strip_tool_arg_keys and request.tool_calls: + request = request.model_copy( + update={"tool_calls": _strip_tool_arg_keys(request.tool_calls, settings.strip_tool_arg_keys)} + ) + if settings.log_requests: + log.info("after strip (%s): tool_calls=%s", sorted(settings.strip_tool_arg_keys), request.tool_calls) + # SPARCReflectionComponent.process is synchronous (and CPU/IO bound on the # LLM call); run it off the event loop so the service stays responsive. try: diff --git a/authbridge/sparc-service/sparc_service/settings.py b/authbridge/sparc-service/sparc_service/settings.py index 74e76e99..a894ca74 100644 --- a/authbridge/sparc-service/sparc_service/settings.py +++ b/authbridge/sparc-service/sparc_service/settings.py @@ -98,6 +98,10 @@ class Settings: host: str = "0.0.0.0" port: int = 8090 + # Request logging / arg-stripping (see api.py). + log_requests: bool = False + strip_tool_arg_keys: frozenset = field(default_factory=frozenset) + # Validation errors collected at load time (provider creds missing, etc.). errors: tuple[str, ...] = field(default_factory=tuple) @@ -152,6 +156,12 @@ def from_env(cls) -> "Settings": f"provider={provider} requires SPARC_MODEL (e.g. azure/ or anthropic/claude-3-5-sonnet)" ) + strip_tool_arg_keys: frozenset[str] = frozenset( + k.strip() + for k in os.getenv("SPARC_STRIP_TOOL_ARG_KEYS", "").split(",") + if k.strip() + ) + return cls( provider=provider, model=model, @@ -171,5 +181,7 @@ def from_env(cls) -> "Settings": llm_registry_id=os.getenv("SPARC_LLM_REGISTRY_ID", "").strip(), host=os.getenv("HOST", "0.0.0.0"), port=_int_env("PORT", 8090), + log_requests=_truthy(os.getenv("SPARC_LOG_REQUESTS", "")), + strip_tool_arg_keys=strip_tool_arg_keys, errors=tuple(errors), )