ltcai 9.9.2 → 9.9.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +44 -39
- package/docs/CHANGELOG.md +109 -0
- package/docs/COMMUNITY_AND_PLUGINS.md +1 -1
- package/docs/DEVELOPMENT.md +1 -1
- package/docs/ONBOARDING.md +1 -1
- package/docs/OPERATIONS.md +1 -1
- package/docs/SURFACE_PARITY.md +47 -0
- package/docs/TRUST_MODEL.md +1 -1
- package/docs/WHY_LATTICE.md +1 -1
- package/docs/kg-schema.md +1 -1
- package/lattice_brain/__init__.py +1 -1
- package/lattice_brain/graph/_kg_common.py +335 -0
- package/lattice_brain/graph/curator.py +141 -0
- package/lattice_brain/graph/discovery_index.py +8 -1
- package/lattice_brain/graph/documents.py +111 -0
- package/lattice_brain/graph/fusion.py +179 -0
- package/lattice_brain/graph/ingest.py +43 -9
- package/lattice_brain/graph/projection.py +382 -37
- package/lattice_brain/graph/retrieval.py +71 -4
- package/lattice_brain/graph/retrieval_policy.py +174 -0
- package/lattice_brain/graph/retrieval_vector.py +122 -2
- package/lattice_brain/ingestion.py +66 -0
- package/lattice_brain/portability.py +41 -12
- package/lattice_brain/runtime/multi_agent.py +1 -1
- package/latticeai/__init__.py +1 -1
- package/latticeai/api/automation_intelligence.py +159 -0
- package/latticeai/api/browser.py +15 -4
- package/latticeai/api/chat.py +33 -2
- package/latticeai/api/chat_agent_http.py +363 -6
- package/latticeai/api/chat_contracts.py +7 -1
- package/latticeai/api/chat_helpers.py +147 -0
- package/latticeai/api/chat_intents.py +217 -10
- package/latticeai/api/chat_stream.py +93 -3
- package/latticeai/api/funnel_metrics.py +31 -0
- package/latticeai/api/knowledge_graph.py +65 -1
- package/latticeai/api/local_files.py +90 -0
- package/latticeai/api/setup.py +134 -1
- package/latticeai/api/tools.py +33 -3
- package/latticeai/app_factory.py +14 -0
- package/latticeai/core/agent.py +355 -18
- package/latticeai/core/agent_eval.py +107 -0
- package/latticeai/core/enterprise.py +5 -0
- package/latticeai/core/file_generation.py +404 -2
- package/latticeai/core/legacy_compatibility.py +1 -1
- package/latticeai/core/marketplace.py +1 -1
- package/latticeai/core/run_store.py +243 -0
- package/latticeai/core/workspace_os.py +1 -1
- package/latticeai/models/router.py +25 -12
- package/latticeai/runtime/persistence_runtime.py +19 -1
- package/latticeai/runtime/router_registration.py +8 -1
- package/latticeai/services/app_context.py +3 -0
- package/latticeai/services/architecture_readiness.py +1 -1
- package/latticeai/services/automation_execution.py +266 -0
- package/latticeai/services/automation_intelligence.py +7 -0
- package/latticeai/services/command_center.py +110 -2
- package/latticeai/services/folder_watch.py +396 -0
- package/latticeai/services/funnel_metrics.py +210 -0
- package/latticeai/services/product_readiness.py +1 -1
- package/latticeai/services/search_service.py +51 -5
- package/latticeai/setup/demo_corpus.py +98 -0
- package/latticeai/tools/filesystem.py +38 -1
- package/package.json +1 -1
- package/scripts/bench_agent_smoke.py +410 -0
- package/scripts/bench_models.py +234 -5
- package/scripts/check_current_release_docs.mjs +1 -1
- package/scripts/funnel_soft_gate.py +192 -0
- package/src-tauri/Cargo.lock +1 -1
- package/src-tauri/Cargo.toml +1 -1
- package/src-tauri/tauri.conf.json +1 -1
- package/static/app/asset-manifest.json +29 -27
- package/static/app/assets/Act-BCmTU0E2.js +1 -0
- package/static/app/assets/{Brain-BnGbytHa.js → Brain-CTnjox7w.js} +2 -2
- package/static/app/assets/BrainHome-BJ3sFNX0.js +2 -0
- package/static/app/assets/BrainSignals-C52lwZVD.js +1 -0
- package/static/app/assets/{Capture-CAKnLcb0.js → Capture-B6vBhFa3.js} +1 -1
- package/static/app/assets/CommandPalette-90u9FWFH.js +1 -0
- package/static/app/assets/Library-pKCK0_tk.js +1 -0
- package/static/app/assets/{LivingBrain-C3VbogVo.js → LivingBrain-Jlf2wFqI.js} +1 -1
- package/static/app/assets/{ProductFlow-BLau7c7N.js → ProductFlow-yg1fKP1P.js} +1 -1
- package/static/app/assets/ReviewCard-DWvD7n9h.js +3 -0
- package/static/app/assets/{System-DRHc_6Su.js → System-BAEuHqNY.js} +1 -1
- package/static/app/assets/{bot-DZkDdh-A.js → bot-nB_buEZD.js} +1 -1
- package/static/app/assets/circle-pause-DLNw6Ucp.js +1 -0
- package/static/app/assets/circle-play-B-IsFL1y.js +1 -0
- package/static/app/assets/{cpu-DyvsDBbj.js → cpu-CEPBHaBl.js} +1 -1
- package/static/app/assets/{folder-open-CffoIr9t.js → folder-open-hmN0N9cX.js} +1 -1
- package/static/app/assets/{hard-drive-C1oojrHb.js → hard-drive-CBV_B_Yd.js} +1 -1
- package/static/app/assets/index-7FAfYm4v.css +2 -0
- package/static/app/assets/{index-BS_3aN1n.js → index-DrmOCySv.js} +3 -3
- package/static/app/assets/{input-B_U7zvbR.js → input-gtVCg-ll.js} +1 -1
- package/static/app/assets/{navigation-xG-gyjbe.js → navigation-Bot0hvuv.js} +1 -1
- package/static/app/assets/{network-BF13HJJF.js → network-jE42eKfT.js} +1 -1
- package/static/app/assets/{primitives-DsuKh3M8.js → primitives-CX2Komon.js} +1 -1
- package/static/app/assets/shield-alert-BftATuAA.js +1 -0
- package/static/app/assets/{textarea-DsDL4ET8.js → textarea-CiMJfOSI.js} +1 -1
- package/static/app/assets/useFocusTrap-B7RPGfFy.js +1 -0
- package/static/app/assets/utils-SJUNVOj5.js +7 -0
- package/static/app/index.html +4 -4
- package/static/sw.js +1 -1
- package/static/app/assets/Act-Cx_gZNbE.js +0 -1
- package/static/app/assets/BrainHome-CRUMJL93.js +0 -2
- package/static/app/assets/BrainSignals-BP8-Qh2g.js +0 -1
- package/static/app/assets/CommandPalette-BoUTXBfm.js +0 -1
- package/static/app/assets/Library-PcfVUco6.js +0 -1
- package/static/app/assets/ReviewCard-CX5RqRqx.js +0 -1
- package/static/app/assets/download-C7yxYHvz.js +0 -1
- package/static/app/assets/index-DU78Kogz.css +0 -2
- package/static/app/assets/sunrise-CIP2ELJ1.js +0 -1
- package/static/app/assets/utils-Dh-Dwtkb.js +0 -7
|
@@ -12,11 +12,21 @@ enable), review-queue gated, local-only, and idempotent per suggestion.
|
|
|
12
12
|
|
|
13
13
|
from __future__ import annotations
|
|
14
14
|
|
|
15
|
+
import asyncio
|
|
16
|
+
import os
|
|
17
|
+
import time
|
|
15
18
|
from typing import Any, Callable, Optional
|
|
16
19
|
|
|
17
20
|
from fastapi import APIRouter, HTTPException, Request
|
|
18
21
|
from pydantic import BaseModel
|
|
19
22
|
|
|
23
|
+
from latticeai.services.automation_execution import (
|
|
24
|
+
build_last_execution,
|
|
25
|
+
dry_run_report,
|
|
26
|
+
enqueue_failed_execution,
|
|
27
|
+
is_automation_workflow,
|
|
28
|
+
summarize_workflow_run,
|
|
29
|
+
)
|
|
20
30
|
from latticeai.services.automation_intelligence import AutomationIntelligenceService
|
|
21
31
|
from latticeai.services.brain_automation import find_installed_recipe_workflow
|
|
22
32
|
|
|
@@ -26,6 +36,21 @@ class SuggestionInstallRequest(BaseModel):
|
|
|
26
36
|
enabled: bool = False
|
|
27
37
|
|
|
28
38
|
|
|
39
|
+
class AutomationRunNowRequest(BaseModel):
|
|
40
|
+
workflow_id: str
|
|
41
|
+
# Dry-run first: the default reports what WOULD happen without side
|
|
42
|
+
# effects; an explicit dry_run=false runs the automation once for real.
|
|
43
|
+
dry_run: bool = True
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _run_now_wait_seconds() -> float:
|
|
47
|
+
raw = os.environ.get("LATTICEAI_AUTOMATION_RUN_NOW_WAIT", "30")
|
|
48
|
+
try:
|
|
49
|
+
return max(0.0, min(float(raw), 300.0))
|
|
50
|
+
except (TypeError, ValueError):
|
|
51
|
+
return 30.0
|
|
52
|
+
|
|
53
|
+
|
|
29
54
|
def create_automation_intelligence_router(
|
|
30
55
|
*,
|
|
31
56
|
service: AutomationIntelligenceService,
|
|
@@ -35,7 +60,10 @@ def create_automation_intelligence_router(
|
|
|
35
60
|
gate_write: Callable[[Request], Optional[str]],
|
|
36
61
|
append_audit_event: Callable[..., None],
|
|
37
62
|
workspace_graph: Callable[[], Any],
|
|
63
|
+
run_executor: Any = None,
|
|
64
|
+
review_queue: Any = None,
|
|
38
65
|
) -> APIRouter:
|
|
66
|
+
from lattice_brain.runtime.statuses import RUN_TERMINAL_STATUSES
|
|
39
67
|
from lattice_brain.workflow import legacy_steps_from_nodes, validate_definition
|
|
40
68
|
|
|
41
69
|
router = APIRouter()
|
|
@@ -123,6 +151,137 @@ def create_automation_intelligence_router(
|
|
|
123
151
|
"already_installed": False,
|
|
124
152
|
}
|
|
125
153
|
|
|
154
|
+
def _stamp_last_execution(
|
|
155
|
+
workflow_id: str, last_execution: dict, scope: Optional[str]
|
|
156
|
+
) -> None:
|
|
157
|
+
"""Persist the execution record on the workflow's metadata (merge)."""
|
|
158
|
+
try:
|
|
159
|
+
store.update_workflow_definition(
|
|
160
|
+
workflow_id,
|
|
161
|
+
metadata={"last_execution": last_execution},
|
|
162
|
+
workspace_id=scope,
|
|
163
|
+
)
|
|
164
|
+
except Exception: # noqa: BLE001 — surfacing must never undo the run
|
|
165
|
+
pass
|
|
166
|
+
|
|
167
|
+
@router.post("/api/automation/run-now")
|
|
168
|
+
async def automation_run_now(req: AutomationRunNowRequest, request: Request):
|
|
169
|
+
"""Run an installed automation once — dry-run by default.
|
|
170
|
+
|
|
171
|
+
* ``dry_run=true``: deterministic report of what WOULD happen; no
|
|
172
|
+
runner executes and nothing but the ``last_execution`` stamp moves.
|
|
173
|
+
* ``dry_run=false``: one real execution through the async run
|
|
174
|
+
executor; the request waits briefly for completion, stamps the
|
|
175
|
+
result, and enqueues failed executions into the review inbox.
|
|
176
|
+
"""
|
|
177
|
+
user = require_user(request)
|
|
178
|
+
scope = gate_write(request)
|
|
179
|
+
try:
|
|
180
|
+
workflow = store.get_workflow(req.workflow_id, workspace_id=scope)
|
|
181
|
+
except FileNotFoundError as exc:
|
|
182
|
+
raise HTTPException(
|
|
183
|
+
status_code=404, detail=f"Automation not found: {req.workflow_id}"
|
|
184
|
+
) from exc
|
|
185
|
+
if not is_automation_workflow(workflow):
|
|
186
|
+
raise HTTPException(
|
|
187
|
+
status_code=404,
|
|
188
|
+
detail=f"Workflow is not an installed automation: {req.workflow_id}",
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
if req.dry_run:
|
|
192
|
+
report = dry_run_report(workflow)
|
|
193
|
+
last_execution = build_last_execution(
|
|
194
|
+
mode="dry_run",
|
|
195
|
+
status=report["status"],
|
|
196
|
+
summary=report["summary"],
|
|
197
|
+
)
|
|
198
|
+
_stamp_last_execution(req.workflow_id, last_execution, scope)
|
|
199
|
+
append_audit_event(
|
|
200
|
+
"automation_run_now",
|
|
201
|
+
user_email=user,
|
|
202
|
+
workflow_id=req.workflow_id,
|
|
203
|
+
dry_run=True,
|
|
204
|
+
status=report["status"],
|
|
205
|
+
)
|
|
206
|
+
return {
|
|
207
|
+
"workflow_id": req.workflow_id,
|
|
208
|
+
"dry_run": True,
|
|
209
|
+
"status": report["status"],
|
|
210
|
+
"report": report,
|
|
211
|
+
"last_execution": last_execution,
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if run_executor is None:
|
|
215
|
+
raise HTTPException(
|
|
216
|
+
status_code=503,
|
|
217
|
+
detail="Automation execution runtime is not available.",
|
|
218
|
+
)
|
|
219
|
+
started = await run_executor.start_workflow(
|
|
220
|
+
workflow,
|
|
221
|
+
workflow_id=req.workflow_id,
|
|
222
|
+
user_email=user or None,
|
|
223
|
+
scope=scope,
|
|
224
|
+
inputs={"trigger": "run_now"},
|
|
225
|
+
)
|
|
226
|
+
run_id = str(((started or {}).get("run") or {}).get("id") or "")
|
|
227
|
+
|
|
228
|
+
# Bounded, non-cancelling wait: poll the durable run row so a slow
|
|
229
|
+
# execution keeps running in the background instead of being aborted.
|
|
230
|
+
run = (started or {}).get("run") or {}
|
|
231
|
+
deadline = time.monotonic() + _run_now_wait_seconds()
|
|
232
|
+
while time.monotonic() < deadline:
|
|
233
|
+
try:
|
|
234
|
+
run = store.get_workflow_run(run_id, workspace_id=scope)
|
|
235
|
+
except FileNotFoundError:
|
|
236
|
+
break
|
|
237
|
+
if str(run.get("status") or "") in RUN_TERMINAL_STATUSES:
|
|
238
|
+
break
|
|
239
|
+
await asyncio.sleep(0.05)
|
|
240
|
+
|
|
241
|
+
status = str(run.get("status") or "running")
|
|
242
|
+
completed = status in RUN_TERMINAL_STATUSES
|
|
243
|
+
summary = (
|
|
244
|
+
summarize_workflow_run(run)
|
|
245
|
+
if completed
|
|
246
|
+
else "started — still running in the background"
|
|
247
|
+
)
|
|
248
|
+
last_execution = build_last_execution(
|
|
249
|
+
mode="live",
|
|
250
|
+
status=status if completed else "running",
|
|
251
|
+
summary=summary,
|
|
252
|
+
run_id=run_id or None,
|
|
253
|
+
)
|
|
254
|
+
_stamp_last_execution(req.workflow_id, last_execution, scope)
|
|
255
|
+
|
|
256
|
+
review_item = None
|
|
257
|
+
if status == "failed":
|
|
258
|
+
review_item = enqueue_failed_execution(
|
|
259
|
+
review_queue,
|
|
260
|
+
workflow=workflow,
|
|
261
|
+
run_id=run_id or None,
|
|
262
|
+
error=summary,
|
|
263
|
+
user_email=user or None,
|
|
264
|
+
workspace_id=scope,
|
|
265
|
+
)
|
|
266
|
+
append_audit_event(
|
|
267
|
+
"automation_run_now",
|
|
268
|
+
user_email=user,
|
|
269
|
+
workflow_id=req.workflow_id,
|
|
270
|
+
dry_run=False,
|
|
271
|
+
run_id=run_id or None,
|
|
272
|
+
status=last_execution["status"],
|
|
273
|
+
)
|
|
274
|
+
payload = {
|
|
275
|
+
"workflow_id": req.workflow_id,
|
|
276
|
+
"dry_run": False,
|
|
277
|
+
"status": last_execution["status"],
|
|
278
|
+
"run_id": run_id or None,
|
|
279
|
+
"last_execution": last_execution,
|
|
280
|
+
}
|
|
281
|
+
if review_item is not None:
|
|
282
|
+
payload["review_item_id"] = review_item.get("id")
|
|
283
|
+
return payload
|
|
284
|
+
|
|
126
285
|
return router
|
|
127
286
|
|
|
128
287
|
|
package/latticeai/api/browser.py
CHANGED
|
@@ -27,7 +27,7 @@ from fastapi import APIRouter, HTTPException, Request
|
|
|
27
27
|
from pydantic import BaseModel
|
|
28
28
|
|
|
29
29
|
from latticeai import __version__
|
|
30
|
-
from lattice_brain.ingestion import IngestionItem
|
|
30
|
+
from lattice_brain.ingestion import IngestionItem, capture_quality_verdict
|
|
31
31
|
|
|
32
32
|
MAX_TAB_BYTES = 4 * 1024 * 1024 # 4 MB per captured tab payload
|
|
33
33
|
MAX_URL_FETCH_BYTES = 4 * 1024 * 1024 # 4 MB cap on a fetched page
|
|
@@ -427,7 +427,10 @@ def create_browser_router(
|
|
|
427
427
|
raise HTTPException(status_code=422, detail=str(exc))
|
|
428
428
|
if not (text or "").strip():
|
|
429
429
|
return {"status": "empty", "source_type": "web_url", "url": url,
|
|
430
|
-
"detail": "No readable text was extracted from the page."
|
|
430
|
+
"detail": "No readable text was extracted from the page.",
|
|
431
|
+
# Structured CTA (backlog #9): the extraction produced
|
|
432
|
+
# nothing, tell the UI what the user can do about it.
|
|
433
|
+
"capture_quality": capture_quality_verdict(None, source_type="web_url")}
|
|
431
434
|
res = pipeline.ingest(
|
|
432
435
|
IngestionItem(
|
|
433
436
|
source_type="web_url", title=title, text=text, source_uri=url,
|
|
@@ -435,7 +438,11 @@ def create_browser_router(
|
|
|
435
438
|
),
|
|
436
439
|
user_email=user,
|
|
437
440
|
)
|
|
438
|
-
|
|
441
|
+
payload = res.as_dict()
|
|
442
|
+
payload["capture_quality"] = capture_quality_verdict(
|
|
443
|
+
res.extraction_quality, source_type="web_url",
|
|
444
|
+
)
|
|
445
|
+
return payload
|
|
439
446
|
|
|
440
447
|
@router.post("/api/browser/ingest-current-tab")
|
|
441
448
|
async def ingest_current_tab(req: IngestTabRequest, request: Request):
|
|
@@ -481,6 +488,10 @@ def create_browser_router(
|
|
|
481
488
|
),
|
|
482
489
|
user_email=user,
|
|
483
490
|
)
|
|
484
|
-
|
|
491
|
+
payload = res.as_dict()
|
|
492
|
+
payload["capture_quality"] = capture_quality_verdict(
|
|
493
|
+
res.extraction_quality, source_type="browser_tab",
|
|
494
|
+
)
|
|
495
|
+
return payload
|
|
485
496
|
|
|
486
497
|
return router
|
package/latticeai/api/chat.py
CHANGED
|
@@ -28,6 +28,7 @@ from latticeai.api.chat_documents import (
|
|
|
28
28
|
)
|
|
29
29
|
from latticeai.api.chat_helpers import (
|
|
30
30
|
_LANG_HINT,
|
|
31
|
+
assess_answer_grounding,
|
|
31
32
|
build_context_quality,
|
|
32
33
|
build_recent_chat_context,
|
|
33
34
|
detect_language,
|
|
@@ -44,6 +45,7 @@ from latticeai.api.chat_helpers import (
|
|
|
44
45
|
workspace_scope_from_request,
|
|
45
46
|
)
|
|
46
47
|
from latticeai.api.chat_history import HistoryRouteDependencies, register_history_routes
|
|
48
|
+
from latticeai.core.run_store import AgentRunStore
|
|
47
49
|
from latticeai.api.chat_intents import ChatIntentController
|
|
48
50
|
from latticeai.api.chat_stream import stream_chat
|
|
49
51
|
from latticeai.services.app_context import AppContext
|
|
@@ -189,6 +191,12 @@ def create_chat_router(context: AppContext) -> APIRouter:
|
|
|
189
191
|
base_dir=context.base_dir or Path.cwd(),
|
|
190
192
|
agent_root=AGENT_ROOT,
|
|
191
193
|
ensure_agent_root=ensure_agent_root,
|
|
194
|
+
funnel_metrics=context.funnel_metrics,
|
|
195
|
+
run_store=(
|
|
196
|
+
AgentRunStore(Path(context.data_dir) / "agent_runs")
|
|
197
|
+
if context.data_dir
|
|
198
|
+
else None
|
|
199
|
+
),
|
|
192
200
|
)
|
|
193
201
|
intent_controller = ChatIntentController(
|
|
194
202
|
model_router=model_router,
|
|
@@ -209,6 +217,7 @@ def create_chat_router(context: AppContext) -> APIRouter:
|
|
|
209
217
|
agent_controller=agent_controller,
|
|
210
218
|
agent_root=AGENT_ROOT,
|
|
211
219
|
ingestion_pipeline=context.ingestion_pipeline,
|
|
220
|
+
funnel_metrics=context.funnel_metrics,
|
|
212
221
|
)
|
|
213
222
|
document_coordinator = DocumentGenerationCoordinator(
|
|
214
223
|
model_router=model_router,
|
|
@@ -273,7 +282,12 @@ def create_chat_router(context: AppContext) -> APIRouter:
|
|
|
273
282
|
)
|
|
274
283
|
|
|
275
284
|
selected_model_id = request_model(req.model)
|
|
276
|
-
|
|
285
|
+
file_intent = is_file_action_request(req.message)
|
|
286
|
+
if file_intent and context.funnel_metrics is not None:
|
|
287
|
+
# UX funnel (backlog #16): one file-intent request counted once,
|
|
288
|
+
# regardless of which delivery path handles it below.
|
|
289
|
+
context.funnel_metrics.increment("file_requests")
|
|
290
|
+
if file_intent:
|
|
277
291
|
direct_response = await intent_controller.direct_file_action(
|
|
278
292
|
req,
|
|
279
293
|
model_id=selected_model_id,
|
|
@@ -284,7 +298,7 @@ def create_chat_router(context: AppContext) -> APIRouter:
|
|
|
284
298
|
return direct_response
|
|
285
299
|
if not selected_model_id:
|
|
286
300
|
return intent_controller.no_model_response()
|
|
287
|
-
if
|
|
301
|
+
if file_intent:
|
|
288
302
|
return await intent_controller.route_file_to_agent(
|
|
289
303
|
req,
|
|
290
304
|
request,
|
|
@@ -375,6 +389,12 @@ def create_chat_router(context: AppContext) -> APIRouter:
|
|
|
375
389
|
)
|
|
376
390
|
if isinstance(trace_seed, dict):
|
|
377
391
|
trace_seed["context_quality"] = context_quality
|
|
392
|
+
if context.funnel_metrics is not None and int(
|
|
393
|
+
context_quality.get("nodes") or 0
|
|
394
|
+
) > 0:
|
|
395
|
+
# UX funnel (backlog #16): a grounded answer is "first value" —
|
|
396
|
+
# the TTFV clock ends on the first successful recall.
|
|
397
|
+
context.funnel_metrics.record_recall_success()
|
|
378
398
|
|
|
379
399
|
history_message = (
|
|
380
400
|
f"{req.message}\n[Image attached]" if req.image_data else req.message
|
|
@@ -464,6 +484,16 @@ def create_chat_router(context: AppContext) -> APIRouter:
|
|
|
464
484
|
req.image_data,
|
|
465
485
|
)
|
|
466
486
|
response_text = str(result)
|
|
487
|
+
# Answer-citation binding (backlog #11): annotate — never block — how
|
|
488
|
+
# the answer relates to the retrieved sources. Recorded on the trace
|
|
489
|
+
# so the Review/proof surfaces see the same verdict as the client.
|
|
490
|
+
grounding = assess_answer_grounding(
|
|
491
|
+
response_text,
|
|
492
|
+
trace=trace_seed if isinstance(trace_seed, dict) else None,
|
|
493
|
+
context_quality=context_quality,
|
|
494
|
+
)
|
|
495
|
+
if isinstance(trace_seed, dict):
|
|
496
|
+
trace_seed["grounding"] = grounding
|
|
467
497
|
trace_record = await chat_service.persist_answer(
|
|
468
498
|
question=req.message,
|
|
469
499
|
response=response_text,
|
|
@@ -482,6 +512,7 @@ def create_chat_router(context: AppContext) -> APIRouter:
|
|
|
482
512
|
"trace_id": trace_record["id"],
|
|
483
513
|
"trace": trace_record,
|
|
484
514
|
"context_quality": context_quality,
|
|
515
|
+
"grounding": grounding,
|
|
485
516
|
}
|
|
486
517
|
)
|
|
487
518
|
|