Feature Specification: LLM Article Analysis
Feature Branch: 003-llm-analysis
Created: 2026-05-29
Status: Draft
Input: User description: "LLM analysis capability — AnalyzeArticleUseCase + ResilientLLMService + SlidingWindowStrategy"
User Scenarios & Testing (mandatory)
User Story 1 - Analyze a Scraped Article (Priority: P1)
A system operator runs the scraper pipeline. After an article is scraped and saved, the system automatically analyzes it using a configured LLM provider to extract a summary, pain points, insights, innovations, and topic-relevant tags.
Why this priority: Core value of the pipeline. Without analysis, scraped articles provide no structured insight to end users.
Independent Test: Can be tested by injecting a single Article entity into AnalyzeArticleUseCase.execute() and asserting that an Analysis record with all required fields is persisted to the database.
Acceptance Scenarios:
- Given a scraped Article with content and a configured LLM provider, When AnalyzeArticleUseCase.execute() is called, Then an Analysis record is persisted with non-empty summary, pain_points, insights, innovations, and at least one tag group.
- Given an ArXiv article with more than 15,000 characters of content, When the analysis is triggered, Then the LLM receives at most 15,000 characters of content.
- Given a successfully analyzed article, When the analysis is persisted, Then the model name, input token count, and output token count are recorded alongside the analysis.
- Given an article already analyzed (duplicate analysis_id FK), When analysis is triggered again, Then the system returns a failure result without overwriting the existing analysis.
User Story 2 - Topic-Mode Aware Tagging (Priority: P1)
The system classifies each article's tags according to the topic's tagging mode. For supervised topics, tags must come from a predefined set. For unsupervised topics, the LLM creates its own tag groups. Semi-supervised allows both.
Why this priority: Tag quality and consistency directly affect downstream filtering and user discovery. Incorrect mode handling corrupts the tag taxonomy.
Independent Test: Can be tested by running AnalyzeArticleUseCase for articles associated with topics in each of the three modes (SUPERVISED, SEMI_SUPERVISED, UNSUPERVISED) and asserting the correct tag group behavior.
Acceptance Scenarios:
- Given an article with a SUPERVISED topic and predefined tag groups in the database, When analysis runs, Then all returned tag keys are restricted to the predefined set.
- Given an article with an UNSUPERVISED topic, When analysis runs, Then new tag group names generated by the LLM are upserted to the database with embeddings.
- Given an article with a SEMI_SUPERVISED topic and some existing tag groups, When analysis runs, Then the LLM may reuse existing groups or create new ones; all new groups are upserted.
- Given an article with no topic_id, When analysis runs, Then all active topics are merged into the prompt and the LLM generates tag groups freely (auto-mode).
- Given a SUPERVISED topic with no tag groups in the database, When analysis runs, Then the system falls back to auto-mode (unsupervised prompt) and continues normally.
User Story 3 - Provider Fallback on Rate Limit or Failure (Priority: P2)
When the primary LLM provider is rate-limited or returns an error, the system automatically falls back to the next configured provider in priority order, ensuring analysis continues without operator intervention.
Why this priority: Provider outages or quota exhaustion are frequent in production. Without fallback, analysis stalls and creates backlogs.
Independent Test: Can be tested by mocking the primary provider to raise RateLimitExhausted and asserting that the secondary provider is called and returns a successful result.
Acceptance Scenarios:
- Given a primary provider that is rate-limited, When analysis is requested, Then the system automatically retries with the next provider in priority order without surfacing an error to the caller.
- Given a primary provider that raises a transient error, When the retry limit is reached, Then the system falls back to the next provider.
- Given all configured providers are exhausted or rate-limited, When analysis is requested, Then the system returns a failure result (not an exception) and logs that all providers failed.
- Given a provider that was demoted due to rate-limiting, When subsequent analysis requests are made, Then that provider is tried last until its rate limit window resets.
- Given a provider returns a malformed JSON response, When the response is received, Then the provider returns None, the system logs a warning, and moves to the next provider without retrying.
User Story 4 - Rate-Limit Enforcement Per Provider (Priority: P2)
The system enforces per-provider rate limits (requests per minute, tokens per minute, requests per day) using a sliding window approach. When a provider nears its limit, requests are briefly queued rather than dropped.
Why this priority: Without rate enforcement, the system triggers HTTP 429 errors or provider bans, which are harder to recover from than managed throttling.
Independent Test: Can be tested by configuring a provider with a low RPM limit and issuing bursts of analysis requests, asserting that requests are appropriately paced.
Acceptance Scenarios:
- Given a provider with an RPM limit of N, When N requests are made within 60 seconds, Then the (N+1)th request is held until the oldest request leaves the 60-second window.
- Given a provider with a TPM limit, When the token usage estimate exceeds the limit, Then new requests are delayed until enough tokens clear the window.
- Given a provider with an RPD (requests per day) limit that has been reached, When a new analysis request arrives, Then RateLimitExhausted is raised immediately and the provider is demoted in the fallback chain.
- Given a provider with no rate-limit configuration (rpm, tpm, rpd all null), When analysis is requested, Then requests pass through immediately with no throttling.
User Story 5 - Analysis Failure Isolation (Priority: P3)
Analysis failures are contained and reported without crashing the pipeline. Each article's analysis is independent; one failure does not block others.
Why this priority: Pipeline resilience is important but less critical than the core analysis behavior.
Independent Test: Can be tested by triggering all providers to fail and asserting AnalysisResult.success is False with a meaningful exception_type field.
Acceptance Scenarios:
- Given a provider that fails after all retries, When analysis is executed, Then AnalyzeArticleUseCase returns AnalysisResult(success=False) with exception_type and exception_message set.
- Given the analysis database write fails, When analysis content is returned from the LLM, Then AnalyzeArticleUseCase returns AnalysisResult(success=False) with the database error details; no exception propagates to the caller.
- Given a tag group embedding fails, When analysis is saved, Then the analysis is still persisted successfully and only a warning is logged.
Edge Cases
- What happens when an article has empty or null content? → LLM receives empty string; expected to return minimal or error response from provider; system returns AnalysisResult(success=False).
- What happens when a provider's API key environment variable is missing? → Provider initialization fails at bootstrap time; system raises ValueError and refuses to start.
- What happens when the LLM returns a response missing required JSON fields? → Provider returns None after logging a validation warning; treated as a provider failure and fallback triggers.
- What happens when the same article is analyzed twice concurrently? → The database unique constraint on article_id prevents duplicate analysis rows; the second write fails and returns AnalysisResult(success=False).
- What happens when tag group embedding service is unavailable? → Analysis persists successfully without embeddings; upserted tag groups have no embedding vector.
Requirements (mandatory)
Functional Requirements
- FR-001: The system MUST analyze a scraped article and produce a structured result containing summary, pain points, insights, innovations, and tag groups.
- FR-002: The system MUST select the analysis prompt based on the article's topic tagging mode (SUPERVISED, SEMI_SUPERVISED, UNSUPERVISED, or no topic).
- FR-003: In SUPERVISED mode, the system MUST constrain generated tags to the predefined tag group keys registered for the topic.
- FR-004: In UNSUPERVISED and SEMI_SUPERVISED modes, the system MUST upsert newly generated tag group names to the database, including embedding vectors where an embedding service is available.
- FR-005: The system MUST record which LLM model produced each analysis along with input and output token counts.
- FR-006: The system MUST attempt analysis through multiple LLM providers in configured priority order, falling back automatically when a provider fails or is rate-limited.
- FR-007: When a provider is rate-limited (RPD exhausted), the system MUST deprioritize it to the end of the fallback chain for subsequent requests.
- FR-008: The system MUST enforce per-provider sliding-window rate limits (RPM, TPM) by queuing requests rather than immediately rejecting them.
- FR-009: The system MUST enforce daily request limits (RPD) per provider and raise a specific rate-limit error when the limit is exceeded.
- FR-010: The system MUST return a structured failure result (not raise an exception) when analysis cannot be completed, including the exception type and message.
- FR-011: For ArXiv articles, the system MUST truncate analysis input to a maximum of 15,000 characters.
- FR-012: The system MUST store analysis content and metadata in a persistent store, keyed uniquely to the article.
- FR-013: Tag group embedding failures MUST NOT block analysis persistence; they are logged as warnings and the pipeline continues.
- FR-014: The system MUST support providers with no rate-limit configuration, treating them as unlimited.
Key Entities
- Analysis: The result of applying an LLM to an article. Contains content (summary, pain_points, insights, innovations, tag_groups) and metadata (model_used, input_tokens, output_tokens). One-to-one with Article.
- AnalysisTagGroup: A grouping of tags generated or selected during analysis. Has a snake_case group key and a list of tag strings. May be predefined (SUPERVISED) or generated (UNSUPERVISED/SEMI_SUPERVISED).
- LLMProvider: A configured backend (Claude, Gemini, OpenRouter) with a priority, model identifier, API credentials reference, and optional rate-limit parameters (RPM, TPM, RPD).
- RateLimitWindow: A rolling 60-second window tracking request count and token consumption for a single provider. Resets naturally as events age out.
- AnalysisPrompt: A rendered, topic-specific instruction sent to the LLM alongside article content. Three variants: auto (unsupervised), fixed (supervised), semi.
Success Criteria (mandatory)
Measurable Outcomes
- SC-001: 95% of article analyses complete successfully end-to-end (LLM call + persistence) in a healthy pipeline run.
- SC-002: When the primary provider is unavailable, the system falls back to a secondary provider without operator intervention and completes analysis within twice the normal latency.
- SC-003: No more than 1 HTTP 429 rate-limit response is received per provider per minute when rate-limit configuration is correctly set.
- SC-004: Analysis pipeline continues processing remaining articles even when up to 20% of individual analyses fail.
- SC-005: Every persisted analysis record has a non-null model_used, input_tokens, and output_tokens value.
- SC-006: In SUPERVISED mode, 100% of generated tag keys match the predefined set for the topic; no out-of-vocabulary tags appear in the database.
Assumptions
- The LLM provider configurations (API keys, models, rate limits) are managed in the database and loaded at pipeline startup; runtime changes to provider config require a restart.
- Each article is analyzed exactly once under normal operation; re-analysis of failed articles is handled by a separate retry mechanism (outside this feature scope).
- The embedding service used for tag group vectors is an optional dependency; the analysis pipeline is fully functional without it.
- The pipeline is single-threaded within a single process; rate-limit state is in-process only and is not shared across multiple pipeline instances.
- Content truncation rules (15,000 character cap for ArXiv) are fixed and not configurable at runtime.
- English is the primary analysis language; translated variants are produced by a separate translation pipeline (004-translation spec).
- A topic's tagging mode is fixed at analysis time; mode changes after articles are analyzed do not retroactively re-tag existing analyses.