activegraph 1.0.0rc2__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.
- activegraph/__init__.py +222 -0
- activegraph/__main__.py +5 -0
- activegraph/behaviors/__init__.py +1 -0
- activegraph/behaviors/base.py +153 -0
- activegraph/behaviors/decorators.py +218 -0
- activegraph/cli/__init__.py +17 -0
- activegraph/cli/main.py +793 -0
- activegraph/cli/quickstart.py +556 -0
- activegraph/core/__init__.py +1 -0
- activegraph/core/clock.py +46 -0
- activegraph/core/event.py +33 -0
- activegraph/core/graph.py +846 -0
- activegraph/core/ids.py +135 -0
- activegraph/core/patch.py +47 -0
- activegraph/core/view.py +59 -0
- activegraph/errors.py +342 -0
- activegraph/frame.py +15 -0
- activegraph/llm/__init__.py +51 -0
- activegraph/llm/anthropic.py +339 -0
- activegraph/llm/cache.py +180 -0
- activegraph/llm/errors.py +257 -0
- activegraph/llm/prompt.py +420 -0
- activegraph/llm/provider.py +66 -0
- activegraph/llm/recorded.py +270 -0
- activegraph/llm/types.py +130 -0
- activegraph/observability/__init__.py +61 -0
- activegraph/observability/logging.py +205 -0
- activegraph/observability/metrics.py +219 -0
- activegraph/observability/migration.py +404 -0
- activegraph/observability/prometheus.py +101 -0
- activegraph/observability/status.py +83 -0
- activegraph/packs/__init__.py +999 -0
- activegraph/packs/diligence/__init__.py +86 -0
- activegraph/packs/diligence/behaviors.py +447 -0
- activegraph/packs/diligence/fixtures/__init__.py +364 -0
- activegraph/packs/diligence/fixtures/companies.py +511 -0
- activegraph/packs/diligence/object_types.py +163 -0
- activegraph/packs/diligence/settings.py +60 -0
- activegraph/packs/diligence/tools.py +124 -0
- activegraph/packs/loader.py +709 -0
- activegraph/packs/scaffold.py +317 -0
- activegraph/policy.py +20 -0
- activegraph/runtime/__init__.py +1 -0
- activegraph/runtime/behavior_graph.py +151 -0
- activegraph/runtime/budget.py +120 -0
- activegraph/runtime/config_errors.py +124 -0
- activegraph/runtime/diff.py +145 -0
- activegraph/runtime/errors.py +216 -0
- activegraph/runtime/exec_errors.py +232 -0
- activegraph/runtime/patterns.py +946 -0
- activegraph/runtime/queue.py +27 -0
- activegraph/runtime/registration_errors.py +291 -0
- activegraph/runtime/registry.py +111 -0
- activegraph/runtime/runtime.py +2441 -0
- activegraph/runtime/scheduler.py +206 -0
- activegraph/runtime/view_builder.py +65 -0
- activegraph/store/__init__.py +41 -0
- activegraph/store/base.py +66 -0
- activegraph/store/conformance.py +161 -0
- activegraph/store/errors.py +84 -0
- activegraph/store/memory.py +118 -0
- activegraph/store/postgres.py +609 -0
- activegraph/store/serde.py +202 -0
- activegraph/store/sqlite.py +446 -0
- activegraph/store/url.py +230 -0
- activegraph/tools/__init__.py +64 -0
- activegraph/tools/base.py +57 -0
- activegraph/tools/cache.py +157 -0
- activegraph/tools/context.py +48 -0
- activegraph/tools/decorators.py +70 -0
- activegraph/tools/errors.py +320 -0
- activegraph/tools/graph_query.py +94 -0
- activegraph/tools/recorded.py +205 -0
- activegraph/tools/web_fetch.py +80 -0
- activegraph/trace/__init__.py +1 -0
- activegraph/trace/causal.py +123 -0
- activegraph/trace/printer.py +495 -0
- activegraph-1.0.0rc2.dist-info/METADATA +228 -0
- activegraph-1.0.0rc2.dist-info/RECORD +82 -0
- activegraph-1.0.0rc2.dist-info/WHEEL +5 -0
- activegraph-1.0.0rc2.dist-info/entry_points.txt +5 -0
- activegraph-1.0.0rc2.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""Reference tool: web_fetch. CONTRACT v0.7 #16.
|
|
2
|
+
|
|
3
|
+
A real `web_fetch` implementation using only Python stdlib. No
|
|
4
|
+
third-party dependency. Marked `deterministic=False`.
|
|
5
|
+
|
|
6
|
+
In production, prefer this for simple cases; for anything serious
|
|
7
|
+
(rate limiting, retry policies, conditional GETs) write your own
|
|
8
|
+
@tool that wraps httpx or aiohttp.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from decimal import Decimal
|
|
14
|
+
|
|
15
|
+
from pydantic import BaseModel, Field
|
|
16
|
+
|
|
17
|
+
from activegraph.tools.context import ToolContext
|
|
18
|
+
from activegraph.tools.decorators import tool
|
|
19
|
+
from activegraph.tools.errors import ToolError
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class WebFetchInput(BaseModel):
|
|
23
|
+
url: str = Field(description="The HTTP/HTTPS URL to fetch.")
|
|
24
|
+
timeout_seconds: float = Field(default=10.0, gt=0)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class WebFetchOutput(BaseModel):
|
|
28
|
+
text: str
|
|
29
|
+
status: int
|
|
30
|
+
final_url: str
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@tool(
|
|
34
|
+
name="web_fetch",
|
|
35
|
+
description="Fetch the body text of a URL via HTTP GET. Follows redirects.",
|
|
36
|
+
input_schema=WebFetchInput,
|
|
37
|
+
output_schema=WebFetchOutput,
|
|
38
|
+
cost_per_call=Decimal("0.001"),
|
|
39
|
+
timeout_seconds=10.0,
|
|
40
|
+
deterministic=False,
|
|
41
|
+
)
|
|
42
|
+
def web_fetch(args: WebFetchInput, ctx: ToolContext) -> WebFetchOutput:
|
|
43
|
+
import urllib.error
|
|
44
|
+
import urllib.request
|
|
45
|
+
|
|
46
|
+
req = urllib.request.Request(args.url, headers={"User-Agent": "activegraph/0.7"})
|
|
47
|
+
try:
|
|
48
|
+
with urllib.request.urlopen(req, timeout=args.timeout_seconds) as resp:
|
|
49
|
+
status = resp.getcode() or 0
|
|
50
|
+
final_url = resp.geturl() or args.url
|
|
51
|
+
body = resp.read()
|
|
52
|
+
except urllib.error.HTTPError as e:
|
|
53
|
+
body = e.read() if hasattr(e, "read") else b""
|
|
54
|
+
return WebFetchOutput(
|
|
55
|
+
text=_decode(body), status=e.code, final_url=args.url
|
|
56
|
+
)
|
|
57
|
+
except urllib.error.URLError as e:
|
|
58
|
+
raise ToolError(
|
|
59
|
+
"tool.network_error",
|
|
60
|
+
f"network error fetching {args.url}: {e.reason}",
|
|
61
|
+
payload_extras={"url": args.url},
|
|
62
|
+
) from e
|
|
63
|
+
except TimeoutError as e:
|
|
64
|
+
raise ToolError(
|
|
65
|
+
"tool.timeout",
|
|
66
|
+
f"timeout after {args.timeout_seconds}s fetching {args.url}",
|
|
67
|
+
payload_extras={"url": args.url, "timeout_seconds": args.timeout_seconds},
|
|
68
|
+
) from e
|
|
69
|
+
return WebFetchOutput(text=_decode(body), status=status, final_url=final_url)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _decode(body: bytes) -> str:
|
|
73
|
+
if not body:
|
|
74
|
+
return ""
|
|
75
|
+
for enc in ("utf-8", "latin-1"):
|
|
76
|
+
try:
|
|
77
|
+
return body.decode(enc)
|
|
78
|
+
except UnicodeDecodeError:
|
|
79
|
+
continue
|
|
80
|
+
return body.decode("utf-8", errors="replace")
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Trace printer + causal-chain audit. Format is contract (CONTRACT #18)."""
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"""Causal-chain audit. Walk back from an object through caused_by links
|
|
2
|
+
until we hit a goal.created (or an event with no parent).
|
|
3
|
+
|
|
4
|
+
CONTRACT v0.6 #15: when an object was created inside an @llm_behavior
|
|
5
|
+
handler, its provenance carries `llm_request_event_id`. The chain
|
|
6
|
+
follows that link first — showing the LLM round-trip (llm.requested
|
|
7
|
+
+ llm.responded with model and cost) — before continuing up through
|
|
8
|
+
the triggering event. The auditability story made concrete: a single
|
|
9
|
+
chain walk renders the full lineage from a claim back to the LLM call
|
|
10
|
+
that produced it, to the document it was extracted from, to the goal
|
|
11
|
+
that started the whole run.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from typing import Any
|
|
17
|
+
|
|
18
|
+
from activegraph.core.event import Event
|
|
19
|
+
from activegraph.core.graph import Graph
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def causal_chain(graph: Graph, object_id: str) -> str:
|
|
23
|
+
obj = graph.get_object(object_id)
|
|
24
|
+
if obj is None:
|
|
25
|
+
return f"(no such object: {object_id})"
|
|
26
|
+
|
|
27
|
+
by_id: dict[str, Event] = {e.id: e for e in graph.events}
|
|
28
|
+
# Find the event that created this object.
|
|
29
|
+
created_by_evt: Event | None = None
|
|
30
|
+
for e in graph.events:
|
|
31
|
+
if e.type == "object.created" and e.payload.get("object", {}).get("id") == object_id:
|
|
32
|
+
created_by_evt = e
|
|
33
|
+
break
|
|
34
|
+
|
|
35
|
+
lines: list[str] = []
|
|
36
|
+
label = obj.data.get("title") or obj.data.get("text") or ""
|
|
37
|
+
label_s = f' "{label}"' if label else ""
|
|
38
|
+
lines.append(f"{obj.id} ({obj.type}){label_s}")
|
|
39
|
+
|
|
40
|
+
# If this object was created inside an @llm_behavior handler, the
|
|
41
|
+
# provenance carries the llm.requested event id. Weave the LLM
|
|
42
|
+
# round-trip into the chain at this point before continuing up
|
|
43
|
+
# through the triggering event.
|
|
44
|
+
llm_request_id = obj.provenance.get("llm_request_event_id")
|
|
45
|
+
# CONTRACT v0.7 #19: tool calls that contributed to the final LLM
|
|
46
|
+
# output are also enumerated. Each entry walks tool.requested +
|
|
47
|
+
# tool.responded, same shape as the LLM block.
|
|
48
|
+
tool_request_ids: list[str] = list(
|
|
49
|
+
obj.provenance.get("tool_request_event_ids") or []
|
|
50
|
+
)
|
|
51
|
+
indent = " "
|
|
52
|
+
if llm_request_id:
|
|
53
|
+
llm_req = by_id.get(llm_request_id)
|
|
54
|
+
if llm_req is not None:
|
|
55
|
+
llm_resp = _find_response_for(by_id, llm_request_id, "llm.responded")
|
|
56
|
+
actor = llm_req.actor or "?"
|
|
57
|
+
model = llm_req.payload.get("model", "?")
|
|
58
|
+
lines.append(
|
|
59
|
+
f"{indent}← {actor} ({llm_req.id}) llm.requested model={model}"
|
|
60
|
+
)
|
|
61
|
+
if llm_resp is not None:
|
|
62
|
+
cost = llm_resp.payload.get("cost_usd")
|
|
63
|
+
cached = llm_resp.payload.get("cache_hit")
|
|
64
|
+
tail = " (cache_hit)" if cached else (f" cost=${_fmt_money(cost)}" if cost else "")
|
|
65
|
+
lines.append(
|
|
66
|
+
f"{indent} ({llm_resp.id}) llm.responded{tail}"
|
|
67
|
+
)
|
|
68
|
+
for tr_id in tool_request_ids:
|
|
69
|
+
tr = by_id.get(tr_id)
|
|
70
|
+
if tr is None:
|
|
71
|
+
continue
|
|
72
|
+
tresp = _find_response_for(by_id, tr_id, "tool.responded")
|
|
73
|
+
tool_name = tr.payload.get("tool", "?")
|
|
74
|
+
lines.append(
|
|
75
|
+
f"{indent}← {tr.actor or '?'} ({tr.id}) tool.requested tool={tool_name}"
|
|
76
|
+
)
|
|
77
|
+
if tresp is not None:
|
|
78
|
+
cost = tresp.payload.get("cost_usd")
|
|
79
|
+
cached = tresp.payload.get("cache_hit")
|
|
80
|
+
err = tresp.payload.get("error")
|
|
81
|
+
if err:
|
|
82
|
+
tail = f" error={err.get('reason', 'tool.error') if isinstance(err, dict) else 'tool.error'}"
|
|
83
|
+
elif cached:
|
|
84
|
+
tail = " (cache_hit)"
|
|
85
|
+
elif cost is not None:
|
|
86
|
+
tail = f" cost=${_fmt_money(cost)}"
|
|
87
|
+
else:
|
|
88
|
+
tail = ""
|
|
89
|
+
lines.append(
|
|
90
|
+
f"{indent} ({tresp.id}) tool.responded{tail}"
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
seen: set[str] = set()
|
|
94
|
+
cursor = created_by_evt
|
|
95
|
+
while cursor is not None:
|
|
96
|
+
if cursor.id in seen:
|
|
97
|
+
lines.append(f"{indent}← (cycle at {cursor.id})")
|
|
98
|
+
break
|
|
99
|
+
seen.add(cursor.id)
|
|
100
|
+
actor = cursor.actor or "?"
|
|
101
|
+
lines.append(f"{indent}← {actor} ({cursor.id}) {cursor.type}")
|
|
102
|
+
if cursor.caused_by is None:
|
|
103
|
+
break
|
|
104
|
+
cursor = by_id.get(cursor.caused_by)
|
|
105
|
+
indent += " "
|
|
106
|
+
|
|
107
|
+
return "\n".join(lines)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _find_response_for(
|
|
111
|
+
by_id: dict[str, Event], request_id: str, response_type: str = "llm.responded"
|
|
112
|
+
) -> Event | None:
|
|
113
|
+
for e in by_id.values():
|
|
114
|
+
if e.type == response_type and e.caused_by == request_id:
|
|
115
|
+
return e
|
|
116
|
+
return None
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _fmt_money(v: Any) -> str:
|
|
120
|
+
try:
|
|
121
|
+
return f"{float(v):.3f}"
|
|
122
|
+
except (TypeError, ValueError):
|
|
123
|
+
return str(v)
|