aethergraph 0.1.0a2__py3-none-any.whl → 0.1.0a4__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 (114) hide show
  1. aethergraph/__main__.py +3 -0
  2. aethergraph/api/v1/artifacts.py +23 -4
  3. aethergraph/api/v1/schemas.py +7 -0
  4. aethergraph/api/v1/session.py +123 -4
  5. aethergraph/config/config.py +2 -0
  6. aethergraph/config/search.py +49 -0
  7. aethergraph/contracts/services/channel.py +18 -1
  8. aethergraph/contracts/services/execution.py +58 -0
  9. aethergraph/contracts/services/llm.py +26 -0
  10. aethergraph/contracts/services/memory.py +10 -4
  11. aethergraph/contracts/services/planning.py +53 -0
  12. aethergraph/contracts/storage/event_log.py +8 -0
  13. aethergraph/contracts/storage/search_backend.py +47 -0
  14. aethergraph/contracts/storage/vector_index.py +73 -0
  15. aethergraph/core/graph/action_spec.py +76 -0
  16. aethergraph/core/graph/graph_fn.py +75 -2
  17. aethergraph/core/graph/graphify.py +74 -2
  18. aethergraph/core/runtime/graph_runner.py +2 -1
  19. aethergraph/core/runtime/node_context.py +66 -3
  20. aethergraph/core/runtime/node_services.py +8 -0
  21. aethergraph/core/runtime/run_manager.py +263 -271
  22. aethergraph/core/runtime/run_types.py +54 -1
  23. aethergraph/core/runtime/runtime_env.py +35 -14
  24. aethergraph/core/runtime/runtime_services.py +308 -18
  25. aethergraph/plugins/agents/default_chat_agent.py +266 -74
  26. aethergraph/plugins/agents/default_chat_agent_v2.py +487 -0
  27. aethergraph/plugins/channel/adapters/webui.py +69 -21
  28. aethergraph/plugins/channel/routes/webui_routes.py +8 -48
  29. aethergraph/runtime/__init__.py +12 -0
  30. aethergraph/server/app_factory.py +10 -1
  31. aethergraph/server/ui_static/assets/index-CFktGdbW.js +4913 -0
  32. aethergraph/server/ui_static/assets/index-DcfkFlTA.css +1 -0
  33. aethergraph/server/ui_static/index.html +2 -2
  34. aethergraph/services/artifacts/facade.py +157 -21
  35. aethergraph/services/artifacts/types.py +35 -0
  36. aethergraph/services/artifacts/utils.py +42 -0
  37. aethergraph/services/channel/channel_bus.py +3 -1
  38. aethergraph/services/channel/event_hub copy.py +55 -0
  39. aethergraph/services/channel/event_hub.py +81 -0
  40. aethergraph/services/channel/factory.py +3 -2
  41. aethergraph/services/channel/session.py +709 -74
  42. aethergraph/services/container/default_container.py +69 -7
  43. aethergraph/services/execution/__init__.py +0 -0
  44. aethergraph/services/execution/local_python.py +118 -0
  45. aethergraph/services/indices/__init__.py +0 -0
  46. aethergraph/services/indices/global_indices.py +21 -0
  47. aethergraph/services/indices/scoped_indices.py +292 -0
  48. aethergraph/services/llm/generic_client.py +342 -46
  49. aethergraph/services/llm/generic_embed_client.py +359 -0
  50. aethergraph/services/llm/types.py +3 -1
  51. aethergraph/services/memory/distillers/llm_long_term.py +60 -109
  52. aethergraph/services/memory/distillers/llm_long_term_v1.py +180 -0
  53. aethergraph/services/memory/distillers/llm_meta_summary.py +57 -266
  54. aethergraph/services/memory/distillers/llm_meta_summary_v1.py +342 -0
  55. aethergraph/services/memory/distillers/long_term.py +48 -131
  56. aethergraph/services/memory/distillers/long_term_v1.py +170 -0
  57. aethergraph/services/memory/facade/chat.py +18 -8
  58. aethergraph/services/memory/facade/core.py +159 -19
  59. aethergraph/services/memory/facade/distillation.py +86 -31
  60. aethergraph/services/memory/facade/retrieval.py +100 -1
  61. aethergraph/services/memory/factory.py +4 -1
  62. aethergraph/services/planning/__init__.py +0 -0
  63. aethergraph/services/planning/action_catalog.py +271 -0
  64. aethergraph/services/planning/bindings.py +56 -0
  65. aethergraph/services/planning/dependency_index.py +65 -0
  66. aethergraph/services/planning/flow_validator.py +263 -0
  67. aethergraph/services/planning/graph_io_adapter.py +150 -0
  68. aethergraph/services/planning/input_parser.py +312 -0
  69. aethergraph/services/planning/missing_inputs.py +28 -0
  70. aethergraph/services/planning/node_planner.py +613 -0
  71. aethergraph/services/planning/orchestrator.py +112 -0
  72. aethergraph/services/planning/plan_executor.py +506 -0
  73. aethergraph/services/planning/plan_types.py +321 -0
  74. aethergraph/services/planning/planner.py +617 -0
  75. aethergraph/services/planning/planner_service.py +369 -0
  76. aethergraph/services/planning/planning_context_builder.py +43 -0
  77. aethergraph/services/planning/quick_actions.py +29 -0
  78. aethergraph/services/planning/routers/__init__.py +0 -0
  79. aethergraph/services/planning/routers/simple_router.py +26 -0
  80. aethergraph/services/rag/facade.py +0 -3
  81. aethergraph/services/scope/scope.py +30 -30
  82. aethergraph/services/scope/scope_factory.py +15 -7
  83. aethergraph/services/skills/__init__.py +0 -0
  84. aethergraph/services/skills/skill_registry.py +465 -0
  85. aethergraph/services/skills/skills.py +220 -0
  86. aethergraph/services/skills/utils.py +194 -0
  87. aethergraph/storage/artifacts/artifact_index_jsonl.py +16 -10
  88. aethergraph/storage/artifacts/artifact_index_sqlite.py +12 -2
  89. aethergraph/storage/docstore/sqlite_doc_sync.py +1 -1
  90. aethergraph/storage/memory/event_persist.py +42 -2
  91. aethergraph/storage/memory/fs_persist.py +32 -2
  92. aethergraph/storage/search_backend/__init__.py +0 -0
  93. aethergraph/storage/search_backend/generic_vector_backend.py +230 -0
  94. aethergraph/storage/search_backend/null_backend.py +34 -0
  95. aethergraph/storage/search_backend/sqlite_lexical_backend.py +387 -0
  96. aethergraph/storage/search_backend/utils.py +31 -0
  97. aethergraph/storage/search_factory.py +75 -0
  98. aethergraph/storage/vector_index/faiss_index.py +72 -4
  99. aethergraph/storage/vector_index/sqlite_index.py +521 -52
  100. aethergraph/storage/vector_index/sqlite_index_vanila.py +311 -0
  101. aethergraph/storage/vector_index/utils.py +22 -0
  102. {aethergraph-0.1.0a2.dist-info → aethergraph-0.1.0a4.dist-info}/METADATA +1 -1
  103. {aethergraph-0.1.0a2.dist-info → aethergraph-0.1.0a4.dist-info}/RECORD +108 -64
  104. {aethergraph-0.1.0a2.dist-info → aethergraph-0.1.0a4.dist-info}/WHEEL +1 -1
  105. aethergraph/plugins/agents/default_chat_agent copy.py +0 -90
  106. aethergraph/server/ui_static/assets/index-BR5GtXcZ.css +0 -1
  107. aethergraph/server/ui_static/assets/index-CQ0HZZ83.js +0 -400
  108. aethergraph/services/eventhub/event_hub.py +0 -76
  109. aethergraph/services/llm/generic_client copy.py +0 -691
  110. aethergraph/services/prompts/file_store.py +0 -41
  111. {aethergraph-0.1.0a2.dist-info → aethergraph-0.1.0a4.dist-info}/entry_points.txt +0 -0
  112. {aethergraph-0.1.0a2.dist-info → aethergraph-0.1.0a4.dist-info}/licenses/LICENSE +0 -0
  113. {aethergraph-0.1.0a2.dist-info → aethergraph-0.1.0a4.dist-info}/licenses/NOTICE +0 -0
  114. {aethergraph-0.1.0a2.dist-info → aethergraph-0.1.0a4.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,312 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ import json
5
+ from typing import Any
6
+
7
+ from aethergraph.contracts.services.llm import LLMClientProtocol
8
+
9
+
10
+ @dataclass
11
+ class ParsedInputs:
12
+ """
13
+ Result of attempting to parse user-provided values for some fields.
14
+
15
+ - values: successfully parsed values, keyed by field name.
16
+ - resolved_keys: subset of field names for which we got a non-null value.
17
+ - missing_keys: subset of field names we still lack.
18
+ - errors: human-readable errors that the agent/orchestrator can surface.
19
+ """
20
+
21
+ values: dict[str, Any]
22
+ resolved_keys: set[str]
23
+ missing_keys: set[str]
24
+ errors: list[str]
25
+
26
+
27
+ class InputParserError(Exception):
28
+ """Hard failure in the input parser (e.g. LLM/schema problem)."""
29
+
30
+
31
+ def normalize_llm_json_object(raw: Any) -> dict[str, Any]:
32
+ """
33
+ Normalize raw LLM output into a JSON object (dict).
34
+
35
+ Handles:
36
+ - dict (already parsed)
37
+ - JSON strings, optionally wrapped in ``` or ```json fences
38
+ - JSON strings with leading/trailing text, by extracting the first {...} block
39
+ """
40
+ # 1) Already a dict: perfect
41
+ if isinstance(raw, dict):
42
+ return raw
43
+
44
+ # 2) String: try to recover JSON from it
45
+ if isinstance(raw, str):
46
+ txt = raw.strip()
47
+ if not txt:
48
+ raise InputParserError("Empty LLM response when expecting JSON object.")
49
+
50
+ # Handle ```json ... ``` or ``` ... ``` fences
51
+ if txt.startswith("```"):
52
+ # Strip first fence line (``` or ```json / ```JSON etc.)
53
+ first_newline = txt.find("\n")
54
+ if first_newline != -1:
55
+ # fence_line = txt[:first_newline].strip().lower()
56
+ # We don't really care which flavor; drop it
57
+ txt = txt[first_newline + 1 :].strip()
58
+ # Strip trailing ``` if present
59
+ if txt.endswith("```"):
60
+ txt = txt[:-3].strip()
61
+
62
+ # First attempt: parse whole string
63
+ try:
64
+ obj = json.loads(txt)
65
+ except json.JSONDecodeError:
66
+ # Second attempt: extract the first {...} block
67
+ start = txt.find("{")
68
+ end = txt.rfind("}")
69
+ if start != -1 and end != -1 and end > start:
70
+ candidate = txt[start : end + 1]
71
+ try:
72
+ obj = json.loads(candidate)
73
+ except json.JSONDecodeError as exc2:
74
+ raise InputParserError(
75
+ f"Cannot parse JSON object from LLM response (substring). Error: {exc2}"
76
+ ) from exc2
77
+ else:
78
+ raise InputParserError(
79
+ "Cannot parse JSON object from LLM response (no JSON object found)."
80
+ ) from None
81
+
82
+ if not isinstance(obj, dict):
83
+ raise InputParserError(f"Expected JSON object (dict), got {type(obj)} instead.")
84
+ return obj
85
+
86
+ # 3) Unsupported type
87
+ raise InputParserError(
88
+ f"LLM returned unsupported type {type(raw)} when expecting a JSON object."
89
+ )
90
+
91
+
92
+ @dataclass
93
+ class InputParser:
94
+ """
95
+ Generic, LLM-backed input parser.
96
+
97
+ Given:
98
+ - a user message
99
+ - a set of expected field names
100
+ - an optional free-form `instruction` string
101
+
102
+ it asks the LLM to extract values and returns a ParsedInputs object.
103
+
104
+ This parser is intentionally generic across agents/verticals. The only
105
+ domain-specific hints it uses are:
106
+ - the field names themselves
107
+ - the optional `instruction` text
108
+ """
109
+
110
+ llm: LLMClientProtocol
111
+
112
+ async def parse_message_for_fields(
113
+ self,
114
+ *,
115
+ message: str,
116
+ missing_keys: list[str],
117
+ instruction: str | None = None,
118
+ ) -> ParsedInputs:
119
+ """
120
+ Ask the LLM to extract values for the given missing_keys.
121
+
122
+ Args:
123
+ message: The user's natural-language reply.
124
+ missing_keys: Field names whose values we want to extract.
125
+ instruction: Optional free-form instruction describing what these
126
+ fields mean or how they should be interpreted.
127
+
128
+ Returns:
129
+ ParsedInputs with values/resolved_keys/missing_keys/errors.
130
+
131
+ Notes:
132
+ - If the LLM cannot confidently determine a field, it should set it
133
+ to null. We then treat that as "missing".
134
+ - If the LLM call fails or returns invalid JSON, we return an object
135
+ with all keys in missing_keys and a populated `errors` list.
136
+ """
137
+ # Build per-field descriptions (generic, based on key names only)
138
+ field_descriptions = self._build_field_descriptions(missing_keys)
139
+
140
+ # Build JSON schema for extraction
141
+ schema = self._build_extraction_schema(missing_keys)
142
+
143
+ system_prompt = (
144
+ "You are an input extraction assistant. "
145
+ "Your task is to read a user message and extract values for a fixed "
146
+ "set of fields. You must return ONLY a JSON object that conforms "
147
+ "to the provided JSON schema.\n\n"
148
+ "If a field is not clearly specified in the user's message, or you "
149
+ "are not confident about its value, you MUST set that field to null. "
150
+ "Do not try to guess values that are not present."
151
+ )
152
+
153
+ instr_header = ""
154
+ if instruction:
155
+ instr_header = f"Additional instructions for this task:\n{instruction}\n\n"
156
+
157
+ fields_description_str = "\n".join(
158
+ f"- {name}: {desc or '(no description)'}" for name, desc in field_descriptions.items()
159
+ )
160
+
161
+ user_prompt = (
162
+ f"{instr_header}"
163
+ "User message:\n"
164
+ f"{message}\n\n"
165
+ "You must extract values for the following fields (if present):\n"
166
+ f"{fields_description_str}\n\n"
167
+ "Return ONLY the JSON object, no explanations."
168
+ )
169
+
170
+ messages = [
171
+ {"role": "system", "content": system_prompt},
172
+ {"role": "user", "content": user_prompt},
173
+ ]
174
+
175
+ try:
176
+ raw, _usage = await self.llm.chat(
177
+ messages,
178
+ output_format="json",
179
+ json_schema=schema,
180
+ schema_name="ParsedInputs",
181
+ strict_schema=True,
182
+ validate_json=True,
183
+ )
184
+ except Exception as exc: # noqa: BLE001
185
+ # Hard LLM failure → all fields still missing; surface error to user.
186
+ return ParsedInputs(
187
+ values={},
188
+ resolved_keys=set(),
189
+ missing_keys=set(missing_keys),
190
+ errors=[
191
+ "I couldn't reliably extract the requested inputs from your reply. "
192
+ "Please restate the values clearly, for example:\n"
193
+ + "\n".join(f"- {k} = <value>" for k in missing_keys),
194
+ f"(Internal parser error: {exc!r})",
195
+ ],
196
+ )
197
+
198
+ # --- Normalize raw into a Python dict ---
199
+ try:
200
+ obj = normalize_llm_json_object(raw)
201
+ except InputParserError as exc:
202
+ return ParsedInputs(
203
+ values={},
204
+ resolved_keys=set(),
205
+ missing_keys=set(missing_keys),
206
+ errors=[
207
+ "I couldn't reliably extract the requested inputs from your reply. "
208
+ "Please restate the values clearly.",
209
+ f"(Parser error: {exc!r})",
210
+ ],
211
+ )
212
+
213
+ # From here on, `obj` is a dict
214
+ values: dict[str, Any] = {}
215
+ resolved: set[str] = set()
216
+ missing: set[str] = set()
217
+
218
+ for key in missing_keys:
219
+ val = obj.get(key, None)
220
+ if val is None:
221
+ missing.add(key)
222
+ else:
223
+ values[key] = val
224
+ resolved.add(key)
225
+
226
+ errors: list[str] = []
227
+ if missing:
228
+ errors.append(
229
+ "I still don't have values for the following fields: "
230
+ + ", ".join(sorted(missing))
231
+ + ". Please specify them explicitly, for example:\n"
232
+ + "\n".join(f"- {k} = <value>" for k in sorted(missing))
233
+ )
234
+
235
+ return ParsedInputs(
236
+ values=values,
237
+ resolved_keys=resolved,
238
+ missing_keys=missing,
239
+ errors=errors,
240
+ )
241
+
242
+ # ------------------------------------------------------------------
243
+ # Internal helpers
244
+ # ------------------------------------------------------------------
245
+
246
+ @staticmethod
247
+ def _build_field_descriptions(
248
+ missing_keys: list[str],
249
+ ) -> dict[str, str | None]:
250
+ """
251
+ Build a mapping { field_name: description_or_None }.
252
+
253
+ Since skills and structured input metadata are deprecated, we
254
+ currently just return `(no description)` placeholders. The
255
+ optional `instruction` string in parse_message_for_fields()
256
+ provides the main domain context.
257
+ """
258
+ return {k: None for k in missing_keys}
259
+
260
+ @staticmethod
261
+ def _build_extraction_schema(
262
+ missing_keys: list[str],
263
+ ) -> dict[str, Any]:
264
+ """
265
+ Build a permissive JSON schema for the extraction object.
266
+
267
+ Each field is allowed to be any JSON type or null. We rely on the
268
+ LLM + external validation to make sense of the values.
269
+
270
+ Schema:
271
+
272
+ {
273
+ "type": "object",
274
+ "properties": {
275
+ "<field>": {
276
+ "anyOf": [
277
+ {"type": "string"},
278
+ {"type": "number"},
279
+ {"type": "integer"},
280
+ {"type": "boolean"},
281
+ {"type": "object"},
282
+ {"type": "array"},
283
+ {"type": "null"}
284
+ ]
285
+ },
286
+ ...
287
+ },
288
+ "required": [],
289
+ "additionalProperties": false
290
+ }
291
+ """
292
+ field_schema: dict[str, Any] = {
293
+ "anyOf": [
294
+ {"type": "string"},
295
+ {"type": "number"},
296
+ {"type": "integer"},
297
+ {"type": "boolean"},
298
+ {"type": "object"},
299
+ {"type": "array"},
300
+ {"type": "null"},
301
+ ]
302
+ }
303
+
304
+ props: dict[str, Any] = {k: field_schema for k in missing_keys}
305
+
306
+ return {
307
+ "type": "object",
308
+ "properties": props,
309
+ # We do NOT require any fields; LLM sets missing ones to null.
310
+ "required": [],
311
+ "additionalProperties": False,
312
+ }
@@ -0,0 +1,28 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+
5
+ from aethergraph.services.planning.plan_types import ValidationResult
6
+
7
+
8
+ @dataclass
9
+ class MissingUserInput:
10
+ """
11
+ Represents one missing external user binding like ${user.dataset_path}.
12
+
13
+ key: the user.<key> part, e.g. "dataset_path"
14
+ locations: where in the plan this key is referenced, e.g. ["load.dataset_path"]
15
+ """
16
+
17
+ key: str
18
+ locations: list[str]
19
+
20
+
21
+ def get_missing_user_inputs(result: ValidationResult) -> list[MissingUserInput]:
22
+ """
23
+ Convert ValidationResult.missing_user_bindings into a nicer list structure.
24
+ """
25
+ items: list[MissingUserInput] = []
26
+ for key, locs in (result.missing_user_bindings or {}).items():
27
+ items.append(MissingUserInput(key=key, locations=list(locs)))
28
+ return items