mirror of
https://github.com/tiennm99/DocsGPT.git
synced 2026-10-18 00:14:38 +00:00
Trace guardrails, workflow steps and research phases
Guardrail evaluations are traced when they call a remote check or fire; a firing guardrail drops every content preview from the stored trace. Each workflow node and each research phase is a step span, and the workflow run id links the trace to its Logs row.
This commit is contained in:
1 parent
f9605e7794
commit
da1bc16eb6
7 files changed
+256
-12
No files matched your search
@@ -4,6 +4,7 @@ import os
|
||||
import time
|
||||
from typing import Dict, Generator, List, Optional
|
||||
|
||||
from docsgpt import tracing
|
||||
from docsgpt.agents.base import BaseAgent
|
||||
from docsgpt.agents.tool_executor import ToolExecutor
|
||||
from docsgpt.agents.tools.graph_search import add_graph_search_tool
|
||||
@@ -51,6 +52,15 @@ SYNTHESIS_PROMPT = _load_prompt("synthesis.txt")
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
|
||||
def _phase_span(phase: str, **attributes):
|
||||
"""Open a trace span for one research phase (clarify, plan, a step, synthesis)."""
|
||||
return tracing.start_span(
|
||||
tracing.KIND_STEP,
|
||||
f"research {phase}",
|
||||
attributes={"docsgpt.research.phase": phase.split(" ")[0], **attributes},
|
||||
)
|
||||
|
||||
class CitationManager:
|
||||
"""Tracks and deduplicates citations across research steps."""
|
||||
|
||||
@@ -169,7 +179,8 @@ class ResearchAgent(BaseAgent):
|
||||
|
||||
# Phase 0: Clarification (skip if user is responding to a prior clarification)
|
||||
if not self._is_follow_up():
|
||||
clarification = self._clarification_phase(query)
|
||||
with _phase_span("clarify"):
|
||||
clarification = self._clarification_phase(query)
|
||||
if clarification:
|
||||
yield {"metadata": {"is_clarification": True}}
|
||||
yield {"answer": clarification}
|
||||
@@ -182,7 +193,11 @@ class ResearchAgent(BaseAgent):
|
||||
|
||||
# Phase 1: Planning (with adaptive depth)
|
||||
yield {"type": "research_progress", "data": {"status": "planning"}}
|
||||
plan, complexity = self._planning_phase(query)
|
||||
with _phase_span("plan") as plan_span:
|
||||
plan, complexity = self._planning_phase(query)
|
||||
plan_span.set(
|
||||
**{"docsgpt.research.steps": len(plan or []), "docsgpt.research.complexity": complexity}
|
||||
)
|
||||
|
||||
if not plan:
|
||||
logger.warning("ResearchAgent: Planning produced no steps, falling back")
|
||||
@@ -222,7 +237,9 @@ class ResearchAgent(BaseAgent):
|
||||
},
|
||||
}
|
||||
|
||||
report = self._research_step(step_query, tools_dict)
|
||||
with _phase_span(f"step {step_num}", **{"docsgpt.research.step": step_num}) as step_span:
|
||||
step_span.preview("query", step_query)
|
||||
report = self._research_step(step_query, tools_dict)
|
||||
intermediate_reports.append({"step": step, "content": report})
|
||||
|
||||
yield {
|
||||
@@ -249,9 +266,18 @@ class ResearchAgent(BaseAgent):
|
||||
"tokens_used": self._tokens_used,
|
||||
},
|
||||
}
|
||||
yield from self._synthesis_phase(
|
||||
query, plan, intermediate_reports, tools_dict, log_context
|
||||
)
|
||||
synthesis_span = _phase_span("synthesis")
|
||||
try:
|
||||
yield from self._synthesis_phase(
|
||||
query, plan, intermediate_reports, tools_dict, log_context
|
||||
)
|
||||
except GeneratorExit:
|
||||
synthesis_span.end(tracing.STATUS_CANCELLED)
|
||||
raise
|
||||
except Exception as exc:
|
||||
synthesis_span.end(error=exc)
|
||||
raise
|
||||
synthesis_span.end()
|
||||
|
||||
# Sources and tool calls
|
||||
self.retrieved_docs = self.citations.get_all_docs()
|
||||
|
||||
@@ -6,6 +6,7 @@ import uuid
|
||||
from datetime import datetime, timezone
|
||||
from typing import Any, Dict, Generator, List, Optional, TYPE_CHECKING
|
||||
|
||||
from docsgpt import tracing
|
||||
from docsgpt.agents.workflows.cel_evaluator import CelEvaluationError, evaluate_cel
|
||||
from docsgpt.agents.workflows.node_agent import WorkflowNodeAgentFactory
|
||||
from docsgpt.agents.workflows.schemas import (
|
||||
@@ -114,6 +115,9 @@ class WorkflowEngine:
|
||||
self, initial_inputs: WorkflowState, query: str
|
||||
) -> Generator[Dict[str, str], None, None]:
|
||||
self._initialize_state(initial_inputs, query)
|
||||
# A workflow run's Logs row finds its trace by this id; a nested
|
||||
# engine (a workflow inside a workflow) keeps the outermost run's.
|
||||
tracing.bind_if_unset(workflow_run_id=self.workflow_run_id)
|
||||
|
||||
# Surface the run id up front so the client can list this run's
|
||||
# artifacts (GET /api/artifacts?workflow_run_id=) once it has been
|
||||
@@ -140,6 +144,16 @@ class WorkflowEngine:
|
||||
break
|
||||
log_entry = self._create_log_entry(node)
|
||||
self._last_node_tool_calls = []
|
||||
step_span = tracing.start_span(
|
||||
tracing.KIND_STEP,
|
||||
f"workflow_step {node.title or node.type.value}",
|
||||
attributes={
|
||||
"docsgpt.workflow.node_id": node.id,
|
||||
"docsgpt.workflow.node_type": node.type.value,
|
||||
"docsgpt.workflow.node_title": node.title or None,
|
||||
"docsgpt.workflow_run_id": self.workflow_run_id,
|
||||
},
|
||||
)
|
||||
|
||||
yield {
|
||||
"type": "workflow_step",
|
||||
@@ -150,7 +164,12 @@ class WorkflowEngine:
|
||||
}
|
||||
|
||||
try:
|
||||
yield from self._execute_node(node)
|
||||
try:
|
||||
yield from self._execute_node(node)
|
||||
except GeneratorExit:
|
||||
step_span.end(tracing.STATUS_CANCELLED)
|
||||
raise
|
||||
step_span.end()
|
||||
log_entry["status"] = ExecutionStatus.COMPLETED.value
|
||||
self._finalize_log_entry(log_entry, pre_state)
|
||||
|
||||
@@ -167,6 +186,7 @@ class WorkflowEngine:
|
||||
"output": node_output,
|
||||
}
|
||||
except Exception as e:
|
||||
step_span.end(error=e)
|
||||
logger.error(f"Error executing node {node.id}: {e}", exc_info=True)
|
||||
log_entry["status"] = ExecutionStatus.FAILED.value
|
||||
log_entry["error"] = str(e)
|
||||
|
||||
Reference in new issue
Block a user