Skip to content

Data Model: LLM Article Analysis

Feature: 003-llm-analysis | Date: 2026-05-29


Domain Entities

Analysis

The result of applying a language model to a single article. One-to-one with Article.

FieldTypeDescription
idUUIDPrimary key (generated on creation)
article_idUUIDFK to Article; unique constraint (one analysis per article)
analysis_contentAnalysisContentStructured text fields extracted by the LLM
analysis_metadataAnalysisMetadataModel name and token counts
analyzed_atdatetimeTimestamp set on persistence

Invariants:

  • article_id must be unique across all Analysis records.
  • All AnalysisContent fields may be null if the LLM response is partial, but at least one field must be non-null for the analysis to be considered successful.
  • analysis_metadata.model_used must be non-empty.

AnalysisContent (Value Object)

The textual output of the LLM analysis.

FieldTypeDescription
summaryOptional[str]2–3 sentence overview of the article
pain_pointsOptional[str]Problems or challenges identified
insightsOptional[str]Key learnings or observations
innovationsOptional[str]Novel contributions or techniques
tag_groupsOptional[List[AnalysisTagGroup]]Topic classification tags

AnalysisMetadata (Value Object)

Provenance and cost information for each analysis.

FieldTypeDescription
model_usedstrIdentifier of the LLM model that produced the analysis (e.g., gemini-3-flash-preview)
input_tokensintNumber of tokens in the prompt sent to the provider
output_tokensintNumber of tokens in the provider's response

AnalysisTagGroup (Value Object)

A named group of tags assigned to an article by the LLM.

FieldTypeDescription
groupstrsnake_case key identifying the tag category (e.g., machine_learning)
tagsList[str]Individual tag values within the group

State transitions (for UNSUPERVISED / SEMI_SUPERVISED modes only):

  • After analysis: new AnalysisTagGroup keys are upserted to TagGroupDefinition table.
  • Upsert includes computing and storing an embedding vector for cosine-similarity matching.
  • If embedding fails, the tag group is still upserted without a vector.

AnalysisResult (Frozen Dataclass — Application Layer)

The return type of AnalyzeArticleUseCase.execute(). Not persisted.

FieldTypeDescription
successboolTrue if analysis was produced and persisted
analysisOptional[Analysis]Populated on success
article_idUUIDAlways populated (for tracking)
article_urlstrAlways populated (for tracking)
exception_typeOptional[str]Class name of the exception on failure
exception_messageOptional[str]Human-readable error message on failure

Infrastructure Configuration Entities

LLMProvider (ORM Model — llm_providers table)

Runtime configuration for each LLM backend. Loaded at pipeline startup.

ColumnTypeDescription
idUUID PK
namestrProvider family: claude, gemini, openrouter
modelstr (unique)Specific model identifier
api_key_envstrName of the environment variable holding the API key
priorityintFallback order; lower = tried first
typestrllm or embedding
is_activeboolWhether this provider is included in the active chain
rpmint?Max requests per minute (null = unlimited)
tpmint?Max tokens per minute (null = unlimited)
rpdint?Max requests per day (null = unlimited)

Rate-limit strategy selection:

  • All of rpm, tpm, rpd are null → NoOpStrategy (no throttling)
  • Any of rpm, tpm, rpd is non-null → SlidingWindowStrategy

Persistence Schema (PostgreSQL)

analyses table

id            UUID        PK
article_id    UUID        FK(articles.id)  UNIQUE
correlation_id UUID       NOT NULL  (legacy column, generated at save time)
analyzed_at   TIMESTAMP   DEFAULT now()
model_used    VARCHAR
input_tokens  INTEGER
output_tokens INTEGER

INDEX: (article_id)
INDEX: (analyzed_at)

analyses_translation table

id          UUID        PK
analysis_id UUID        FK(analyses.id)
language    VARCHAR     (e.g., 'en', 'zh-TW')
summary     TEXT
pain_points TEXT
insights    TEXT
innovations TEXT

UNIQUE: (analysis_id, language)

Note: English content is written here (language='en') as the initial analysis output. Additional languages are added by the translation pipeline (004-translation).


Rate-Limit State (In-Memory)

These are not persisted; they exist only for the lifetime of a single pipeline run.

SlidingWindowStrategy (per provider instance)

StateTypeDescription
_request_windowdeque[(timestamp, count)]Rolling 60-second request events
_token_windowdeque[(timestamp, count)]Rolling 60-second token events
_daily_request_countintCumulative requests since process start
_rpmint?Max requests per 60s window
_tpmint?Max tokens per 60s window
_rpdint?Max requests per day (raises RateLimitExhausted when exceeded)

Window behavior: On each acquire(), entries older than 60 seconds are evicted from both deques before checking limits. If the current window sum exceeds the limit, the strategy sleeps until the oldest entry ages out.