traceforge-toolkit 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.
- traceforge/__init__.py +72 -0
- traceforge/__main__.py +5 -0
- traceforge/_generated.py +254 -0
- traceforge/adapters/__init__.py +12 -0
- traceforge/adapters/base.py +82 -0
- traceforge/adapters/genai_otel.py +164 -0
- traceforge/adapters/mapped_json.py +297 -0
- traceforge/adapters/otel.py +220 -0
- traceforge/boundary/__init__.py +46 -0
- traceforge/boundary/data/boundary-model.joblib +0 -0
- traceforge/boundary/decode.py +87 -0
- traceforge/boundary/features.py +137 -0
- traceforge/boundary/inference.py +267 -0
- traceforge/boundary/inferencer.py +146 -0
- traceforge/classify/__init__.py +95 -0
- traceforge/classify/cmd.py +109 -0
- traceforge/classify/coding.py +213 -0
- traceforge/classify/config.py +554 -0
- traceforge/classify/core.py +266 -0
- traceforge/classify/data/binary_info.yaml +358 -0
- traceforge/classify/data/canonical_tools.yaml +251 -0
- traceforge/classify/data/effect_overrides.yaml +480 -0
- traceforge/classify/data/mcp_profiles.yaml +711 -0
- traceforge/classify/data/recommendation_rules.yaml +111 -0
- traceforge/classify/data/risk.yaml +534 -0
- traceforge/classify/data/shell_defaults.yaml +95 -0
- traceforge/classify/data/shell_rules.yaml +1192 -0
- traceforge/classify/data/tool_classifications.yaml +215 -0
- traceforge/classify/data/verb_inference.yaml +218 -0
- traceforge/classify/mcp.py +181 -0
- traceforge/classify/phases.py +75 -0
- traceforge/classify/powershell.py +93 -0
- traceforge/classify/registry.py +138 -0
- traceforge/classify/risk.py +553 -0
- traceforge/classify/rules.py +215 -0
- traceforge/classify/schema.yaml +282 -0
- traceforge/classify/shell.py +503 -0
- traceforge/classify/tools.py +91 -0
- traceforge/classify/workflow.py +32 -0
- traceforge/cli/__init__.py +42 -0
- traceforge/cli/config_cmd.py +130 -0
- traceforge/cli/detect.py +46 -0
- traceforge/cli/download_cmd.py +74 -0
- traceforge/cli/factory.py +60 -0
- traceforge/cli/gate_cmd.py +30 -0
- traceforge/cli/init_cmd.py +86 -0
- traceforge/cli/replay.py +130 -0
- traceforge/cli/runner.py +181 -0
- traceforge/cli/score.py +217 -0
- traceforge/cli/status.py +65 -0
- traceforge/cli/watch.py +297 -0
- traceforge/config/__init__.py +80 -0
- traceforge/config/defaults.py +149 -0
- traceforge/config/loader.py +239 -0
- traceforge/config/mappings.py +104 -0
- traceforge/config/models.py +579 -0
- traceforge/enricher.py +576 -0
- traceforge/formatting/__init__.py +12 -0
- traceforge/formatting/budget.py +92 -0
- traceforge/formatting/density.py +154 -0
- traceforge/gate/__init__.py +17 -0
- traceforge/gate/client.py +145 -0
- traceforge/gate/external.py +305 -0
- traceforge/gate/registry.py +108 -0
- traceforge/gate/server.py +174 -0
- traceforge/gates/__init__.py +8 -0
- traceforge/gates/pii.py +352 -0
- traceforge/gates/pii_patterns.yaml +228 -0
- traceforge/governance/__init__.py +121 -0
- traceforge/governance/assessor.py +441 -0
- traceforge/governance/budget.py +59 -0
- traceforge/governance/canonical.py +56 -0
- traceforge/governance/codec.py +414 -0
- traceforge/governance/context.py +249 -0
- traceforge/governance/drift.py +196 -0
- traceforge/governance/emitter.py +234 -0
- traceforge/governance/envelope.py +138 -0
- traceforge/governance/ifc.py +364 -0
- traceforge/governance/integrity.py +162 -0
- traceforge/governance/labeler.py +250 -0
- traceforge/governance/mcp_drift.py +328 -0
- traceforge/governance/monitor.py +539 -0
- traceforge/governance/observer.py +276 -0
- traceforge/governance/persistence.py +401 -0
- traceforge/governance/phase1.py +77 -0
- traceforge/governance/pii.py +276 -0
- traceforge/governance/pipeline.py +840 -0
- traceforge/governance/registry.py +110 -0
- traceforge/governance/results.py +183 -0
- traceforge/governance/risk_wrapper.py +113 -0
- traceforge/governance/rules.py +368 -0
- traceforge/governance/scorer.py +262 -0
- traceforge/governance/shield.py +318 -0
- traceforge/governance/state.py +466 -0
- traceforge/governance/types.py +176 -0
- traceforge/mappings/__init__.py +1 -0
- traceforge/mappings/aider.yaml +216 -0
- traceforge/mappings/aider_markdown.yaml +113 -0
- traceforge/mappings/amazonq.yaml +56 -0
- traceforge/mappings/antigravity.yaml +88 -0
- traceforge/mappings/claude.yaml +93 -0
- traceforge/mappings/cline.yaml +286 -0
- traceforge/mappings/codex.yaml +158 -0
- traceforge/mappings/continue_dev.yaml +57 -0
- traceforge/mappings/copilot.yaml +266 -0
- traceforge/mappings/copilot_markdown.yaml +72 -0
- traceforge/mappings/copilot_vscode.yaml +181 -0
- traceforge/mappings/crewai.yaml +592 -0
- traceforge/mappings/goose.yaml +128 -0
- traceforge/mappings/langgraph.yaml +165 -0
- traceforge/mappings/maf.yaml +90 -0
- traceforge/mappings/maf_transcript.yaml +99 -0
- traceforge/mappings/openai_agents.yaml +144 -0
- traceforge/mappings/opencode.yaml +473 -0
- traceforge/mappings/openhands.yaml +320 -0
- traceforge/mappings/pydantic_ai.yaml +183 -0
- traceforge/mappings/smolagents.yaml +89 -0
- traceforge/mappings/sweagent.yaml +82 -0
- traceforge/migrations/__init__.py +1 -0
- traceforge/migrations/env.py +60 -0
- traceforge/migrations/models.py +145 -0
- traceforge/migrations/runner.py +44 -0
- traceforge/migrations/script.py.mako +25 -0
- traceforge/migrations/versions/0001_initial.py +176 -0
- traceforge/migrations/versions/__init__.py +4 -0
- traceforge/models.py +29 -0
- traceforge/parsers/__init__.py +21 -0
- traceforge/parsers/aider.py +416 -0
- traceforge/parsers/base.py +226 -0
- traceforge/parsers/copilot.py +314 -0
- traceforge/phase/__init__.py +50 -0
- traceforge/phase/data/phase-model.joblib +0 -0
- traceforge/phase/data/potion-base-8M/README.md +99 -0
- traceforge/phase/data/potion-base-8M/config.json +13 -0
- traceforge/phase/data/potion-base-8M/model.safetensors +0 -0
- traceforge/phase/data/potion-base-8M/modules.json +14 -0
- traceforge/phase/data/potion-base-8M/tokenizer.json +1 -0
- traceforge/phase/event_rows.py +107 -0
- traceforge/phase/features.py +468 -0
- traceforge/phase/inference.py +279 -0
- traceforge/phase/inferencer.py +171 -0
- traceforge/phase/segmentation.py +258 -0
- traceforge/pipeline.py +891 -0
- traceforge/preprocessors/__init__.py +34 -0
- traceforge/preprocessors/amazonq.py +224 -0
- traceforge/preprocessors/antigravity.py +116 -0
- traceforge/preprocessors/claude.py +95 -0
- traceforge/preprocessors/cline.py +36 -0
- traceforge/preprocessors/codex.py +311 -0
- traceforge/preprocessors/continue_dev.py +119 -0
- traceforge/preprocessors/copilot_vscode.py +171 -0
- traceforge/preprocessors/goose.py +156 -0
- traceforge/preprocessors/maf_transcript.py +84 -0
- traceforge/preprocessors/openai_agents.py +86 -0
- traceforge/preprocessors/opencode.py +85 -0
- traceforge/preprocessors/openhands.py +36 -0
- traceforge/preprocessors/pydantic_ai.py +62 -0
- traceforge/preprocessors/registry.py +24 -0
- traceforge/preprocessors/smolagents.py +90 -0
- traceforge/py.typed +0 -0
- traceforge/sdk/__init__.py +59 -0
- traceforge/sdk/gate_policy.py +63 -0
- traceforge/sdk/gate_types.py +140 -0
- traceforge/sdk/pipeline.py +265 -0
- traceforge/sdk/verdict.py +81 -0
- traceforge/sinks/__init__.py +23 -0
- traceforge/sinks/base.py +95 -0
- traceforge/sinks/callback.py +132 -0
- traceforge/sinks/console.py +112 -0
- traceforge/sinks/factory.py +94 -0
- traceforge/sinks/jsonl.py +125 -0
- traceforge/sinks/otel_exporter.py +212 -0
- traceforge/sinks/parquet.py +260 -0
- traceforge/sinks/s3.py +206 -0
- traceforge/sinks/sqlite_output.py +234 -0
- traceforge/sinks/webhook.py +136 -0
- traceforge/sources/__init__.py +18 -0
- traceforge/sources/auto_detect.py +173 -0
- traceforge/sources/base.py +45 -0
- traceforge/sources/file_poll.py +136 -0
- traceforge/sources/file_watch.py +221 -0
- traceforge/sources/http_poll.py +141 -0
- traceforge/sources/replay.py +63 -0
- traceforge/sources/sqlite.py +187 -0
- traceforge/sources/sse.py +198 -0
- traceforge/telemetry/__init__.py +192 -0
- traceforge/title/__init__.py +23 -0
- traceforge/title/_resolve.py +79 -0
- traceforge/title/context.py +295 -0
- traceforge/title/data/boilerplate_files.json +14 -0
- traceforge/title/heuristics.py +429 -0
- traceforge/title/hygiene.py +90 -0
- traceforge/title/inference.py +314 -0
- traceforge/title/inferencer.py +477 -0
- traceforge/title/naming.py +398 -0
- traceforge/trace.py +291 -0
- traceforge/tracking/__init__.py +30 -0
- traceforge/tracking/models.py +115 -0
- traceforge/tracking/phase_tracker.py +288 -0
- traceforge/types.py +315 -0
- traceforge_toolkit-0.1.0.dist-info/METADATA +188 -0
- traceforge_toolkit-0.1.0.dist-info/RECORD +205 -0
- traceforge_toolkit-0.1.0.dist-info/WHEEL +4 -0
- traceforge_toolkit-0.1.0.dist-info/entry_points.txt +2 -0
- traceforge_toolkit-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
"""Goose preprocessor — flatten nested content_json into typed events."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from traceforge.preprocessors.registry import register_preprocessor
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@register_preprocessor("goose")
|
|
12
|
+
def preprocess_goose(obj: dict[str, Any]) -> list[dict[str, Any]]:
|
|
13
|
+
"""Flatten Goose nested content_json into separate typed events."""
|
|
14
|
+
results = []
|
|
15
|
+
role = obj.get("role")
|
|
16
|
+
content_json_raw = obj.get("content_json")
|
|
17
|
+
ts = obj.get("created_at") or obj.get("created_timestamp")
|
|
18
|
+
|
|
19
|
+
if not content_json_raw:
|
|
20
|
+
return [obj]
|
|
21
|
+
|
|
22
|
+
# Parse content_json if it's a string
|
|
23
|
+
if isinstance(content_json_raw, str):
|
|
24
|
+
try:
|
|
25
|
+
content_items = json.loads(content_json_raw)
|
|
26
|
+
except (json.JSONDecodeError, ValueError):
|
|
27
|
+
return [obj]
|
|
28
|
+
else:
|
|
29
|
+
content_items = content_json_raw
|
|
30
|
+
|
|
31
|
+
if not isinstance(content_items, list):
|
|
32
|
+
return [obj]
|
|
33
|
+
|
|
34
|
+
# Extract nested events from content array
|
|
35
|
+
has_text = False
|
|
36
|
+
for item in content_items:
|
|
37
|
+
if not isinstance(item, dict):
|
|
38
|
+
continue
|
|
39
|
+
item_type = item.get("type", "")
|
|
40
|
+
|
|
41
|
+
if item_type == "text":
|
|
42
|
+
has_text = True
|
|
43
|
+
elif item_type == "toolRequest":
|
|
44
|
+
tool_call = item.get("toolCall", {})
|
|
45
|
+
value = (tool_call.get("value") or {}) if isinstance(tool_call, dict) else {}
|
|
46
|
+
results.append(
|
|
47
|
+
{
|
|
48
|
+
"role": "tool_use",
|
|
49
|
+
"created_at": ts,
|
|
50
|
+
"name": value.get("name", ""),
|
|
51
|
+
"id": item.get("id", ""),
|
|
52
|
+
"input": value.get("arguments", {}),
|
|
53
|
+
}
|
|
54
|
+
)
|
|
55
|
+
elif item_type == "toolResponse":
|
|
56
|
+
tool_result = item.get("toolResult", {})
|
|
57
|
+
results.append(
|
|
58
|
+
{
|
|
59
|
+
"role": "tool_result",
|
|
60
|
+
"created_at": ts,
|
|
61
|
+
"tool_use_id": item.get("id", ""),
|
|
62
|
+
"content": (tool_result.get("value") or {}).get("content", "")
|
|
63
|
+
if isinstance(tool_result, dict)
|
|
64
|
+
else "",
|
|
65
|
+
"is_success": tool_result.get("status") == "success"
|
|
66
|
+
if isinstance(tool_result, dict)
|
|
67
|
+
else False,
|
|
68
|
+
}
|
|
69
|
+
)
|
|
70
|
+
elif item_type == "thinking":
|
|
71
|
+
results.append(
|
|
72
|
+
{
|
|
73
|
+
"role": "thinking",
|
|
74
|
+
"created_at": ts,
|
|
75
|
+
"thinking": item.get("thinking", ""),
|
|
76
|
+
}
|
|
77
|
+
)
|
|
78
|
+
elif item_type == "redactedThinking":
|
|
79
|
+
results.append(
|
|
80
|
+
{
|
|
81
|
+
"role": "redacted_thinking",
|
|
82
|
+
"created_at": ts,
|
|
83
|
+
"data": item.get("data", ""),
|
|
84
|
+
}
|
|
85
|
+
)
|
|
86
|
+
elif item_type == "image":
|
|
87
|
+
results.append(
|
|
88
|
+
{
|
|
89
|
+
"role": "image",
|
|
90
|
+
"created_at": ts,
|
|
91
|
+
"data": item.get("data", ""),
|
|
92
|
+
}
|
|
93
|
+
)
|
|
94
|
+
elif item_type == "toolConfirmationRequest":
|
|
95
|
+
results.append(
|
|
96
|
+
{
|
|
97
|
+
"role": "tool_confirmation_request",
|
|
98
|
+
"created_at": ts,
|
|
99
|
+
"content": item.get("prompt", ""),
|
|
100
|
+
"name": item.get("toolName", ""),
|
|
101
|
+
"id": item.get("id", ""),
|
|
102
|
+
}
|
|
103
|
+
)
|
|
104
|
+
elif item_type == "actionRequired":
|
|
105
|
+
data = item.get("data", {}) if isinstance(item.get("data"), dict) else {}
|
|
106
|
+
# ActionRequiredData sub-variants:
|
|
107
|
+
# Elicitation: {actionType, id, message, requestedSchema}
|
|
108
|
+
# ToolConfirmation: {actionType, id, toolName, arguments, prompt}
|
|
109
|
+
# ElicitationResponse: {actionType, id, userData}
|
|
110
|
+
content = data.get("message") or data.get("prompt") or str(data.get("userData", ""))
|
|
111
|
+
results.append(
|
|
112
|
+
{
|
|
113
|
+
"role": "action_required",
|
|
114
|
+
"created_at": ts,
|
|
115
|
+
"content": content,
|
|
116
|
+
"action_type": data.get("actionType", ""),
|
|
117
|
+
}
|
|
118
|
+
)
|
|
119
|
+
elif item_type == "frontendToolRequest":
|
|
120
|
+
tool_call = item.get("toolCall", {})
|
|
121
|
+
value = (tool_call.get("value") or {}) if isinstance(tool_call, dict) else {}
|
|
122
|
+
results.append(
|
|
123
|
+
{
|
|
124
|
+
"role": "frontend_tool_request",
|
|
125
|
+
"created_at": ts,
|
|
126
|
+
"name": value.get("name", ""),
|
|
127
|
+
"id": item.get("id", ""),
|
|
128
|
+
"input": value.get("arguments", {}),
|
|
129
|
+
}
|
|
130
|
+
)
|
|
131
|
+
elif item_type == "systemNotification":
|
|
132
|
+
results.append(
|
|
133
|
+
{
|
|
134
|
+
"role": "system_notification",
|
|
135
|
+
"created_at": ts,
|
|
136
|
+
"content": item.get("msg", ""),
|
|
137
|
+
}
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
# Always emit the message itself (with role)
|
|
141
|
+
if has_text or not results:
|
|
142
|
+
text_parts = [
|
|
143
|
+
i.get("text", "")
|
|
144
|
+
for i in content_items
|
|
145
|
+
if isinstance(i, dict) and i.get("type") == "text"
|
|
146
|
+
]
|
|
147
|
+
results.insert(
|
|
148
|
+
0,
|
|
149
|
+
{
|
|
150
|
+
"role": role,
|
|
151
|
+
"created_at": ts,
|
|
152
|
+
"content": "\n".join(text_parts) if text_parts else content_json_raw,
|
|
153
|
+
},
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
return results
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""MAF transcript preprocessor — normalize Activity JSONL into typed events.
|
|
2
|
+
|
|
3
|
+
The Microsoft 365 Agents SDK `FileTranscriptStore` writes one Activity per JSONL
|
|
4
|
+
line. Activities use `type` ("message", "typing", "event", "invoke", etc.) and
|
|
5
|
+
`from.role` ("bot", "user") to distinguish direction. This preprocessor emits a
|
|
6
|
+
compound `_event_type` field of the form `{type}.{from_role}` so the YAML mapping
|
|
7
|
+
can discriminate assistant vs user messages.
|
|
8
|
+
|
|
9
|
+
Expected input shape (each JSONL line parsed as dict):
|
|
10
|
+
{
|
|
11
|
+
"type": "message",
|
|
12
|
+
"text": "I'll look into that for you.",
|
|
13
|
+
"timestamp": "2025-01-15T10:30:00Z",
|
|
14
|
+
"from": {"id": "bot-id", "name": "Agent", "role": "bot"},
|
|
15
|
+
"recipient": {"id": "user-id", "name": "User", "role": "user"},
|
|
16
|
+
"conversation": {"id": "conv-123"},
|
|
17
|
+
"channel_id": "directline",
|
|
18
|
+
"id": "activity-456",
|
|
19
|
+
...
|
|
20
|
+
}
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
from typing import Any
|
|
26
|
+
|
|
27
|
+
from traceforge.preprocessors.registry import register_preprocessor
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@register_preprocessor("maf_transcript")
|
|
31
|
+
def preprocess_maf_transcript(obj: dict[str, Any]) -> list[dict[str, Any]]:
|
|
32
|
+
"""Flatten MAF Activity dict into a single event with compound type."""
|
|
33
|
+
activity_type = obj.get("type", "unknown")
|
|
34
|
+
|
|
35
|
+
# Determine sender role — normalize to lowercase
|
|
36
|
+
from_obj = obj.get("from") or obj.get("from_") or {}
|
|
37
|
+
from_role = ""
|
|
38
|
+
if isinstance(from_obj, dict):
|
|
39
|
+
from_role = (from_obj.get("role") or "").lower()
|
|
40
|
+
|
|
41
|
+
# Compound type: e.g. "message.bot", "message.user", "typing.bot", "event.bot"
|
|
42
|
+
event_type = f"{activity_type}.{from_role}" if from_role else activity_type
|
|
43
|
+
|
|
44
|
+
# Flatten key fields to top level for YAML mapping
|
|
45
|
+
result: dict[str, Any] = {
|
|
46
|
+
"_event_type": event_type,
|
|
47
|
+
"timestamp": obj.get("timestamp"),
|
|
48
|
+
"text": obj.get("text"),
|
|
49
|
+
"activity_id": obj.get("id"),
|
|
50
|
+
"channel_id": obj.get("channel_id"),
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
# Conversation ID
|
|
54
|
+
conversation = obj.get("conversation")
|
|
55
|
+
if isinstance(conversation, dict):
|
|
56
|
+
result["conversation_id"] = conversation.get("id")
|
|
57
|
+
|
|
58
|
+
# From metadata
|
|
59
|
+
if isinstance(from_obj, dict):
|
|
60
|
+
result["from_id"] = from_obj.get("id")
|
|
61
|
+
result["from_name"] = from_obj.get("name")
|
|
62
|
+
result["from_role"] = from_obj.get("role")
|
|
63
|
+
|
|
64
|
+
# Recipient metadata
|
|
65
|
+
recipient = obj.get("recipient")
|
|
66
|
+
if isinstance(recipient, dict):
|
|
67
|
+
result["recipient_id"] = recipient.get("id")
|
|
68
|
+
result["recipient_name"] = recipient.get("name")
|
|
69
|
+
|
|
70
|
+
# Attachments (tool inputs/outputs often come as attachments)
|
|
71
|
+
attachments = obj.get("attachments")
|
|
72
|
+
if attachments and isinstance(attachments, list):
|
|
73
|
+
result["attachments"] = attachments
|
|
74
|
+
result["attachment_count"] = len(attachments)
|
|
75
|
+
|
|
76
|
+
# Value field (used for invoke activities — tool calls, adaptive cards)
|
|
77
|
+
if obj.get("value") is not None:
|
|
78
|
+
result["value"] = obj["value"]
|
|
79
|
+
|
|
80
|
+
# Channel data (framework-specific extensions)
|
|
81
|
+
if obj.get("channel_data") is not None:
|
|
82
|
+
result["channel_data"] = obj["channel_data"]
|
|
83
|
+
|
|
84
|
+
return [result]
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""OpenAI Agents SDK preprocessor — normalize trace/span exports."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from traceforge.preprocessors.registry import register_preprocessor
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@register_preprocessor("openai_agents")
|
|
11
|
+
def preprocess_openai_agents(obj: dict[str, Any]) -> list[dict[str, Any]]:
|
|
12
|
+
"""Flatten native Trace.export()/Span.export() rows into typed events."""
|
|
13
|
+
object_type = obj.get("object")
|
|
14
|
+
|
|
15
|
+
# Passthrough already-typed rows (re-ingested events or generic conformance
|
|
16
|
+
# probes that arrive keyed by the post-preprocessor type_field).
|
|
17
|
+
if object_type is None and obj.get("event_type"):
|
|
18
|
+
return [obj]
|
|
19
|
+
|
|
20
|
+
if object_type == "trace":
|
|
21
|
+
return [
|
|
22
|
+
{
|
|
23
|
+
"event_type": "trace",
|
|
24
|
+
"trace_id": obj.get("id"),
|
|
25
|
+
"workflow_name": obj.get("workflow_name"),
|
|
26
|
+
"group_id": obj.get("group_id"),
|
|
27
|
+
"metadata": obj.get("metadata"),
|
|
28
|
+
}
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
if object_type != "trace.span":
|
|
32
|
+
return []
|
|
33
|
+
|
|
34
|
+
span_data = obj.get("span_data") or {}
|
|
35
|
+
if not isinstance(span_data, dict):
|
|
36
|
+
return []
|
|
37
|
+
|
|
38
|
+
span_type = span_data.get("type")
|
|
39
|
+
base = {
|
|
40
|
+
"span_id": obj.get("id"),
|
|
41
|
+
"trace_id": obj.get("trace_id"),
|
|
42
|
+
"parent_id": obj.get("parent_id"),
|
|
43
|
+
"started_at": obj.get("started_at"),
|
|
44
|
+
"ended_at": obj.get("ended_at"),
|
|
45
|
+
"error": obj.get("error"),
|
|
46
|
+
"span_data": span_data,
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if span_type == "function":
|
|
50
|
+
started = {
|
|
51
|
+
**base,
|
|
52
|
+
"event_type": "function.started",
|
|
53
|
+
"timestamp": obj.get("started_at"),
|
|
54
|
+
"tool_name": span_data.get("name"),
|
|
55
|
+
"arguments": span_data.get("input"),
|
|
56
|
+
}
|
|
57
|
+
finished_type = "function.failed" if obj.get("error") else "function.completed"
|
|
58
|
+
finished = {
|
|
59
|
+
**base,
|
|
60
|
+
"event_type": finished_type,
|
|
61
|
+
"timestamp": obj.get("ended_at") or obj.get("started_at"),
|
|
62
|
+
"tool_name": span_data.get("name"),
|
|
63
|
+
"result": span_data.get("output"),
|
|
64
|
+
}
|
|
65
|
+
return [started, finished]
|
|
66
|
+
|
|
67
|
+
event_type_by_span = {
|
|
68
|
+
"agent": "agent.completed",
|
|
69
|
+
"generation": "generation.completed",
|
|
70
|
+
"response": "response.completed",
|
|
71
|
+
"handoff": "handoff.completed",
|
|
72
|
+
"guardrail": "guardrail.completed",
|
|
73
|
+
"custom": "custom.completed",
|
|
74
|
+
"speech": "speech.completed",
|
|
75
|
+
"speech_group": "speech_group.completed",
|
|
76
|
+
"transcription": "transcription.completed",
|
|
77
|
+
"mcp_tools": "mcp_tools.completed",
|
|
78
|
+
}
|
|
79
|
+
event_type = event_type_by_span.get(str(span_type), f"{span_type}.completed")
|
|
80
|
+
return [
|
|
81
|
+
{
|
|
82
|
+
**base,
|
|
83
|
+
"event_type": event_type,
|
|
84
|
+
"timestamp": obj.get("ended_at") or obj.get("started_at"),
|
|
85
|
+
}
|
|
86
|
+
]
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""OpenCode preprocessor — normalize v1.17 event-sourced SQLite rows."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import re
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from traceforge.preprocessors.registry import register_preprocessor
|
|
10
|
+
|
|
11
|
+
_VERSION_SUFFIX_RE = re.compile(r"\.\d+$")
|
|
12
|
+
_MESSAGE_ROLES: dict[tuple[str, str], str] = {}
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@register_preprocessor("opencode")
|
|
16
|
+
def preprocess_opencode(obj: dict[str, Any]) -> list[dict[str, Any]]:
|
|
17
|
+
"""Normalize OpenCode's SQLite `event` rows to stable mapping types."""
|
|
18
|
+
data = obj.get("data")
|
|
19
|
+
if isinstance(data, str):
|
|
20
|
+
try:
|
|
21
|
+
data = json.loads(data)
|
|
22
|
+
except (json.JSONDecodeError, TypeError):
|
|
23
|
+
data = {}
|
|
24
|
+
if not isinstance(data, dict):
|
|
25
|
+
data = {}
|
|
26
|
+
|
|
27
|
+
norm = dict(obj)
|
|
28
|
+
norm["data"] = data
|
|
29
|
+
if "properties" not in norm and data:
|
|
30
|
+
norm["properties"] = data
|
|
31
|
+
norm["type"] = _VERSION_SUFFIX_RE.sub("", str(obj.get("type", "")))
|
|
32
|
+
norm["_timestamp"] = _timestamp(data, norm)
|
|
33
|
+
|
|
34
|
+
if norm["type"] == "message.updated":
|
|
35
|
+
info = data.get("info") if isinstance(data.get("info"), dict) else {}
|
|
36
|
+
session_id = str(data.get("sessionID") or info.get("sessionID") or "")
|
|
37
|
+
message_id = str(info.get("id") or "")
|
|
38
|
+
role = str(info.get("role") or "unknown")
|
|
39
|
+
if session_id and message_id and role != "unknown":
|
|
40
|
+
_MESSAGE_ROLES[(session_id, message_id)] = role
|
|
41
|
+
norm["type"] = f"message.updated.{role}"
|
|
42
|
+
norm["message_role"] = role
|
|
43
|
+
return [norm]
|
|
44
|
+
|
|
45
|
+
if norm["type"] == "message.part.updated":
|
|
46
|
+
part = data.get("part") if isinstance(data.get("part"), dict) else {}
|
|
47
|
+
part_type = str(part.get("type") or "unknown")
|
|
48
|
+
session_id = str(data.get("sessionID") or part.get("sessionID") or "")
|
|
49
|
+
message_id = str(part.get("messageID") or "")
|
|
50
|
+
role = _MESSAGE_ROLES.get((session_id, message_id), "unknown")
|
|
51
|
+
norm["message_role"] = role
|
|
52
|
+
|
|
53
|
+
if part_type == "text" and role in {"user", "assistant"}:
|
|
54
|
+
norm["type"] = f"message.part.text.{role}"
|
|
55
|
+
elif part_type == "tool":
|
|
56
|
+
state = part.get("state") if isinstance(part.get("state"), dict) else {}
|
|
57
|
+
status = str(state.get("status") or "unknown")
|
|
58
|
+
if status == "pending":
|
|
59
|
+
# OpenCode emits a provisional tool row before input/timing exists;
|
|
60
|
+
# the following running/completed rows carry the canonical signal.
|
|
61
|
+
return []
|
|
62
|
+
norm["type"] = f"message.part.tool.{status}"
|
|
63
|
+
else:
|
|
64
|
+
norm["type"] = f"message.part.{part_type}"
|
|
65
|
+
return [norm]
|
|
66
|
+
|
|
67
|
+
return [norm]
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _timestamp(data: dict[str, Any], obj: dict[str, Any]) -> Any:
|
|
71
|
+
props = obj.get("properties") if isinstance(obj.get("properties"), dict) else {}
|
|
72
|
+
info = data.get("info") if isinstance(data.get("info"), dict) else {}
|
|
73
|
+
info_time = info.get("time") if isinstance(info.get("time"), dict) else {}
|
|
74
|
+
part = data.get("part") if isinstance(data.get("part"), dict) else {}
|
|
75
|
+
part_time = part.get("time") if isinstance(part.get("time"), dict) else {}
|
|
76
|
+
|
|
77
|
+
return (
|
|
78
|
+
data.get("timestamp")
|
|
79
|
+
or data.get("time")
|
|
80
|
+
or info_time.get("updated")
|
|
81
|
+
or info_time.get("created")
|
|
82
|
+
or part_time.get("end")
|
|
83
|
+
or part_time.get("start")
|
|
84
|
+
or props.get("timestamp")
|
|
85
|
+
)
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""OpenHands preprocessor — compound action/observation discriminator."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from traceforge.preprocessors.registry import register_preprocessor
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@register_preprocessor("openhands")
|
|
11
|
+
def preprocess_openhands(obj: dict[str, Any]) -> list[dict[str, Any]]:
|
|
12
|
+
"""Normalize OpenHands compound discriminator (legacy or SDK events).
|
|
13
|
+
|
|
14
|
+
OpenHands 0.x used an ``action``/``observation`` compound discriminator.
|
|
15
|
+
OpenHands 1.x SDK persistence uses ``kind`` plus ``source``/``tool_name``.
|
|
16
|
+
Synthesize the YAML ``action`` discriminator for both formats while
|
|
17
|
+
preserving the native nested payload for dot-path extraction.
|
|
18
|
+
"""
|
|
19
|
+
kind = obj.get("kind")
|
|
20
|
+
if kind in {"SystemPromptEvent", "ActionEvent", "ObservationEvent", "MessageEvent"}:
|
|
21
|
+
normalized = dict(obj)
|
|
22
|
+
if kind in {"ActionEvent", "ObservationEvent"}:
|
|
23
|
+
normalized["action"] = f"{kind}.{obj.get('tool_name', 'unknown')}"
|
|
24
|
+
elif kind == "MessageEvent":
|
|
25
|
+
normalized["action"] = f"MessageEvent.{obj.get('source', 'unknown')}"
|
|
26
|
+
else:
|
|
27
|
+
normalized["action"] = kind
|
|
28
|
+
return [normalized]
|
|
29
|
+
|
|
30
|
+
if "action" in obj:
|
|
31
|
+
return [obj]
|
|
32
|
+
elif "observation" in obj:
|
|
33
|
+
normalized = dict(obj)
|
|
34
|
+
normalized["action"] = f"observation.{normalized['observation']}"
|
|
35
|
+
return [normalized]
|
|
36
|
+
return [obj]
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""PydanticAI preprocessor — multi-level discrimination to flat type field."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from traceforge.preprocessors.registry import register_preprocessor
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@register_preprocessor("pydantic_ai")
|
|
11
|
+
def preprocess_pydantic_ai(obj: dict[str, Any]) -> list[dict[str, Any]]:
|
|
12
|
+
"""Normalize PydanticAI multi-level discrimination to flat type field.
|
|
13
|
+
|
|
14
|
+
Preserves nested structure for _resolve_path; only synthesizes the "type"
|
|
15
|
+
discriminator and extracts text content from parts arrays.
|
|
16
|
+
"""
|
|
17
|
+
# Stream and callback events have event_kind
|
|
18
|
+
if "event_kind" in obj:
|
|
19
|
+
normalized = dict(obj)
|
|
20
|
+
event_kind = normalized["event_kind"]
|
|
21
|
+
|
|
22
|
+
if event_kind == "function_tool_call":
|
|
23
|
+
normalized["type"] = "tool_call_start"
|
|
24
|
+
elif event_kind == "function_tool_result":
|
|
25
|
+
normalized["type"] = "tool_call_end"
|
|
26
|
+
elif event_kind == "model_response_stream":
|
|
27
|
+
normalized["type"] = "model_response_chunk"
|
|
28
|
+
if "chunk" not in normalized:
|
|
29
|
+
normalized["chunk"] = (normalized.get("part") or {}).get("content", "")
|
|
30
|
+
else:
|
|
31
|
+
normalized["type"] = f"stream.{event_kind}"
|
|
32
|
+
return [normalized]
|
|
33
|
+
|
|
34
|
+
# Messages have kind (request/response)
|
|
35
|
+
kind = obj.get("kind")
|
|
36
|
+
if kind == "response":
|
|
37
|
+
normalized = dict(obj)
|
|
38
|
+
normalized["type"] = "model_response"
|
|
39
|
+
# Extract text from parts for convenience
|
|
40
|
+
parts = normalized.get("parts") or []
|
|
41
|
+
text_parts = [
|
|
42
|
+
p.get("content", "")
|
|
43
|
+
for p in parts
|
|
44
|
+
if isinstance(p, dict) and p.get("part_kind") == "text"
|
|
45
|
+
]
|
|
46
|
+
if text_parts:
|
|
47
|
+
normalized["content"] = "\n".join(text_parts)
|
|
48
|
+
return [normalized]
|
|
49
|
+
elif kind == "request":
|
|
50
|
+
normalized = dict(obj)
|
|
51
|
+
normalized["type"] = "model_request"
|
|
52
|
+
parts = normalized.get("parts") or []
|
|
53
|
+
user_parts = [
|
|
54
|
+
p.get("content", "")
|
|
55
|
+
for p in parts
|
|
56
|
+
if isinstance(p, dict) and p.get("part_kind") == "user-prompt"
|
|
57
|
+
]
|
|
58
|
+
if user_parts:
|
|
59
|
+
normalized["content"] = "\n".join(user_parts)
|
|
60
|
+
return [normalized]
|
|
61
|
+
|
|
62
|
+
return [obj]
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""Preprocessor registry — central registration and lookup."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Callable
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
PreprocessorFn = Callable[[dict[str, Any]], list[dict[str, Any]]]
|
|
9
|
+
_PREPROCESSORS: dict[str, PreprocessorFn] = {}
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def register_preprocessor(name: str) -> Callable[[PreprocessorFn], PreprocessorFn]:
|
|
13
|
+
"""Decorator to register a preprocessor function."""
|
|
14
|
+
|
|
15
|
+
def decorator(fn: PreprocessorFn) -> PreprocessorFn:
|
|
16
|
+
_PREPROCESSORS[name] = fn
|
|
17
|
+
return fn
|
|
18
|
+
|
|
19
|
+
return decorator
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def get_preprocessor(name: str) -> PreprocessorFn | None:
|
|
23
|
+
"""Look up a registered preprocessor by name."""
|
|
24
|
+
return _PREPROCESSORS.get(name)
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"""smolagents preprocessor — infer step type from field presence."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from traceforge.preprocessors.registry import register_preprocessor
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@register_preprocessor("smolagents")
|
|
11
|
+
def preprocess_smolagents(obj: dict[str, Any]) -> list[dict[str, Any]]:
|
|
12
|
+
"""Infer step type from field presence (smolagents has no discriminator).
|
|
13
|
+
|
|
14
|
+
Only synthesizes the step_type field and extracts timestamps from timing.
|
|
15
|
+
If step_type is already present, trusts the existing value.
|
|
16
|
+
Nested structures (token_usage, tool_calls) preserved for _resolve_path.
|
|
17
|
+
"""
|
|
18
|
+
normalized = dict(obj)
|
|
19
|
+
|
|
20
|
+
# Extract timestamp from timing dict if present
|
|
21
|
+
timing = normalized.get("timing", {})
|
|
22
|
+
if isinstance(timing, dict) and "start_time" in timing:
|
|
23
|
+
normalized["timestamp"] = timing["start_time"]
|
|
24
|
+
|
|
25
|
+
# If step_type already present (e.g., from callback wrappers), trust it
|
|
26
|
+
if "step_type" in normalized:
|
|
27
|
+
# Still handle tool_calls splitting for ActionStep
|
|
28
|
+
if normalized["step_type"] == "ActionStep":
|
|
29
|
+
tool_calls = normalized.get("tool_calls", [])
|
|
30
|
+
if tool_calls and isinstance(tool_calls, list):
|
|
31
|
+
results: list[dict[str, Any]] = [normalized]
|
|
32
|
+
for tc in tool_calls:
|
|
33
|
+
if isinstance(tc, dict):
|
|
34
|
+
fn = tc.get("function", {})
|
|
35
|
+
results.append(
|
|
36
|
+
{
|
|
37
|
+
"step_type": "ToolCall",
|
|
38
|
+
"timestamp": normalized.get("timestamp"),
|
|
39
|
+
"tool_name": fn.get("name", "") if isinstance(fn, dict) else "",
|
|
40
|
+
"call_id": tc.get("id", ""),
|
|
41
|
+
"tool_input": fn.get("arguments", "")
|
|
42
|
+
if isinstance(fn, dict)
|
|
43
|
+
else "",
|
|
44
|
+
}
|
|
45
|
+
)
|
|
46
|
+
return results
|
|
47
|
+
return [normalized]
|
|
48
|
+
|
|
49
|
+
# Determine step type from field presence
|
|
50
|
+
# Order matters: check most specific first
|
|
51
|
+
if "step_number" in normalized:
|
|
52
|
+
# ActionStep — but check if it's the final answer
|
|
53
|
+
if normalized.get("is_final_answer"):
|
|
54
|
+
# ActionStep with is_final_answer=true: action_output IS the answer
|
|
55
|
+
normalized["step_type"] = "FinalAnswer"
|
|
56
|
+
normalized["output"] = normalized.get("action_output", "")
|
|
57
|
+
else:
|
|
58
|
+
normalized["step_type"] = "ActionStep"
|
|
59
|
+
tool_calls = normalized.get("tool_calls", [])
|
|
60
|
+
if tool_calls and isinstance(tool_calls, list):
|
|
61
|
+
results = [normalized]
|
|
62
|
+
for tc in tool_calls:
|
|
63
|
+
if isinstance(tc, dict):
|
|
64
|
+
fn = tc.get("function", {})
|
|
65
|
+
results.append(
|
|
66
|
+
{
|
|
67
|
+
"step_type": "ToolCall",
|
|
68
|
+
"timestamp": normalized.get("timestamp"),
|
|
69
|
+
"tool_name": fn.get("name", "") if isinstance(fn, dict) else "",
|
|
70
|
+
"call_id": tc.get("id", ""),
|
|
71
|
+
"tool_input": fn.get("arguments", "") if isinstance(fn, dict) else "",
|
|
72
|
+
}
|
|
73
|
+
)
|
|
74
|
+
return results
|
|
75
|
+
elif "plan" in normalized:
|
|
76
|
+
normalized["step_type"] = "PlanningStep"
|
|
77
|
+
elif "system_prompt" in normalized:
|
|
78
|
+
normalized["step_type"] = "SystemPromptStep"
|
|
79
|
+
elif "task" in normalized:
|
|
80
|
+
normalized["step_type"] = "TaskStep"
|
|
81
|
+
elif (
|
|
82
|
+
"output" in normalized
|
|
83
|
+
and len(set(normalized.keys()) - {"output", "timestamp", "step_type"}) == 0
|
|
84
|
+
):
|
|
85
|
+
# Bare FinalAnswerStep: only has "output" (+ maybe timestamp)
|
|
86
|
+
normalized["step_type"] = "FinalAnswer"
|
|
87
|
+
else:
|
|
88
|
+
normalized["step_type"] = "unknown"
|
|
89
|
+
|
|
90
|
+
return [normalized]
|
traceforge/py.typed
ADDED
|
File without changes
|