-
…p+CAS, tenant enforcement Audit findings 1-5 confirmed real; root causes were (a) identity self-reported by the client, (b) every failure path defaulting open. - ERP /ai/whoami (saas-8s+ @c8e0057 follow-up) resolves token → real user/tenant/type; AuthzService.resolveIdentity is now the single identity entry point. Client-supplied userid/brandsid/usertype removed from all request bodies and from chat.html. - dev-login is a real switch (erp.dev-login.enabled, default false) and its ERP creds no longer have built-in admin/666666 defaults; blank token in production → 401, never a silent fall back to the dev (sysadmin) account. - conversations/op/form endpoints require login; conversation ids are namespaced by user id and ownership-checked (403 otherwise); /op/pending no longer returns sPayload. - op confirm/cancel check the proposer, and confirm claims the draft via CAS so concurrent/repeat confirms cannot execute twice; bill numbers are regenerated at execution time instead of replaying the propose-time snapshot. - FK options take the tenant from the introspected identity and return empty rather than dropping the sBrandsId predicate. - secrets moved to env vars (DB_URL/DB_USERNAME/DB_PASSWORD/REDIS_*/LLM_*/ERP_BASEURL); allowMultiQueries=false. NOTE: the previously committed credentials must be rotated. - ids interpolated into ERP URLs are validated (safeId) to stop query/path injection. - update path rejects system columns, resolves FK names, and coerces by column type; numeric coercion now rejects unparseable input instead of writing 0/truncating, and proposal summaries show the value that will actually be written. - ResponseStatusException keeps its 401/403 status instead of being wrapped as 200. - anti-fabrication guard stays on when the intent gate itself fails.
-
- drop jQuery + page-in-page card; collapsible sidebar, single scroll area, light minimal style, markdown tables (markdown-it) - collectForm: grid layout, required flags, typed controls, enum dropdown, FK secondary picker modal (multi-column list / search / paging / select) - form submit posts structured fields to /api/agent/form/submit (no NL re-encoding); legacy FORM_SUBMIT_MARK route removed - askUser card: option chips + always-available free-text input - history replays from ledger endpoint; localStorage history dropped
-
- delete queryData (QueryTool, sqlChatModel bean, llm.sql-model, jsqlparser dep) - delete kgSearch (findForms stays), delete loadSkill (SkillTool/SkillService) - single system prompt (domain map + 业务常识) replaces 3 ToolScope versions; ToolScope removed - intent gate reduced to 查询/新增/操作已有单据/其他 with class definitions only; write action derived from utterance in router - dedup: shared locateRecord() in ProposeWriteTool; name-field/label lookups consolidated into FormResolverService
-
- Replace langchain4j-ollama with langchain4j-open-ai; AgentFactory/ QueryTool depend on ChatModel/StreamingChatModel interfaces - OllamaJsonClient -> LlmJsonClient: raw /v1/chat/completions with response_format json_schema; single OkHttpClient instance - Thinking off at all 3 call sites via reasoning_effort=none (the only switch that works on /v1; think:false and /no_think are ignored) - Drop client-side length params (num_ctx/num_predict/max_tokens) — server-side OLLAMA_CONTEXT_LENGTH=16384 on xlyllm covers them - Unified tracing: TracingChatModelListener.record() shared by listener callbacks and LlmJsonClient; per-request model name from ctx; sql model now has the listener too (was untraced and mislabeled) - Config keys langchain4j.ollama.* -> llm.{base-url,api-key,chat-model, sql-model}; both models = qwen3.6-27b-iq3:latest (tools+thinking+ vision, verified: streaming tool-calls / json_schema / reasoning off) Verified end-to-end on :8199: intent gate traced (340/28 tokens), agent tool loop, correct answers; warm 1-4s, cold load ~96s (KEEP_ALIVE=30s). -
P4 retirement + code slim-down (~18.5k lines deleted), on top of the intent-gate WIP (docs §17/§18 + agent/tool refinements): - Delete old 8-scene multi-agent stack: XlyErpService, SceneSelector/ Chati/ErpAi/DynamicTableNl2Sql agents, DynamicToolProvider (62 meta tools), Scene/ToolMeta/ParamRule entities+mappers+startup caches - Delete whole milvus/tts/ocr packages, /api/tts + /api/ocr endpoints, tts.html, python stream_server.py; strip dead TTS JS from chat.html (playByIndex/handleNormalResponse had no callers) - Three-pass orphan sweep: 17 dead utils, 16 dead entities, dead constants/exceptions, RedisService+RedisConfig, duplicate JacksonConfig, OperableChatMemoryProvider, old prompt generators; fold PageController into MvcConfig; trim OkHttpUtil 495→39 lines - pom: ~35→13 deps (drop mybatis/JPA/webflux/tika/pdfbox/poi/jieba/ jsoup/gson/fastjson2/springdoc/mapstruct/pagehelper/pinyin4j/ spring-retry/jnr-ffi/hutool/ocr/milvus/embeddings; add starter-jdbc); war 200M+→48M - application.yml: drop milvus/tts/ocr/tesseract/mybatis blocks; GlobalExceptionHandler returns plain {code,message} JSON - docs: mark P4 retirement done in agent-architecture §13/§18 Verified: mvn clean package OK; boot smoke on :8199 (Started 0.9s, /chat 200, health db UP). Old /api/tts & /api/ocr now 404 by design.
-
Add a deterministic §5 intent stage before ReAct: extract {intent, form, entities, missing slots} via constrained JSON, then expose only the 3-5 tools relevant to that intent instead of all 12. New: - agent/Intent, agent/ToolScope: intent + visible-tool-set model - service/IntentService: constrained-JSON intent/entity extraction - service/OllamaJsonClient: JSON-mode Ollama calls - service/SlotFillService: slot extraction/prefill of known values Wire through AgentFactory, SystemPromptService, QueryTool, ProposeWriteTool, ErpClient, AgentChatController, OpController, chat.html.
-
User: form rendered every field as a plain text box; no type (dropdown/date/number), and FK fields didn't pull options from their source tables. - FormCollect schema now carries per-field type: fkselect | select | number | date | text (+ options for select, fkTable for fkselect, hint for placeholder). Types inferred from column data type for generic forms; curated for quote. - New GET /api/agent/form/options?table=&q=&brandsid= -> FK options from the source table (elecustomer/eleproduct/elematerials...), whitelisted to tables that appear as sFkTable in the field dict (rejects arbitrary tables e.g. gdslogininfo), tenant-filtered + LIMIT. - chat.html renderFormCollect renders by type: fkselect = searchable <input list=datalist> that fetches options on focus/typing; select = <select>; number = <input type=number>; date picker; text. - Fix: tolerant label matching (strip parentheticals) so '多数量' maps whether or not the LLM keeps the '(逗号分隔)' hint -> multi-qty rows now reliably created. Verified via agent: quote form shows 客户/产品/物料=fkselect, 单位/印刷/颜色/单双面=select, 尺寸/数量/单价/系数=number; options endpoint returns real names (客户 q=Little -> Little Antelope); non-FK table rejected; full write incl 多数量 (BJD202607086 -> 2000/4000/6000).
-
…en, prod injects live token)
-
- ErpClient.execStaging(token, opId): POST /ai/execStaging/{opId} (user-token身份) - OpController: erp.exec-staging.enabled -> delegate confirm to ERP executor; default false keeps tested direct path - application-saaslocal.yml: erp.exec-staging.enabled=false (documented) -
…at UI (question/form_collect/token) - QueryTool: enforce table allowlist (viw_* + form data-sources + field-dict tables) via jsqlparser TablesNamesFinder -> blocks NL2SQL reading gdslogininfo/sysjurisdiction/ai_op_queue etc; single-table tenant predicate injection (sBrandsId); brand hint in prompt - TracingChatModelListener: config-gated Langfuse ingestion export (generation span) via JDK HttpClient, no new deps; docker-compose.langfuse.yml self-host - chat.html: send identity+token in chat req; render question(options) + form_collect(rich fields); forward Authorization on confirm/cancel - application-saaslocal.yml: langfuse config (off by default)
-
…ormCollect + Skills + KgSearch - AgentIdentity + AgentFactory: build agent per request with token+form-allowlist carried in tool instances (per-call context; robust vs LC4j callback threading) - ErpClient: token-aware read/write/examine; user-token expiry does NOT silently re-login as dev-admin (no privilege escalation) - AuthzService: devIdentity()/userIdentity() resolve granted-form set (sAuthsId) - proposeExamine tool + OpController examine dispatch + forward user Authorization on confirm - InteractionTool.askUser (structured options), FormCollectTool.collectForm (real gdsconfigformslave schema) - SkillService + SkillTool.loadSkill + ai_skill digest in system prompt - KgQueryTool.kgSearch: L2 neighbors/flow + L3 field->table - tools ErpReadTool/ProposeWriteTool/QueryTool/FormCollectTool now per-request (not @Component)
-
- ai_op_queue: staging table for AI write ops (draft|confirmed|executed|failed|cancelled) - ProposeWriteTool.proposeUpdate(entity, record, field, newValue): self-resolves the entity master table (excludes viw_* report views), resolves field via 字段字典, locates the unique record + old value, stages a DRAFT to ai_op_queue — DOES NOT execute; returns a proposal - ErpClient.updateForm: executes via ERP addUpdateDelBusinessData with the frontend-compatible payload {data:[{sTable,name:master,column:[{handleType:update,sId,field:value}]}]} - OpController: deterministic (non-LLM) POST /op/{id}/confirm (executes + writes back status) / cancel / GET pending - AgentChatController.onToolExecuted: on proposeUpdate, attach conversation + push write_proposal SSE - chat.html: confirm/cancel card + result echo - system prompt: proposeUpdate flow; never claim a write is done before confirm Verified end-to-end: '把必胜客简称改成必胜客中国' -> proposal card -> confirm -> ERP update -> DB changed; cancel -> DB unchanged. Nothing executes without explicit user confirm. -
- ErpReadTool: optional keyword -> LIKE filter on the form's resolved name field (viw_kg_field_dict, *Name); keyword doc clarified (only for locating a named record, empty for counts) - ErpClient.readForm: accepts filterField/filterValue -> bFilter - KgQueryTool.findForms: rank by bAiTool(curated) + table + flow-connectivity + name length so the top hit is usually the canonical form - SystemPromptService: hard 'always Simplified Chinese, no other language' rule (kills qwen language drift); prefer top form; read directly instead of over-asking - AgentConfig: qwen3 think(false)/returnThinking(false) -> fast + disciplined tool-calling, no leaked chain-of-thought - application-saaslocal.yml: chat model -> qwen3:14b Verified: no more foreign-language leaks; '多少客户'->92; keyword lookup finds 必胜客; proc-backed stock forms readable; greeting 2s, form lookup ~6s. Known limit: which form/columns get picked for a specific-field-of-specific-record query still varies (deferred KgSearch).
-
- RedisChatMemoryStore (ChatMemoryStore): persist per-conversation messages to Redis (chat:mem:{convId}, 30d TTL) -> survives restart; agent memory now Redis-backed via MessageWindowChatMemory.builder().chatMemoryStore(...) - ConversationService: per-user conversation metadata (chat:convs:{userId} hash), title from first message, list/create/delete/history - ConversationController: GET/POST/DELETE /api/agent/conversations + GET /{id}/messages - AgentChatController: touch conversation on each turn - chat.html: conversation sidebar (list/switch/new/delete), load history on switch, refresh on send Verified: multi-turn memory recalls facts; after full restart, conversation list + history + memory all persist (agent still recalls prior-turn facts). -
- ErpClient: thin HTTP client to ERP xlyEntry; dev-login (/checklogin admin) mints+caches a real session token, auto re-login on session expiry (code=-2); reads getBusinessDataByFormcustomId (read-only params only, never bUpdate) - ErpReadTool.readFormData(formId,moduleId): reads a form's real rows, renders Chinese column labels from viw_kg_field_dict, first 10 rows + total count - wired into the agent (tools = findForms + readFormData); system prompt now describes the findForms->readFormData flow (still read-only, writes still 'under development') - application-saaslocal.yml: erp.baseurl -> local xlyEntry :8697 + dev-login creds Verified: '我们有多少客户' -> agent finds the form, reads 92 real customers, renders a clean Chinese table (name/category/salesperson/tax).
-
- ReActAgent: AiServices streaming tool-loop (replaces 8-scene SceneSelector path) - AgentConfig: single agent = streaming Ollama(qwen2.5:14b) + KgQueryTool + per-conversation memory + L1-domain system prompt - SystemPromptService: injects L1 domain map (viw_kg_domain) into system prompt; degrades gracefully if KG views missing - KgQueryTool.findForms: read-only form-catalog lookup over local viw_ai_useful_forms (global metadata, no tenant) — interim formId resolver - AgentChatController: POST /api/agent/chat SSE stream ({type:token|reset|done|error}); reset drops pre-tool-call narration - chat.html: consume the new SSE endpoint (streamed), per-page conversationId, drop TTS-borrow path - application-saaslocal.yml: add to worktree (was master-only untracked -> profile silently hit remote DB) Verified running (saaslocal, :8099): streaming chat, tool loop returns real forms, write requests correctly report 'under development'.