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.
- aethergraph/__main__.py +3 -0
- aethergraph/api/v1/artifacts.py +23 -4
- aethergraph/api/v1/schemas.py +7 -0
- aethergraph/api/v1/session.py +123 -4
- aethergraph/config/config.py +2 -0
- aethergraph/config/search.py +49 -0
- aethergraph/contracts/services/channel.py +18 -1
- aethergraph/contracts/services/execution.py +58 -0
- aethergraph/contracts/services/llm.py +26 -0
- aethergraph/contracts/services/memory.py +10 -4
- aethergraph/contracts/services/planning.py +53 -0
- aethergraph/contracts/storage/event_log.py +8 -0
- aethergraph/contracts/storage/search_backend.py +47 -0
- aethergraph/contracts/storage/vector_index.py +73 -0
- aethergraph/core/graph/action_spec.py +76 -0
- aethergraph/core/graph/graph_fn.py +75 -2
- aethergraph/core/graph/graphify.py +74 -2
- aethergraph/core/runtime/graph_runner.py +2 -1
- aethergraph/core/runtime/node_context.py +66 -3
- aethergraph/core/runtime/node_services.py +8 -0
- aethergraph/core/runtime/run_manager.py +263 -271
- aethergraph/core/runtime/run_types.py +54 -1
- aethergraph/core/runtime/runtime_env.py +35 -14
- aethergraph/core/runtime/runtime_services.py +308 -18
- aethergraph/plugins/agents/default_chat_agent.py +266 -74
- aethergraph/plugins/agents/default_chat_agent_v2.py +487 -0
- aethergraph/plugins/channel/adapters/webui.py +69 -21
- aethergraph/plugins/channel/routes/webui_routes.py +8 -48
- aethergraph/runtime/__init__.py +12 -0
- aethergraph/server/app_factory.py +10 -1
- aethergraph/server/ui_static/assets/index-CFktGdbW.js +4913 -0
- aethergraph/server/ui_static/assets/index-DcfkFlTA.css +1 -0
- aethergraph/server/ui_static/index.html +2 -2
- aethergraph/services/artifacts/facade.py +157 -21
- aethergraph/services/artifacts/types.py +35 -0
- aethergraph/services/artifacts/utils.py +42 -0
- aethergraph/services/channel/channel_bus.py +3 -1
- aethergraph/services/channel/event_hub copy.py +55 -0
- aethergraph/services/channel/event_hub.py +81 -0
- aethergraph/services/channel/factory.py +3 -2
- aethergraph/services/channel/session.py +709 -74
- aethergraph/services/container/default_container.py +69 -7
- aethergraph/services/execution/__init__.py +0 -0
- aethergraph/services/execution/local_python.py +118 -0
- aethergraph/services/indices/__init__.py +0 -0
- aethergraph/services/indices/global_indices.py +21 -0
- aethergraph/services/indices/scoped_indices.py +292 -0
- aethergraph/services/llm/generic_client.py +342 -46
- aethergraph/services/llm/generic_embed_client.py +359 -0
- aethergraph/services/llm/types.py +3 -1
- aethergraph/services/memory/distillers/llm_long_term.py +60 -109
- aethergraph/services/memory/distillers/llm_long_term_v1.py +180 -0
- aethergraph/services/memory/distillers/llm_meta_summary.py +57 -266
- aethergraph/services/memory/distillers/llm_meta_summary_v1.py +342 -0
- aethergraph/services/memory/distillers/long_term.py +48 -131
- aethergraph/services/memory/distillers/long_term_v1.py +170 -0
- aethergraph/services/memory/facade/chat.py +18 -8
- aethergraph/services/memory/facade/core.py +159 -19
- aethergraph/services/memory/facade/distillation.py +86 -31
- aethergraph/services/memory/facade/retrieval.py +100 -1
- aethergraph/services/memory/factory.py +4 -1
- aethergraph/services/planning/__init__.py +0 -0
- aethergraph/services/planning/action_catalog.py +271 -0
- aethergraph/services/planning/bindings.py +56 -0
- aethergraph/services/planning/dependency_index.py +65 -0
- aethergraph/services/planning/flow_validator.py +263 -0
- aethergraph/services/planning/graph_io_adapter.py +150 -0
- aethergraph/services/planning/input_parser.py +312 -0
- aethergraph/services/planning/missing_inputs.py +28 -0
- aethergraph/services/planning/node_planner.py +613 -0
- aethergraph/services/planning/orchestrator.py +112 -0
- aethergraph/services/planning/plan_executor.py +506 -0
- aethergraph/services/planning/plan_types.py +321 -0
- aethergraph/services/planning/planner.py +617 -0
- aethergraph/services/planning/planner_service.py +369 -0
- aethergraph/services/planning/planning_context_builder.py +43 -0
- aethergraph/services/planning/quick_actions.py +29 -0
- aethergraph/services/planning/routers/__init__.py +0 -0
- aethergraph/services/planning/routers/simple_router.py +26 -0
- aethergraph/services/rag/facade.py +0 -3
- aethergraph/services/scope/scope.py +30 -30
- aethergraph/services/scope/scope_factory.py +15 -7
- aethergraph/services/skills/__init__.py +0 -0
- aethergraph/services/skills/skill_registry.py +465 -0
- aethergraph/services/skills/skills.py +220 -0
- aethergraph/services/skills/utils.py +194 -0
- aethergraph/storage/artifacts/artifact_index_jsonl.py +16 -10
- aethergraph/storage/artifacts/artifact_index_sqlite.py +12 -2
- aethergraph/storage/docstore/sqlite_doc_sync.py +1 -1
- aethergraph/storage/memory/event_persist.py +42 -2
- aethergraph/storage/memory/fs_persist.py +32 -2
- aethergraph/storage/search_backend/__init__.py +0 -0
- aethergraph/storage/search_backend/generic_vector_backend.py +230 -0
- aethergraph/storage/search_backend/null_backend.py +34 -0
- aethergraph/storage/search_backend/sqlite_lexical_backend.py +387 -0
- aethergraph/storage/search_backend/utils.py +31 -0
- aethergraph/storage/search_factory.py +75 -0
- aethergraph/storage/vector_index/faiss_index.py +72 -4
- aethergraph/storage/vector_index/sqlite_index.py +521 -52
- aethergraph/storage/vector_index/sqlite_index_vanila.py +311 -0
- aethergraph/storage/vector_index/utils.py +22 -0
- {aethergraph-0.1.0a2.dist-info → aethergraph-0.1.0a4.dist-info}/METADATA +1 -1
- {aethergraph-0.1.0a2.dist-info → aethergraph-0.1.0a4.dist-info}/RECORD +108 -64
- {aethergraph-0.1.0a2.dist-info → aethergraph-0.1.0a4.dist-info}/WHEEL +1 -1
- aethergraph/plugins/agents/default_chat_agent copy.py +0 -90
- aethergraph/server/ui_static/assets/index-BR5GtXcZ.css +0 -1
- aethergraph/server/ui_static/assets/index-CQ0HZZ83.js +0 -400
- aethergraph/services/eventhub/event_hub.py +0 -76
- aethergraph/services/llm/generic_client copy.py +0 -691
- aethergraph/services/prompts/file_store.py +0 -41
- {aethergraph-0.1.0a2.dist-info → aethergraph-0.1.0a4.dist-info}/entry_points.txt +0 -0
- {aethergraph-0.1.0a2.dist-info → aethergraph-0.1.0a4.dist-info}/licenses/LICENSE +0 -0
- {aethergraph-0.1.0a2.dist-info → aethergraph-0.1.0a4.dist-info}/licenses/NOTICE +0 -0
- {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
|