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.
Files changed (82) hide show
  1. activegraph/__init__.py +222 -0
  2. activegraph/__main__.py +5 -0
  3. activegraph/behaviors/__init__.py +1 -0
  4. activegraph/behaviors/base.py +153 -0
  5. activegraph/behaviors/decorators.py +218 -0
  6. activegraph/cli/__init__.py +17 -0
  7. activegraph/cli/main.py +793 -0
  8. activegraph/cli/quickstart.py +556 -0
  9. activegraph/core/__init__.py +1 -0
  10. activegraph/core/clock.py +46 -0
  11. activegraph/core/event.py +33 -0
  12. activegraph/core/graph.py +846 -0
  13. activegraph/core/ids.py +135 -0
  14. activegraph/core/patch.py +47 -0
  15. activegraph/core/view.py +59 -0
  16. activegraph/errors.py +342 -0
  17. activegraph/frame.py +15 -0
  18. activegraph/llm/__init__.py +51 -0
  19. activegraph/llm/anthropic.py +339 -0
  20. activegraph/llm/cache.py +180 -0
  21. activegraph/llm/errors.py +257 -0
  22. activegraph/llm/prompt.py +420 -0
  23. activegraph/llm/provider.py +66 -0
  24. activegraph/llm/recorded.py +270 -0
  25. activegraph/llm/types.py +130 -0
  26. activegraph/observability/__init__.py +61 -0
  27. activegraph/observability/logging.py +205 -0
  28. activegraph/observability/metrics.py +219 -0
  29. activegraph/observability/migration.py +404 -0
  30. activegraph/observability/prometheus.py +101 -0
  31. activegraph/observability/status.py +83 -0
  32. activegraph/packs/__init__.py +999 -0
  33. activegraph/packs/diligence/__init__.py +86 -0
  34. activegraph/packs/diligence/behaviors.py +447 -0
  35. activegraph/packs/diligence/fixtures/__init__.py +364 -0
  36. activegraph/packs/diligence/fixtures/companies.py +511 -0
  37. activegraph/packs/diligence/object_types.py +163 -0
  38. activegraph/packs/diligence/settings.py +60 -0
  39. activegraph/packs/diligence/tools.py +124 -0
  40. activegraph/packs/loader.py +709 -0
  41. activegraph/packs/scaffold.py +317 -0
  42. activegraph/policy.py +20 -0
  43. activegraph/runtime/__init__.py +1 -0
  44. activegraph/runtime/behavior_graph.py +151 -0
  45. activegraph/runtime/budget.py +120 -0
  46. activegraph/runtime/config_errors.py +124 -0
  47. activegraph/runtime/diff.py +145 -0
  48. activegraph/runtime/errors.py +216 -0
  49. activegraph/runtime/exec_errors.py +232 -0
  50. activegraph/runtime/patterns.py +946 -0
  51. activegraph/runtime/queue.py +27 -0
  52. activegraph/runtime/registration_errors.py +291 -0
  53. activegraph/runtime/registry.py +111 -0
  54. activegraph/runtime/runtime.py +2441 -0
  55. activegraph/runtime/scheduler.py +206 -0
  56. activegraph/runtime/view_builder.py +65 -0
  57. activegraph/store/__init__.py +41 -0
  58. activegraph/store/base.py +66 -0
  59. activegraph/store/conformance.py +161 -0
  60. activegraph/store/errors.py +84 -0
  61. activegraph/store/memory.py +118 -0
  62. activegraph/store/postgres.py +609 -0
  63. activegraph/store/serde.py +202 -0
  64. activegraph/store/sqlite.py +446 -0
  65. activegraph/store/url.py +230 -0
  66. activegraph/tools/__init__.py +64 -0
  67. activegraph/tools/base.py +57 -0
  68. activegraph/tools/cache.py +157 -0
  69. activegraph/tools/context.py +48 -0
  70. activegraph/tools/decorators.py +70 -0
  71. activegraph/tools/errors.py +320 -0
  72. activegraph/tools/graph_query.py +94 -0
  73. activegraph/tools/recorded.py +205 -0
  74. activegraph/tools/web_fetch.py +80 -0
  75. activegraph/trace/__init__.py +1 -0
  76. activegraph/trace/causal.py +123 -0
  77. activegraph/trace/printer.py +495 -0
  78. activegraph-1.0.0rc2.dist-info/METADATA +228 -0
  79. activegraph-1.0.0rc2.dist-info/RECORD +82 -0
  80. activegraph-1.0.0rc2.dist-info/WHEEL +5 -0
  81. activegraph-1.0.0rc2.dist-info/entry_points.txt +5 -0
  82. 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)