qfa.services.coding#

Coding service — assign hierarchical codes to a feedback record.

Backs POST /v1/assign-codes. Picking is one shot: the full coding framework is flattened into one option per node (at every depth, not just leaves), and a single LLM call selects the best-fitting path(s) directly — no recursive per-level picking. Judging is per level: each selected path is then scored level by level by a separate judge call per level, stopping at the first level that falls below confidence_threshold. Candidates below the threshold are dropped, and the ones that survive are ranked and truncated to max_codes.

This is the only use case whose pick step selects from the whole flattened framework rather than a fixed sequence of calls, which is why it lives in its own module (ADR-017): its private helpers are used by nothing else.

Per ADR-017 CodingService has no base class. The scaffolding it shares with the other use cases — the token-budget guard and the deadline→timeout derivation — comes from the injected LLMCallExecutor, and everything else it needs (the LLM connection, the anonymiser) is named explicitly in its constructor.

Both the pick and the per-level judge run on the primary LLM connection. The judge/primary split introduced by #258 deliberately excludes this path, so the service takes no judge client at all — see tests/services/test_orchestrator_judge_routing.py, which pins the exclusion as a decision rather than an oversight.

Module attributes

NO_CODING_LEAD

Literal first line of every explanation returned when no code is applied.

NO_CODING_EMPTY_CONTENT_EXPLANATION

Explanation for a record whose content is empty (issue #138).

NO_CODING_NOTHING_RELEVANT_EXPLANATION

Explanation for when the LLM selected nothing at all, or nothing it selected survived judging.

Classes

CodingService(llm, anonymizer, executor)

Assign hierarchical codes to a feedback record via pick/judge calls.

qfa.services.coding.NO_CODING_LEAD = 'NO CODING APPLIED.'#

Literal first line of every explanation returned when no code is applied.

EspoCRM surfaces assigned_codes.0.explanation verbatim as autoCodingExplanation, so this line is what a user reads first when a record comes back uncoded (#256).

qfa.services.coding.NO_CODING_EMPTY_CONTENT_EXPLANATION = 'NO CODING APPLIED.\nThe feedback text was empty, so there was nothing to code.'#

Explanation for a record whose content is empty (issue #138).

qfa.services.coding.NO_CODING_NOTHING_RELEVANT_EXPLANATION = 'NO CODING APPLIED.\nNo code in the framework was judged relevant to this feedback.'#

Explanation for when the LLM selected nothing at all, or nothing it selected survived judging.

class qfa.services.coding.CodingService(llm: LLMPort, anonymizer: AnonymizationPort, executor: LLMCallExecutor)[source]#

Bases: object

Assign hierarchical codes to a feedback record via pick/judge calls.

Parameters:
  • llm (LLMPort) – The LLM provider adapter used for every call this service makes — both the one-shot pick and the per-level judge. There is no second connection: #258 scoped the judge/primary split to the quality-score judges on analyse and summarise.

  • anonymizer (AnonymizationPort) – The anonymisation adapter used to redact PII from each assembled prompt before it leaves the process.

  • executor (LLMCallExecutor) – The shared LLM-call scaffolding (ADR-017), used here for the pre-flight token-budget guard and deadline-derived timeout. Injected rather than self-constructed so the composition root stays the one place the object graph is assembled.

async assign_codes(request: CodingAssignmentRequestModel, deadline: datetime) CodingAssignmentResultModel[source]#

Assign hierarchical codes to a feedback record.

Picking is one shot: the full coding framework is flattened into one option per node (at every depth, not just leaves), and a single LLM call selects the best-fitting path(s) directly — no recursive per-level picking. Judging is unchanged from the per-level design: each selected path is then scored level by level by a separate judge call per level, stopping at the first level that falls below confidence_threshold, exactly as when picking was also per-level.

Parameters:
  • request (CodingAssignmentRequest) – Feedback records, coding framework, max_codes, and tenant id.

  • deadline (datetime) – Absolute UTC deadline by which all records must be coded.

Returns:

Per-record codes from the judge, ordered by confidence, highest first. assigned_codes is never empty: when no code is applied it holds exactly one entry with null coding_level_*/ confidence_* fields and an explanation leading with NO CODING APPLIED. (#256). That explanation lists the near misses when confidence_threshold filtered every candidate out, and states that nothing was relevant when nothing was selected at all.

Return type:

CodingAssignmentResult

Raises:
  • AnalysisTimeoutError – When deadline is reached before every record is processed.

  • AnalysisError – When the judge returns a score outside 0.0-1.0.

  • LLMTimeoutError – When a single LLM completion exceeds the configured timeout.

  • LLMRateLimitError – When the LLM provider returns rate limiting.

  • LLMError – For other LLM provider failures. A pick response that fails schema validation (LLMResponseParseError) is treated as an empty pick instead of being raised.