10 KiB
phase, plan, subsystem, tags, dependency_graph, tech_stack, key_files, decisions, metrics
| phase | plan | subsystem | tags | dependency_graph | tech_stack | key_files | decisions | metrics | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 07-redo-and-optimize-llm-integration | 03 | backend/ai-providers |
|
|
|
|
|
|
Phase 7 Plan 03: Anthropic output_config + Classifier ProviderConfig Refactor Summary
AnthropicProvider refactored with singleton _client, output_config constrained-decoding structured output, and _truncate; classifier.py wired to load_provider_config(session) replacing inline dict construction; ai_config.py stub removed; 3 xfailed tests promoted.
Tasks Completed
| Task | Description | Commit | Files |
|---|---|---|---|
| 1 | AnthropicProvider singleton + output_config + truncation + no MAX_AI_CHARS | efc177a |
anthropic_provider.py, ai/__init__.py, tests/test_ai_providers.py |
| 2 | Classifier wired to load_provider_config + ai_config stub removed | 95c386f |
services/ai_config.py, services/classifier.py, tests/test_ai_config.py, tests/test_classifier.py |
What Was Built
Task 1: AnthropicProvider Refactor
backend/ai/anthropic_provider.py rewritten:
MAX_AI_CHARS = 8_000module constant deleteddef _client(self)property method deleted__init__signature widened to(api_key, model, context_chars, base_url=None)— uniform factory contractself._client = anthropic.AsyncAnthropic(api_key=self._api_key)stored as singleton in__init__(D-07)def _truncate(self, text)added — identical 60/40 split pattern to OpenAIProvider (D-13)_CLASSIFICATION_SCHEMAand_SUGGESTIONS_SCHEMAmodule-level dicts added (required + additionalProperties=False)classify()passesoutput_config={"format": {"type": "json_schema", "schema": _CLASSIFICATION_SCHEMA}}(D-03)suggest_topics()passesoutput_config={"format": {"type": "json_schema", "schema": _SUGGESTIONS_SCHEMA}}(D-03)- Graceful degradation:
stop_reason != "end_turn"→raw = ""→parse_classification("")returns empty ClassificationResult (T-07-08) health_check()does NOT passoutput_config— only verifies connectivity/auth
backend/ai/__init__.py updated:
- Anthropic branch now passes
context_chars=effective_context_chars, base_url=effective_base_urlto AnthropicProvider - Comment updated to reflect widened ctor (Plan 03)
backend/tests/test_ai_providers.py updated:
test_anthropic_structured_outputpromoted from xfail — patchesai.anthropic_provider.anthropic.AsyncAnthropic, assertsoutput_configkwarg present and matches_CLASSIFICATION_SCHEMA, asserts AsyncAnthropic constructed once (singleton)test_anthropic_stop_reason_fallbackadded — simulatesstop_reason="max_tokens", asserts empty ClassificationResult returned without crash (T-07-08 regression guard)
Task 2: Classifier ProviderConfig Refactor + ai_config Stub Removal
backend/services/ai_config.py rewritten:
_ProviderConfigStubclass deleted entirely- Module-level import:
from ai.provider_config import ProviderConfig, PROVIDER_DEFAULTS(no more lazy import inside function body) load_provider_config(session)return type changed toOptional[ProviderConfig]seed_system_settings_from_envnow usesPROVIDER_DEFAULTS.get(provider_id, {}).get("context_chars", 8000)for accurate default context_chars per provider
backend/services/classifier.py refactored:
- Imports:
from services.ai_config import load_provider_configandfrom ai.provider_config import ProviderConfig, PROVIDER_DEFAULTS classify_document: inline_settings = {...}dict construction replaced with:- Per-user override path:
ProviderConfig(provider_id=ai_provider, ...)from PROVIDER_DEFAULTS (api_key="" — T-07-06 mitigation documented) - System path:
await load_provider_config(session)→ None fallback to env-var defaults provider = get_provider(config)— unchanged call signature
- Per-user override path:
suggest_topics_for_document: same load/override/fallback pattern applied- No
text[:N]slices remain — truncation fully delegated to provider_truncate()
backend/tests/test_ai_config.py promoted:
test_api_key_encrypt_decrypt: round-trip smoke test (encrypt → decrypt → assert equality), domain salt isolation (different provider_id → different ciphertext), cross-domain decrypt raisesInvalidTokentest_load_provider_config: DB integration test skipped withoutINTEGRATION=1(psycopg guard)
backend/tests/test_classifier.py updated:
test_per_user_provider: capturesProviderConfiginstead of dict; assertsconfig.provider_id == "openai"andconfig.model == "gpt-4o"(D-06 contract enforced in tests)test_default_provider_fallback: patchesload_provider_configto return None; captures ProviderConfig; assertsconfig.provider_id == "ollama"(env fallback path)
Verification Results
grep -v '^#' backend/ai/anthropic_provider.py | grep -c 'MAX_AI_CHARS'→ 0grep -c 'output_config=' backend/ai/anthropic_provider.py→ 3 (classify, suggest_topics, and schema comment)grep -c '_ProviderConfigStub' backend/services/ai_config.py→ 0grep -c 'await load_provider_config(' backend/services/classifier.py→ 2 (one per function)- Full test suite: 1 failed (pre-existing test_extract_docx ModuleNotFoundError), 366 passed, 12 xfailed, 6 skipped — no new failures; xfailed count down by 3 from wave 2
Deviations from Plan
Auto-fixed Issues
1. [Rule 1 - Bug] Classifier tests used dict-based interface
- Found during: Task 2 (test_per_user_provider and test_default_provider_fallback asserted dict fields)
- Issue: After the refactor,
get_providerreceives aProviderConfigobject. The existing tests captured the dict argument and assertedsettings.get("active_provider")etc. These would fail withAttributeErroror wrong assertions on a ProviderConfig object. - Fix: Updated
test_per_user_providerto assertconfig.provider_id == "openai"andconfig.model == "gpt-4o". Updatedtest_default_provider_fallbackto patchload_provider_configreturning None and assertconfig.provider_id == "ollama". - Files modified: backend/tests/test_classifier.py
- Commit:
95c386f
2. [Rule 2 - Missing functionality] seed_system_settings_from_env used hardcoded context_chars=8000
- Found during: Task 2 code review
- Issue: The original seed function always inserted
context_chars=8000regardless of provider, which would insert the wrong default for providers like Anthropic (180,000) or Groq (128,000). - Fix: Updated seed to use
PROVIDER_DEFAULTS.get(provider_id, {}).get("context_chars", 8000)so the seeded row reflects the correct default for each provider. - Files modified: backend/services/ai_config.py
- Commit:
95c386f
Known Stubs
None — all plan goals implemented; no placeholders.
Threat Flags
No new threat surface introduced. Changes are internal service-layer refactoring:
- T-07-06 (per-user api_key isolation) mitigated: classifier per-user override path uses
api_key=""— documented in comment - T-07-08 (Anthropic stop_reason degradation) mitigated: classify() falls back to
parse_classification("")whenstop_reason != "end_turn"
Self-Check: PASSED
Files modified:
- backend/ai/anthropic_provider.py — FOUND (singleton _client, output_config, _truncate, no MAX_AI_CHARS)
- backend/ai/__init__.py — FOUND (context_chars + base_url passed to AnthropicProvider)
- backend/services/ai_config.py — FOUND (ProviderConfig import at module level, no _ProviderConfigStub)
- backend/services/classifier.py — FOUND (load_provider_config + ProviderConfig construction)
- backend/tests/test_ai_providers.py — FOUND (test_anthropic_structured_output promoted)
- backend/tests/test_ai_config.py — FOUND (test_api_key_encrypt_decrypt promoted)
- backend/tests/test_classifier.py — FOUND (per_user_provider + default_fallback tests updated)
Commits: