- Select one storage backend per instance via AGENT_STORAGE, retaining
memory as the backward-compatible default. Manage the selected async
repository, official checkpointer and connection pools in lifespan;
fail startup on unavailable or uninitialized storage without fallback.
- Add SQLite and PostgreSQL adapters, schema initialization CLI, sample
PostgreSQL compose configuration and a Windows-compatible serve entry.
SQLite enforces single-instance file ownership; PostgreSQL uses a
dedicated pool for session advisory locks plus version/epoch fencing.
- Make repository IO and commit callbacks awaitable. Persist business
fields, a complete committed graph snapshot and turn result/version in
one business transaction before success. Rebuild execution checkpoints
from that snapshot, discarding uncommitted progress after interruption.
- Support optional Idempotency-Key on durable chat requests. Replay
committed results and return 409 on changed request content; delete
removes business/history data and replay records while retaining only
an epoch/version tombstone to reject stale commits.
- Document migration, database switching boundaries, backup/deployment
expectations and crash recovery. Ignore generated database files.
Validation: 315 tests passed with real SQLite and isolated PostgreSQL,
including abrupt child-process exits before/after business commit;
both backends passed CLI migration and actual HTTP server restart smoke.
Existing API tests were rechecked after isolating them from user storage.
An initial Windows CreateProcess permission failure did not recur in the
full diagnostic run; failure assertions were not skipped or relaxed.
Delete checkpoints before removing business fields. Suppress only an
explicit CheckpointNotFoundError; propagate storage failures and
cancellation so the endpoint cannot report false success. Prefer async
checkpoint deletion and fail clearly when cleanup is unsupported.
Classify SSE error events as 401 for authentication, 429 for rate limits,
and 500 for other failures, using the same fixed messages as non-stream
responses. Errors terminate the stream without a success done event or
provider exception text in the response.
Validation: 286 offline tests passed (19 added), including failed-delete
HTTP retry with preserved business/checkpoint state, explicit missing
threads, cancellation, missing cleanup capability, and HTTP SSE errors
before and after text output.
- Validate stage prefixes before forwarding model tokens. Invalid prefixes
may use the existing repair budget; once output starts, validation failure
aborts the turn without appending a regenerated reply. Bound the token
queue and close inner streams when consumption stops.
- Replace delete-time lock eviction with reference-counted session locks
covering holders and waiters. Serialize set/get with chat/stream/delete
and release cancelled waiters without replacing a live lock.
- Store person synonyms under workflow-native driver keys and expose
consistent alias views to single/group reads and graph prompt injection.
- Separate committed field patches from public formUpdate visibility.
Commit callbacks keep repository fields synchronized even when updates
are hidden or the consumer closes after graph completion.
- Disable OpenAI SDK automatic retries. Own bounded retry attempts in the
application, use 1/2/4 second backoff, retry streams only before text is
emitted, enforce attempt timeouts, and close upstream/client resources.
- Translate FastGPT SDK max_retries (total attempts) from the configured
extra-retry count, and verify the real SDK loop uses four attempts.
- Document the per-LLM budget and streaming failure semantics without
claiming the whole multi-call HTTP turn has the same timeout.
Validation: 267 offline tests passed, including actual SDK requests through
MockTransport, stream repair/abort, timeout/close, lock races, aliases and
formUpdate visibility; existing four dependency deprecation warnings.
Replaces the 'complete answer sliced into chunks' stopgap with token-
level streaming. Graph nodes are untouched: they still consume the
batched LLMService.complete interface and validate full replies.
- agent/llm: OpenAILLMService.stream_tokens() iterates the provider
stream with the same message construction, temperature and extra_body
forwarding as complete(); identical error classification (401 ->
LLMAuthError, 429 -> LLMRateLimitError, timeout via asyncio.timeout
around the whole iteration); deliberately no retries (tokens already
emitted cannot be recalled)
- backends/langgraph: _TokenPassThroughLLM wraps the injected LLM for
stream turns - dialogue calls push tokens to a queue bridge while
accumulating the full text for node validation; calls marked with the
【字段提取】 marker take the silent inner.complete path so extraction
JSON never leaks into the client stream; LLMs without stream_tokens
fall back to single-chunk push (fake/test compatibility)
- stream(): graph runs in a background task; the generator yields
TextDelta per token and FormUpdate after the turn; graph-task errors
re-raise with the existing ChatBackend error mapping; generator close
(client disconnect) cancels the graph task and the in-flight LLM call
- fixed-answer turns with no LLM call (welcome) fall back to emitting
the full response text, preserving previous behavior
- endpoint untouched: its buffering parser already handles arbitrary
chunk boundaries (state tags split across tokens, stage-first order)
Verified: 241 unit tests (11 new streaming tests: token incrementality,
extraction bypass, FormUpdate ordering, error mapping on both paths,
disconnect cancellation, fallback chunking, SSE protocol end-to-end
with per-token text_delta events)
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.