Workflow reconciliation (see previous commit) showed the 20261022
state variable really stores person identity under driver{i}ID /
driver{i}Phonenumber (x2), while the API get_info group keys
(sfzmhm{i}/sjhm{i}) have zero references in any workflow version -
the legacy group read always returned empty for workflow-produced data.
- registry: +4 keys (driver1/2ID, driver1/2Phonenumber) in
human1/human2 groups, SENSITIVE, documented as synonymous with
sfzmhm{i}/sjhm{i}; 24 -> 28 specs
- driver{i}License deliberately NOT registered: the sandbox reads
state.hphm{i} and only renames the output for confirmation prompts -
it is not a state key, set_info rejects it as unknown (workflow
never consumes it)
- intentional diff documented: acdhuman1/2 group output now includes
the driver keys (legacy FastGPT output did not) - fixes the legacy
'workflow person data unreadable by group' gap; synonym merging
(driver1ID vs sfzmhm1) deferred pending caller confirmation
- tests: registry size 28, driver keys present with sensitive level,
License alias excluded
Verified: 230 unit tests
LLM/output stage codes outside STATUS_CODE_MAP were passed through
silently with an empty nextStage - undetectable drift between the
prompt output code set and the endpoint mapping table.
- endpoints: _stage_label / _warn_unknown_stage_code helpers; both
streaming and non-streaming paths emit a warning with sessionId,
clientMode, raw and normalized codes when the label lookup misses
(transparency behavior unchanged: code passes through, label empty)
- known codes and covered internal normalizations (e.g. 2020->0002)
stay silent; the warning fires on the normalized value
Verified: 229 unit tests (4 new monitoring tests)
FastGPT maintains full history server-side; LangGraph truncated the
injected history at a hardcoded 50 messages, so long conversations
behaved differently across backends.
- Config.AGENT_HISTORY_MESSAGES (env): max messages injected into the
model prompt (recent N, both roles counted); default 50 unchanged;
larger values support long conversations; 0 = no truncation, matching
FastGPT's full-history semantics. Only the injected window is
affected - dialogue stays fully persisted in the checkpoint.
- validate(): rejects negative values; 0 and positive accepted
- conversation_history reads the config (import-time class attribute,
monkeypatched in tests); explicit None-check keeps 0 falsy-safe
- .env.example documents the knob
Verified: 225 unit tests (6 new history-window tests)
FastGPT serializes requests per chatId server-side; the LangGraph
backend held all state in memory with no locking, so two concurrent
requests on one session could interleave (checkpoint write races,
garbled no_reply/turn bookkeeping).
- AccidentAppService holds per-session asyncio locks; chat(), stream()
and delete_session() execute under the session lock (LLM call
included - same-session turns are strictly serialized, different
sessions never contend)
- set_info/get_info stay synchronous and lock-free: repository
load/apply_update are atomic dict operations with no mixed
intermediate state; a racing set vs chat yields one complete value
- delete pops the lock entry after cleanup; a chat that arrived
concurrently executes afterwards against the fresh (deleted)
session - matching FastGPT's delete-then-chat new-conversation
semantics
- tests: same-session concurrent chats serialize (interval pairing
probe), different sessions run parallel, delete waits for in-flight
chat, chat-vs-set interleave leaves no torn values
Verified: 219 unit tests (4 new concurrency tests)
Replaces the unequal SLA (LLM 30s/2 retries vs FastGPT client 60s/3)
that made the LangGraph backend look less stable under gray release.
- core/fastgpt_client: FASTGPT_TIMEOUT_SECONDS / FASTGPT_MAX_RETRIES
module constants (were inline literals in lifespan)
- agent/llm: OpenAILLMService defaults 60.0s / 3 retries
- core/config: named-profile fallback defaults 60/3 (prefixed and
JSON-dict forms)
- contract test test/backends/test_sla_alignment.py pins both sides to
the same constants so the SLA cannot drift apart again
- retry backoff intentionally differs (FastGPT fixed 1s vs LLM
exponential) and is documented; aligned parameters are per-call
timeout and max retry count
Verified: 215 unit tests (3 new SLA contract tests)
Closes the error-code gap between LangGraph and FastGPT backends: the
endpoint's 401/429 branches previously could never trigger under the
LangGraph backend because all provider failures collapsed into LLMAPIError
(500).
- agent/llm: LLMAuthError / LLMRateLimitError as LLMAPIError subclasses;
OpenAILLMService raises them from openai AuthenticationError (401, no
retry) and RateLimitError (429, after bounded retries exhausted)
- backends/langgraph: _complete_turn translates LLMAuthError ->
ChatBackendAuthenticationError and LLMRateLimitError ->
ChatBackendRateLimitError; stream reuses complete() so the mapping
applies to both paths; other errors (timeout etc.) propagate unchanged
- endpoint untouched: existing 401/429 response branches now fire for
both backends
Verified: 212 unit tests (7 new error-mapping tests)
- fields_registry: FORM_TO_BUSINESS_KEY bidirectional mapping between
the graph's six form keys (has_injury...) and the acd business keys
(ywrysw...); bjrjs/ywmtc have no form source (set_info only);
business_patch_from_form maps graph diffs to whitelisted business keys
and filters internal/unknown keys
- app_service: chat injection now maps repository business keys back to
form keys; both complete() and stream() write back field diffs
(FormUpdate events captured in stream, closing the skeleton gap);
form keys never leak into the business repository
- docs/assignment6-compat.md: mapping table documented, stream write-back
backlog item resolved
- tests: mapping coverage, write-back mapping end-to-end, reverse
mapping injection (bjrjs/ywmtc excluded), stream FormUpdate write-back
Verified: 205 unit tests
- fields_registry: 24 business keys in acd/human1/human2 groups with
value_type, writable and SensitiveLevel metadata; strict bool/int
parsing (no bool('0') trap); INTERNAL_FIELD_KEYS protection list
- repository: SessionRepository protocol + InMemorySessionRepository
(idempotent delete; PostgreSQL swap-in is assignment 7)
- AccidentAppService: chat/set_info/get_info/delete_session share one
business state - chat injects latest repository fields into the graph
and writes back validated field diffs; set/get/delete make zero LLM
calls and zero helper conversations (includeInputInfo accepted but
unused); unknown get keys keep the '' JSON-empty-string contract,
unknown set keys are rejected by whitelist
- ChatInput.injected_fields neutral contract extension; LangGraphBackend
merges injected fields into graph input (set_info visible next chat)
- endpoints: thin layer - langgraph mode routes business state through
the app service (fastgpt mode keeps the legacy path); delete cleans
repository + graph checkpoint, idempotent
- tests: registry completeness (sfzmwh/sjwh/csbw included), app service
combos with LLM-call spy, HTTP acceptance flow (set -> get -> chat ->
groups -> delete -> no leak, idempotent re-delete)
- docs/assignment6-compat.md: behavior diff list vs FastGPT status quo
and the audit backlog (arbitrary set_info key usage, stream formUpdate
write-back, sensitive-field display boundary)
Verified: 200 unit tests (172 existing + 28 new)
- merge main (b0ff2e0..c7ca781): workflow/20261022 moves dynamic context
out of fixed prompts - collection/extraction prompts carry no inline vars;
reference time and current form travel in the user message <turn_context>
block; photo/confirm data kept at prompt tails
- prompts: all 8 files regenerated from 20261022 (byte-identity tests
point at the new export)
- collect: user message = <turn_context>(本轮参考时间) + <user_input>;
extraction message adds 当前表单; single trusted clock per turn threads
through reply/extraction/repair
- photo: raw input only (20261022 photo nodes use system_text, no wrapper)
- workflow fake llm: unwraps <user_input> before event parsing
- fields: accident_time normalized to extraction format (YYYY年M月D日H点m分,
strip zero padding) - fixes form assertion mismatch with padded output
- evals/run.py: trust_env=False - bypass Windows system proxy (registry
proxy at 192.168.0.4:7897 was intercepting localhost and returning 503)
- verified: 172 unit tests; ALL golden suites pass on real model
(46/46): collection_skip 12/12, photo_skip 4/4, no_false 2/2,
button_guidance 12/12, non_motor_vehicle 14/14, time_confirmation 2/2
- time_context.py: server-side observation clocks for relative-time
evidence (anti client-forgery); resolves X分钟前/小时前 against the
observation timestamp of the turn that produced them, never re-basing
history on the current clock; flags historical references without a
reliable baseline
- state: dialogue_timestamps keyed by dialogue message index, persisted
with checkpoint; welcome seeds index 0
- collect: single clock per turn shared by reply/extraction/repair;
time_evidence injected into runtime context; reply_validator hook
rejects time claims lacking evidence (one shared repair budget);
model still interprets facts and chooses actions
- prompts: collection prompt restructured (shorter, state-bound-to-action
table, gatekeeper principle, time source rules); extraction prompt
aligned; workflow@20261021 json updated to match
- evals: prompt_probes / prompt_compare / time_boundary probe scripts
(supplementary checks, original cases untouched)
- docs: collection-prompt-optimization.md (method + baselines),
time-boundary-fix.md (problem, design, verification)
- gitignore: editor artifact dirs (.agents/.claude/.vscode, skills-lock)
Verified: 171 unit tests pass (16 new time-context tests)
- prompts/collection.md: versioned collection system prompt extracted
from workflow node yQaBxfrWjbRr with source/version/porting notes
- llm.py: LLMService protocol; FakeLLMService (scripted, records
calls); OpenAILLMService (async, wait_for timeout with cancellation,
retry only on timeout/429/connection, errors normalized)
- collect.py: run_collect reference impl - one LLM call produces
state prefix + body, controlled repair (max 1 retry) for missing
prefix or illegal stage proposal, whitelist guard, CollectReplyError
without raw model content; dialogue history + confirmed fields
injected into prompt so the model skips known info (作业4-5)
- fields.py / clock.py: three-state field candidates (unknown/
candidate/confirmed), per-type validation (count not inferred from
description, future time rejected), injectable Clock
- graph.py: welcome node - first message returns fixed 1001 greeting
without LLM (mirrors workflow is_first_message -> Assigned Reply#3),
route empty stage to welcome
- endpoints/delete_session: backend switch - langgraph clears graph
checkpoint, fastgpt path unchanged
- config: AGENT_LLM_PROVIDER fake|openai with validation
- untrack .env (secrets), move template to root .env.example
Verified:
- pytest 103 passed; smoke turns 1/2/1
- Real LLM e2e: POST /chat multi-turn context works
- Golden eval collection_skip_known_info: 12/12 cases pass
- Golden eval no_false: 8/10 cases pass; 4 confirm/photo cases fail
at final step - model routes on short reply '对' (prompt-adherence
gap, photo/confirm stages are assignment 5 scope); recorded in
evals/results/20261020-144856
Known limits: field extraction from model reply not wired (stub);
photo/confirm nodes still scaffold; clarify counting is approximate.
Architecture (mirrors workflow/20261016 chatNode structure):
- events.py: client markers parsed to typed ClientEvent (sources
verified against workflow JSON); plain text like '拍好了' is not an event
- transitions.py: LEGAL_TRANSITIONS whitelist drafted from chatNode
prompts and Condition edges (16 entries, each with source_ref);
is_legal_transition rejects unlisted/terminal-violating moves
- graph.py: multi-node graph (collect/single_photo/dual_photo/...
matching workflow chatNodes), route_by_stage conditional edges,
_fake_llm_propose simulates LLM stage-code proposals (assignment 4
replaces with real LLM); code only keeps context bookkeeping
(no_reply_count) and validation
- state.py: expanded domain fields (pending_question, accident/
person fields, clarify/no-reply counts, turn_patch, state_version)
Key finding: 单车拍照#6/双车拍照#7 are browser_addon branches ending
at 0000, so 2005/2015 allow both 300x (direct) and 0000.
Verified: pytest 87 passed (24 transition/event tests newly green).
Known limit: stage proposals come from a fake LLM; per-stage scripts
and real LLM integration are assignments 4-5.
- src/backends/langgraph.py: LangGraphBackend implements ChatBackend;
complete() runs one graph turn per session (thread_id=sessionId) and
returns state-prefixed content; stream() emits the full state prefix
before body fragments (full-answer slicing, not yet real incremental
output - assignment 8)
- src/core/config.py: AGENT_BACKEND=fastgpt|langgraph, langgraph mode
no longer requires FastGPT credentials at startup
- src/core/fastgpt_client.py: lifespan builds the backend by switch
- test/backends/test_langgraph_backend.py: adapter acceptance tests
Verified:
- pytest: 48 passed
- AGENT_BACKEND=langgraph uvicorn: POST /chat -> 1002;
browser_addon -> browser_addon.1002; SSE = stage_code, text_delta,
single done
Known limit: stream slices a complete answer; no formUpdate deltas yet
(assignment 6).
- Introduced a normalization function for client mode to default to "direct" when input is empty or None.
- Updated the clientMode field to use a new type with BeforeValidator for improved validation.
- Added a test to ensure empty client mode inputs correctly default to "direct".
- Replaced direct FastGPT client usage with a backend-neutral chat interface, allowing for improved flexibility and maintainability.
- Introduced a new `ChatBackend` protocol and implemented `FastGPTBackend` to handle chat operations.
- Updated the chat endpoint to utilize the new backend structure, enhancing the handling of chat requests and responses.
- Added comprehensive tests to ensure compatibility and functionality of the new backend integration.
- Replaced hardcoded URLs with a base URL variable for better configurability.
- Updated request payloads to use sessionId and timeStamp variables.
- Enhanced the chat request with new parameters: needFormUpdate and useTextChunk.
- Adjusted response content types and lengths for consistency.
- Updated timestamps in response headers for accuracy.
- Moved SentenceTextChunker and SentenceTextChunkerConfig to the utils module for better organization.
- Updated pytest.ini to include the current directory in the Python path.
- Added a new utils module with shared utility helpers.
- Adjusted import paths in the test files to reflect the new location of text chunking classes.
- Introduced SentenceTextChunker and SentenceTextChunkerConfig for improved text processing in chat responses.
- Updated chat endpoint to conditionally use text chunking based on the new 'useTextChunk' parameter from the request.
- Enhanced logging to include 'useTextChunk' status and adjusted text delta handling to support chunked responses.
- Modified ProcessRequest_chat model to include 'useTextChunk' field for request handling.
- Added unit tests for SentenceTextChunker to ensure correct chunking behavior and edge case handling.
- Changed ANALYSIS_SERVICE_URL to localhost for local development.
- Updated ANALYSIS_AUTH_TOKEN and APP_ID for improved security.
- Added new functions in endpoints.py for form extraction and stage code normalization.
- Enhanced chat handling to support form updates and improved event streaming.
- Updated models to include new fields for form update handling.