Commit Graph
11 Commits
Author SHA1 Message Date
Eric Wang 24e2cfbe10 fix(backends): make streamed replies and session state consistent
- 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.
2026-10-23 15:54:51 +08:00
Eric Wang 952bffe6f0 feat(streaming): real incremental SSE - model tokens flow through graph to client (assignment 8)
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)
2026-10-23 00:20:03 +08:00
Eric Wang b8008da959 feat(config): configurable conversation history window (B3)
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)
2026-10-22 23:01:40 +08:00
Eric Wang ca73b1e8c5 feat(backends): map LLM auth/rate-limit errors to backend-neutral errors
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)
2026-10-22 21:50:56 +08:00
Eric Wang e72d9c9bc0 feat(agent): align with workflow@20261022 - turn_context messages and prompt tails
- 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
2026-10-22 12:28:24 +08:00
Eric Wang 9e28f69b3c feat(agent): trusted time evidence and collection prompt optimization
- 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)
2026-10-22 09:41:32 +08:00
Eric Wang 74db6be385 feat(agent): form pipeline + collection prompt workflow@20261021
- merge main: workflow/20261021 (collection prompt adds step-5 mandatory
  time readback-confirm before routing; non-motor/0003 branch rework;
  accident_cause unified schema), updated eval suites (photo_skip/
  button_guidance rewritten, no_false trimmed to asr, new
  non_motor_vehicle + time_confirmation with form/reply assertions),
  runner gains form/formMatches/replyMatches expectations
- prompts/collection.md regenerated from 20261021; extraction prompt
  archived (contentExtract description, 6-key whitelist)
- collect: extraction call per turn (extraction.md + current time/form
  injected), merge follows 更新表单 semantics (meaningful values only,
  number/string equality, diff -> turn_patch); accident_fields now uses
  external form keys
- backend: form_update flows to ChatResult and SSE formUpdate event
  (stream emits prefix, FormUpdate, body)
- fields: FORM_FIELDS whitelist with strict type validation
- verified: 155 unit tests; golden evals on real model - photo_skip 4/4,
  no_false 2/2, photo_button_guidance 12/12, non_motor_vehicle 14/14,
  collection_skip 10/12, time_confirmation 1/2; remaining 3 failures are
  model sequence/literal-template adherence, recorded in plan appendix
2026-10-21 17:10:33 +08:00
Eric Wang 6d0a2169c8 refactor(agent): inject deps via context_schema, LLM-driven photo/confirm nodes
- graph.py: LangGraph-native DI - StateGraph(context_schema=GraphDeps),
  nodes take (state, runtime: Runtime[GraphDeps]); build_graph() decoupled
  from LLM; deleted scaffold _event_reply/_fake_llm_propose/stub nodes
- photo.py: model proposes stage+body via per-mode prompts (direct/addon);
  validator enforces clientMode exits; deterministic event forcing removed
- confirm.py: single runtime for single/dual confirm, extraction call
  isolated per person_index
- llm.py: workflow_fake_llm() (offline workflow-faithful responder,
  history-aware) for smoke/tests
- backend: LangGraphBackend(graph, llm) passes context per call
- tests: invoke helpers inject GraphDeps; illegal-proposal test asserts
  error + state untouched via second well-behaved fake

Verified: 143 passed; smoke turns 1/2/1 with original welcome text
2026-10-21 15:11:16 +08:00
Eric Wang 1c6b1a513d feat(agent): LLM-powered collection node with welcome stage and session-scoped delete (assignment 4)
- 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.
2026-10-20 14:53:46 +08:00
Eric Wang 8323d53a70 feat(agent): domain events and transition whitelist validation (assignment 3)
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.
2026-10-19 16:25:04 +08:00
Eric Wang 5020903791 feat(agent): add LangGraph scaffold for assignment 1
- src/agent/state.py: minimal five-field AgentState
- src/agent/graph.py: START -> normalize_input -> reply -> END,
  compiled with InMemorySaver; reply emits <state>1002</state> prefix
- examples/langgraph_smoke.py: offline smoke, turns 1/2/1 verified
- test/agent/test_scaffold.py: turn accumulation, thread isolation,
  fixed-template assertions (3 passed)
- requirements.txt: pin langgraph==1.2.9

Known limit: InMemorySaver state disappears on process exit.
2026-10-19 13:32:43 +08:00