continuous-intelligence-layer 0.1.0__py3-none-any.whl
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.
- continuous_intelligence_layer/__init__.py +28 -0
- continuous_intelligence_layer/_core/__init__.py +6 -0
- continuous_intelligence_layer/_core/exporter.py +517 -0
- continuous_intelligence_layer/_core/graph_exporter.py +92 -0
- continuous_intelligence_layer/_core/utils.py +170 -0
- continuous_intelligence_layer/anthropic/__init__.py +9 -0
- continuous_intelligence_layer/anthropic/init.py +232 -0
- continuous_intelligence_layer/anthropic/instrumentation.py +91 -0
- continuous_intelligence_layer/crewai/__init__.py +9 -0
- continuous_intelligence_layer/crewai/init.py +228 -0
- continuous_intelligence_layer/crewai/instrumentation.py +83 -0
- continuous_intelligence_layer/langgraph/__init__.py +12 -0
- continuous_intelligence_layer/langgraph/init.py +253 -0
- continuous_intelligence_layer/langgraph/instrumentation.py +71 -0
- continuous_intelligence_layer/openai/__init__.py +9 -0
- continuous_intelligence_layer/openai/init.py +229 -0
- continuous_intelligence_layer/openai/instrumentation.py +67 -0
- continuous_intelligence_layer-0.1.0.dist-info/METADATA +633 -0
- continuous_intelligence_layer-0.1.0.dist-info/RECORD +38 -0
- continuous_intelligence_layer-0.1.0.dist-info/WHEEL +4 -0
- continuous_intelligence_layer-0.1.0.dist-info/licenses/LICENSE +21 -0
- evaluators/__init__.py +32 -0
- evaluators/base_evaluator.py +181 -0
- evaluators/crewai_input_evaluator.py +249 -0
- evaluators/input_evaluator.py +121 -0
- evaluators/models.py +180 -0
- evaluators/output_evaluator.py +247 -0
- evaluators/runner.py +313 -0
- evaluators/tool_agent_evaluator.py +305 -0
- graph_builder/__init__.py +6 -0
- graph_builder/builder.py +212 -0
- graph_builder/models.py +159 -0
- graph_builder/mongo_store.py +588 -0
- llm_router/__init__.py +3 -0
- llm_router/router.py +71 -0
- rca_engine/__init__.py +5 -0
- rca_engine/incident_report.py +162 -0
- rca_engine/rca_engine.py +202 -0
evaluators/models.py
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
"""
|
|
2
|
+
models.py
|
|
3
|
+
---------
|
|
4
|
+
Pydantic models shared by all 3 evaluators and the RCA Engine.
|
|
5
|
+
|
|
6
|
+
EvaluationResult is the canonical output of every evaluator. Every field
|
|
7
|
+
is designed to be stored as-is in MongoDB, written to JSONL, and fed to the
|
|
8
|
+
RCA Engine without any transformation.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import uuid
|
|
14
|
+
from datetime import datetime, timezone
|
|
15
|
+
from enum import Enum
|
|
16
|
+
from typing import Any, Optional
|
|
17
|
+
|
|
18
|
+
from pydantic import BaseModel, Field
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
# ── Enums ──────────────────────────────────────────────────────────────────
|
|
22
|
+
|
|
23
|
+
class EvaluationStatus(str, Enum):
|
|
24
|
+
PASS = "PASS"
|
|
25
|
+
FAIL = "FAIL"
|
|
26
|
+
WARNING = "WARNING" # non-blocking issue worth noting
|
|
27
|
+
SKIP = "SKIP" # evaluator not applicable to this node type
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class Severity(str, Enum):
|
|
31
|
+
"""Severity of a FAIL/WARNING. Only set when status != PASS/SKIP."""
|
|
32
|
+
LOW = "LOW"
|
|
33
|
+
MEDIUM = "MEDIUM"
|
|
34
|
+
HIGH = "HIGH"
|
|
35
|
+
CRITICAL = "CRITICAL"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
# ── Core result model ──────────────────────────────────────────────────────
|
|
39
|
+
|
|
40
|
+
class EvaluationResult(BaseModel):
|
|
41
|
+
"""
|
|
42
|
+
The standard output of every evaluator.
|
|
43
|
+
|
|
44
|
+
Stored in 3 places:
|
|
45
|
+
1. MongoDB — as an `evaluations` document referencing its node_id
|
|
46
|
+
2. JSONL — appended to evaluation_results.jsonl
|
|
47
|
+
3. Python — returned from evaluator.evaluate(node, graph)
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
# ── Identity ─────────────────────────────────────────────────────────
|
|
51
|
+
evaluation_id: str = Field(default_factory=lambda: str(uuid.uuid4())[:16])
|
|
52
|
+
evaluator: str # "InputEvaluator" | "OutputEvaluator" | "ToolAgentEvaluator"
|
|
53
|
+
|
|
54
|
+
# ── Links back to the execution ───────────────────────────────────────
|
|
55
|
+
node_id: str
|
|
56
|
+
node_name: str
|
|
57
|
+
node_type: str
|
|
58
|
+
execution_id: str
|
|
59
|
+
session_id: Optional[str] = None
|
|
60
|
+
|
|
61
|
+
# ── Verdict ───────────────────────────────────────────────────────────
|
|
62
|
+
status: EvaluationStatus
|
|
63
|
+
severity: Optional[Severity] = None # None when status=PASS or SKIP
|
|
64
|
+
confidence: float = 0.0 # 0.0–1.0
|
|
65
|
+
|
|
66
|
+
# ── Human-readable explanation ────────────────────────────────────────
|
|
67
|
+
reason: str
|
|
68
|
+
suggestion: Optional[str] = None # null when status=PASS
|
|
69
|
+
|
|
70
|
+
# ── Evaluator-specific sub-checks ─────────────────────────────────────
|
|
71
|
+
# Each evaluator populates this with its own structured checks dict.
|
|
72
|
+
checks: dict[str, Any] = Field(default_factory=dict)
|
|
73
|
+
|
|
74
|
+
# ── Operational metadata ──────────────────────────────────────────────
|
|
75
|
+
timestamp: str = Field(
|
|
76
|
+
default_factory=lambda: datetime.now(timezone.utc).isoformat()
|
|
77
|
+
)
|
|
78
|
+
metadata: dict[str, Any] = Field(default_factory=dict)
|
|
79
|
+
|
|
80
|
+
class Config:
|
|
81
|
+
use_enum_values = True
|
|
82
|
+
|
|
83
|
+
def to_dict(self) -> dict:
|
|
84
|
+
return self.model_dump()
|
|
85
|
+
|
|
86
|
+
def is_ok(self) -> bool:
|
|
87
|
+
return self.status in (EvaluationStatus.PASS, EvaluationStatus.SKIP)
|
|
88
|
+
|
|
89
|
+
def worst_severity(self) -> str | None:
|
|
90
|
+
"""Return severity value string, or None."""
|
|
91
|
+
return self.severity if self.severity else None
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
# ── Aggregated result for one node ─────────────────────────────────────────
|
|
95
|
+
|
|
96
|
+
class NodeEvaluationSummary(BaseModel):
|
|
97
|
+
"""
|
|
98
|
+
Bundles all 3 evaluator results for one ExecutionNode.
|
|
99
|
+
Written to the node document in MongoDB as flat props + JSON blob.
|
|
100
|
+
"""
|
|
101
|
+
node_id: str
|
|
102
|
+
node_name: str
|
|
103
|
+
execution_id: str
|
|
104
|
+
node_type: str = ""
|
|
105
|
+
|
|
106
|
+
input_result: Optional[EvaluationResult] = None
|
|
107
|
+
output_result: Optional[EvaluationResult] = None
|
|
108
|
+
tool_agent_result: Optional[EvaluationResult] = None
|
|
109
|
+
|
|
110
|
+
@property
|
|
111
|
+
def overall_status(self) -> str:
|
|
112
|
+
results = [r for r in [self.input_result, self.output_result, self.tool_agent_result] if r]
|
|
113
|
+
statuses = {r.status for r in results}
|
|
114
|
+
if EvaluationStatus.FAIL in statuses:
|
|
115
|
+
return "FAIL"
|
|
116
|
+
if EvaluationStatus.WARNING in statuses:
|
|
117
|
+
return "WARNING"
|
|
118
|
+
return "PASS"
|
|
119
|
+
|
|
120
|
+
@property
|
|
121
|
+
def worst_severity(self) -> str | None:
|
|
122
|
+
order = [Severity.CRITICAL, Severity.HIGH, Severity.MEDIUM, Severity.LOW]
|
|
123
|
+
results = [r for r in [self.input_result, self.output_result, self.tool_agent_result] if r]
|
|
124
|
+
for sev in order:
|
|
125
|
+
if any(r.severity == sev for r in results):
|
|
126
|
+
return sev.value
|
|
127
|
+
return None
|
|
128
|
+
|
|
129
|
+
@property
|
|
130
|
+
def has_hallucination(self) -> bool:
|
|
131
|
+
if self.output_result and self.output_result.checks:
|
|
132
|
+
return bool(self.output_result.checks.get("has_hallucination", False))
|
|
133
|
+
return False
|
|
134
|
+
|
|
135
|
+
@property
|
|
136
|
+
def has_injection(self) -> bool:
|
|
137
|
+
if self.input_result and self.input_result.checks:
|
|
138
|
+
return bool(self.input_result.checks.get("is_prompt_injected", False))
|
|
139
|
+
return False
|
|
140
|
+
|
|
141
|
+
@property
|
|
142
|
+
def has_missing_context(self) -> bool:
|
|
143
|
+
if self.input_result and self.input_result.checks:
|
|
144
|
+
missing = self.input_result.checks.get("missing_context", [])
|
|
145
|
+
return len(missing) > 0
|
|
146
|
+
return False
|
|
147
|
+
|
|
148
|
+
@property
|
|
149
|
+
def tool_selection_correct(self) -> bool | None:
|
|
150
|
+
"""
|
|
151
|
+
True/False only for Agent/Router nodes with a judged tool_selection verdict
|
|
152
|
+
(the agent's own decision — the canonical source). None for Tool/Retriever
|
|
153
|
+
nodes (which only carry a redundant copy) and for nodes where the check
|
|
154
|
+
wasn't applicable (verdict N/A).
|
|
155
|
+
"""
|
|
156
|
+
if not self.tool_agent_result or not self.tool_agent_result.checks:
|
|
157
|
+
return None
|
|
158
|
+
if self.node_type not in ("Agent", "Router"):
|
|
159
|
+
return None
|
|
160
|
+
ts = self.tool_agent_result.checks.get("tool_selection", {})
|
|
161
|
+
if ts.get("verdict") in (None, "N/A"):
|
|
162
|
+
return None
|
|
163
|
+
return bool(ts.get("is_right_tool"))
|
|
164
|
+
|
|
165
|
+
def all_results(self) -> list[EvaluationResult]:
|
|
166
|
+
return [r for r in [self.input_result, self.output_result, self.tool_agent_result] if r]
|
|
167
|
+
|
|
168
|
+
def flat_eval_props(self) -> dict:
|
|
169
|
+
"""Flat boolean properties written directly to the node document for fast filtering."""
|
|
170
|
+
return {
|
|
171
|
+
"eval_overall_status": self.overall_status,
|
|
172
|
+
"eval_severity": self.worst_severity,
|
|
173
|
+
"eval_input_passed": self.input_result.status == "PASS" if self.input_result else None,
|
|
174
|
+
"eval_output_passed": self.output_result.status == "PASS" if self.output_result else None,
|
|
175
|
+
"eval_tool_passed": self.tool_agent_result.status == "PASS" if self.tool_agent_result else None,
|
|
176
|
+
"tool_selection_correct": self.tool_selection_correct,
|
|
177
|
+
"has_hallucination": self.has_hallucination,
|
|
178
|
+
"has_injection": self.has_injection,
|
|
179
|
+
"has_missing_context": self.has_missing_context,
|
|
180
|
+
}
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
"""
|
|
2
|
+
output_evaluator.py
|
|
3
|
+
-------------------
|
|
4
|
+
Evaluator 2 — Output Evaluator
|
|
5
|
+
|
|
6
|
+
Validates the OUTPUT of every execution node.
|
|
7
|
+
Sees: node.input (full context) + node.output.
|
|
8
|
+
|
|
9
|
+
Auto-detects output type:
|
|
10
|
+
- Structured → dict/JSON: checks consistency, structure correctness, handoff relevance
|
|
11
|
+
- Unstructured → string: checks hallucination, toxicity, relevance, completeness
|
|
12
|
+
|
|
13
|
+
checks schema for STRUCTURED output:
|
|
14
|
+
{
|
|
15
|
+
"output_type": "structured",
|
|
16
|
+
"is_relevant_to_input": bool,
|
|
17
|
+
"is_structurally_correct": bool,
|
|
18
|
+
"is_internally_consistent": bool,
|
|
19
|
+
"handoff_is_valid": bool | null, # null if no handoff
|
|
20
|
+
"schema_violations": [str],
|
|
21
|
+
"consistency_issues": [str]
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
checks schema for UNSTRUCTURED output:
|
|
25
|
+
{
|
|
26
|
+
"output_type": "unstructured",
|
|
27
|
+
"is_relevant": bool,
|
|
28
|
+
"is_complete": bool,
|
|
29
|
+
"has_hallucination": bool,
|
|
30
|
+
"is_toxic": bool,
|
|
31
|
+
"hallucination_evidence": [str], # specific unsupported claims
|
|
32
|
+
"toxicity_evidence": str | null,
|
|
33
|
+
"completeness_gaps": [str] # what's missing from the answer
|
|
34
|
+
}
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
from __future__ import annotations
|
|
38
|
+
|
|
39
|
+
import json
|
|
40
|
+
|
|
41
|
+
from graph_builder.models import ExecutionGraph, ExecutionNode
|
|
42
|
+
from .base_evaluator import BaseEvaluator
|
|
43
|
+
from .models import EvaluationResult, EvaluationStatus, Severity
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
_PROMPT_STRUCTURED = """\
|
|
47
|
+
You are an AI agent execution auditor. Evaluate the OUTPUT of an AI agent node that produced STRUCTURED (JSON/dict) output.
|
|
48
|
+
|
|
49
|
+
## Node Being Evaluated
|
|
50
|
+
{node_context}
|
|
51
|
+
|
|
52
|
+
## Full Context (what this node had access to)
|
|
53
|
+
{full_context}
|
|
54
|
+
|
|
55
|
+
## Output Produced
|
|
56
|
+
{output}
|
|
57
|
+
|
|
58
|
+
## Instructions
|
|
59
|
+
Analyze whether this structured output is correct. Check for:
|
|
60
|
+
|
|
61
|
+
1. **Relevance** — Does the output address the input request? Is it relevant?
|
|
62
|
+
2. **Structural Correctness** — Is the JSON/dict well-formed? Are required keys present?
|
|
63
|
+
3. **Internal Consistency** — Are there contradictions within the output itself?
|
|
64
|
+
4. **Handoff Validity** — If this output routes to another agent (handoff), is that routing decision appropriate given the input?
|
|
65
|
+
5. **Schema Violations** — List any fields that are wrong type, missing, or extraneous.
|
|
66
|
+
|
|
67
|
+
## Severity Guide
|
|
68
|
+
- CRITICAL: Output is completely wrong or will corrupt downstream agents
|
|
69
|
+
- HIGH: Major structural violations or highly irrelevant output
|
|
70
|
+
- MEDIUM: Inconsistencies or partially wrong structure
|
|
71
|
+
- LOW: Minor style issues or unnecessary fields
|
|
72
|
+
|
|
73
|
+
## Response Format
|
|
74
|
+
Respond ONLY with a valid JSON object. No explanation outside the JSON.
|
|
75
|
+
|
|
76
|
+
{{
|
|
77
|
+
"status": "PASS" | "FAIL" | "WARNING",
|
|
78
|
+
"severity": "LOW" | "MEDIUM" | "HIGH" | "CRITICAL" | null,
|
|
79
|
+
"confidence": <float 0.0–1.0>,
|
|
80
|
+
"reason": "<1-2 sentence explanation>",
|
|
81
|
+
"suggestion": "<specific fix or null if PASS>",
|
|
82
|
+
"checks": {{
|
|
83
|
+
"output_type": "structured",
|
|
84
|
+
"is_relevant_to_input": true | false,
|
|
85
|
+
"is_structurally_correct": true | false,
|
|
86
|
+
"is_internally_consistent": true | false,
|
|
87
|
+
"handoff_is_valid": true | false | null,
|
|
88
|
+
"schema_violations": ["<violation1>"],
|
|
89
|
+
"consistency_issues": ["<issue1>"]
|
|
90
|
+
}}
|
|
91
|
+
}}
|
|
92
|
+
"""
|
|
93
|
+
|
|
94
|
+
_PROMPT_UNSTRUCTURED = """\
|
|
95
|
+
You are an AI agent execution auditor. Evaluate the OUTPUT of an AI agent node that produced UNSTRUCTURED (text) output.
|
|
96
|
+
|
|
97
|
+
## Node Being Evaluated
|
|
98
|
+
{node_context}
|
|
99
|
+
|
|
100
|
+
## Full Context (what this node had access to)
|
|
101
|
+
{full_context}
|
|
102
|
+
|
|
103
|
+
## Output / Response Produced
|
|
104
|
+
{output}
|
|
105
|
+
|
|
106
|
+
## Instructions
|
|
107
|
+
Analyze whether this text output is correct. Check for:
|
|
108
|
+
|
|
109
|
+
1. **Relevance** — Does the response directly address the input question/task?
|
|
110
|
+
2. **Completeness** — Does it answer fully, or are important parts missing?
|
|
111
|
+
3. **Hallucination** — Does it make specific factual claims NOT supported by the context provided? List each claim separately.
|
|
112
|
+
4. **Toxicity** — Does the output contain harmful, offensive, or inappropriate content?
|
|
113
|
+
|
|
114
|
+
## Important Hallucination Guide
|
|
115
|
+
Only flag hallucination if the output makes specific, verifiable claims about facts, names, numbers, or events that are not present in or supportable by the context. Do NOT flag it as hallucination if the node is doing legitimate reasoning or summarization.
|
|
116
|
+
|
|
117
|
+
## Severity Guide
|
|
118
|
+
- CRITICAL: Toxic content, or severe hallucination with multiple unsupported facts
|
|
119
|
+
- HIGH: Clear hallucination of key facts, or output is completely off-topic
|
|
120
|
+
- MEDIUM: Partial hallucination or significantly incomplete answer
|
|
121
|
+
- LOW: Minor incompleteness or slightly off-topic
|
|
122
|
+
|
|
123
|
+
## Response Format
|
|
124
|
+
Respond ONLY with a valid JSON object. No explanation outside the JSON.
|
|
125
|
+
|
|
126
|
+
{{
|
|
127
|
+
"status": "PASS" | "FAIL" | "WARNING",
|
|
128
|
+
"severity": "LOW" | "MEDIUM" | "HIGH" | "CRITICAL" | null,
|
|
129
|
+
"confidence": <float 0.0–1.0>,
|
|
130
|
+
"reason": "<1-2 sentence explanation>",
|
|
131
|
+
"suggestion": "<specific fix or null if PASS>",
|
|
132
|
+
"checks": {{
|
|
133
|
+
"output_type": "unstructured",
|
|
134
|
+
"is_relevant": true | false,
|
|
135
|
+
"is_complete": true | false,
|
|
136
|
+
"has_hallucination": true | false,
|
|
137
|
+
"is_toxic": true | false,
|
|
138
|
+
"hallucination_evidence": ["<specific unsupported claim 1>"],
|
|
139
|
+
"toxicity_evidence": "<description or null>",
|
|
140
|
+
"completeness_gaps": ["<missing item 1>"]
|
|
141
|
+
}}
|
|
142
|
+
}}
|
|
143
|
+
"""
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _detect_output_type(output: object) -> str:
|
|
147
|
+
"""Return 'structured' if output is a dict/list, else 'unstructured'."""
|
|
148
|
+
if isinstance(output, (dict, list)):
|
|
149
|
+
return "structured"
|
|
150
|
+
if isinstance(output, str):
|
|
151
|
+
stripped = output.strip()
|
|
152
|
+
if stripped.startswith("{") or stripped.startswith("["):
|
|
153
|
+
try:
|
|
154
|
+
json.loads(stripped)
|
|
155
|
+
return "structured"
|
|
156
|
+
except (json.JSONDecodeError, ValueError):
|
|
157
|
+
pass
|
|
158
|
+
return "unstructured"
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
class OutputEvaluator(BaseEvaluator):
|
|
162
|
+
"""Evaluator 2: Validates output quality, hallucination, structure, and handoffs."""
|
|
163
|
+
|
|
164
|
+
name = "OutputEvaluator"
|
|
165
|
+
|
|
166
|
+
def should_run(self, node: ExecutionNode, graph: ExecutionGraph) -> bool:
|
|
167
|
+
# Runs on every node with actual content — a pure lifecycle/
|
|
168
|
+
# bookkeeping span (zero I/O) produced no output to judge.
|
|
169
|
+
return self._has_content(node)
|
|
170
|
+
|
|
171
|
+
def _run(self, node: ExecutionNode, graph: ExecutionGraph) -> EvaluationResult:
|
|
172
|
+
output = node.output or node.response
|
|
173
|
+
output_type = _detect_output_type(output)
|
|
174
|
+
|
|
175
|
+
# Build full context: parent + sibling inputs
|
|
176
|
+
full_context = self._build_full_context(node, graph)
|
|
177
|
+
|
|
178
|
+
if output_type == "structured":
|
|
179
|
+
prompt = _PROMPT_STRUCTURED.format(
|
|
180
|
+
node_context=self._format_node(node),
|
|
181
|
+
full_context=full_context,
|
|
182
|
+
output=self._safe_json(output),
|
|
183
|
+
)
|
|
184
|
+
else:
|
|
185
|
+
prompt = _PROMPT_UNSTRUCTURED.format(
|
|
186
|
+
node_context=self._format_node(node),
|
|
187
|
+
full_context=full_context,
|
|
188
|
+
output=self._safe_json(output)[:3000],
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
parsed = self._call_and_parse(prompt)
|
|
192
|
+
|
|
193
|
+
status_raw = parsed.get("status", "FAIL")
|
|
194
|
+
try:
|
|
195
|
+
status = EvaluationStatus(status_raw)
|
|
196
|
+
except ValueError:
|
|
197
|
+
status = EvaluationStatus.FAIL
|
|
198
|
+
|
|
199
|
+
checks = parsed.get("checks", {})
|
|
200
|
+
|
|
201
|
+
severity = self._severity_from_status(parsed, default=Severity.MEDIUM)
|
|
202
|
+
# Override: toxicity is always CRITICAL
|
|
203
|
+
if checks.get("is_toxic"):
|
|
204
|
+
status = EvaluationStatus.FAIL
|
|
205
|
+
severity = Severity.CRITICAL
|
|
206
|
+
|
|
207
|
+
return EvaluationResult(
|
|
208
|
+
evaluator=self.name,
|
|
209
|
+
node_id=node.node_id,
|
|
210
|
+
node_name=node.name,
|
|
211
|
+
node_type=node.node_type,
|
|
212
|
+
execution_id=node.execution_id,
|
|
213
|
+
session_id=node.session_id,
|
|
214
|
+
status=status,
|
|
215
|
+
severity=severity if status != EvaluationStatus.PASS else None,
|
|
216
|
+
confidence=float(parsed.get("confidence", 0.5)),
|
|
217
|
+
reason=parsed.get("reason", ""),
|
|
218
|
+
suggestion=parsed.get("suggestion"),
|
|
219
|
+
checks=checks,
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
def _build_full_context(self, node: ExecutionNode, graph: ExecutionGraph) -> str:
|
|
223
|
+
"""
|
|
224
|
+
Build a human-readable string of everything this node had access to:
|
|
225
|
+
- Parent node's input/output
|
|
226
|
+
- Sibling nodes (same parent) that ran before this node
|
|
227
|
+
"""
|
|
228
|
+
lines = []
|
|
229
|
+
parent = self._get_parent(node, graph)
|
|
230
|
+
if parent:
|
|
231
|
+
lines.append(f"[Parent Node: {parent.name} ({parent.node_type})]")
|
|
232
|
+
lines.append(f" Parent Input: {str(parent.input or '')[:1000]}")
|
|
233
|
+
lines.append(f" Parent Output: {str(parent.output or '')[:1000]}")
|
|
234
|
+
|
|
235
|
+
# Siblings that ran before (ordered by timestamp)
|
|
236
|
+
if parent:
|
|
237
|
+
siblings = [
|
|
238
|
+
n for n in graph.get_children(parent.node_id)
|
|
239
|
+
if n.node_id != node.node_id
|
|
240
|
+
and (n.timestamp or "") < (node.timestamp or "")
|
|
241
|
+
]
|
|
242
|
+
for sib in siblings:
|
|
243
|
+
lines.append(f"\n[Prior Sibling: {sib.name} ({sib.node_type})]")
|
|
244
|
+
lines.append(f" Input: {str(sib.input or '')[:500]}")
|
|
245
|
+
lines.append(f" Output: {str(sib.output or '')[:500]}")
|
|
246
|
+
|
|
247
|
+
return "\n".join(lines) if lines else "(No additional context available)"
|