hyperforge 1.0.0.post19__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.
- hyperforge/__init__.py +16 -0
- hyperforge/agent.py +81 -0
- hyperforge/api/__init__.py +20 -0
- hyperforge/api/app.py +155 -0
- hyperforge/api/authentication.py +271 -0
- hyperforge/api/commands.py +33 -0
- hyperforge/api/internal/__init__.py +4 -0
- hyperforge/api/internal/inspect.py +30 -0
- hyperforge/api/internal/router.py +3 -0
- hyperforge/api/logging.py +18 -0
- hyperforge/api/models.py +129 -0
- hyperforge/api/session.py +197 -0
- hyperforge/api/settings.py +38 -0
- hyperforge/api/utils.py +354 -0
- hyperforge/api/v1/__init__.py +23 -0
- hyperforge/api/v1/agents.py +531 -0
- hyperforge/api/v1/interaction.py +430 -0
- hyperforge/api/v1/mcp_content.py +311 -0
- hyperforge/api/v1/mcp_interaction.py +322 -0
- hyperforge/api/v1/oauth.py +60 -0
- hyperforge/api/v1/prompt.py +129 -0
- hyperforge/api/v1/router.py +3 -0
- hyperforge/api/v1/schema.py +56 -0
- hyperforge/api/v1/session.py +182 -0
- hyperforge/api/v1/utils.py +12 -0
- hyperforge/api/v1/workflows.py +643 -0
- hyperforge/arag.py +28 -0
- hyperforge/broker/__init__.py +52 -0
- hyperforge/broker/local.py +116 -0
- hyperforge/broker/redis.py +161 -0
- hyperforge/configure.py +571 -0
- hyperforge/context/__init__.py +0 -0
- hyperforge/context/agent.py +377 -0
- hyperforge/context/config.py +103 -0
- hyperforge/database.py +3 -0
- hyperforge/db/__init__.py +6 -0
- hyperforge/db/agents.py +1521 -0
- hyperforge/db/encryption.py +91 -0
- hyperforge/db/exceptions.py +26 -0
- hyperforge/db/settings.py +16 -0
- hyperforge/db/workflow_cleanup.py +69 -0
- hyperforge/definition.py +13 -0
- hyperforge/driver.py +31 -0
- hyperforge/dummy.py +28 -0
- hyperforge/engine.py +189 -0
- hyperforge/exceptions.py +14 -0
- hyperforge/feature_flag.py +105 -0
- hyperforge/fixtures.py +602 -0
- hyperforge/interaction.py +116 -0
- hyperforge/llm.py +75 -0
- hyperforge/manager.py +432 -0
- hyperforge/memory/__init__.py +5 -0
- hyperforge/memory/memory.py +974 -0
- hyperforge/minimal_fixtures.py +75 -0
- hyperforge/models.py +336 -0
- hyperforge/nua.py +336 -0
- hyperforge/openapi.py +63 -0
- hyperforge/prompts.py +188 -0
- hyperforge/pubsub.py +90 -0
- hyperforge/py.typed +0 -0
- hyperforge/redis_utils.py +82 -0
- hyperforge/retrieval/__init__.py +0 -0
- hyperforge/retrieval/agent.py +169 -0
- hyperforge/retrieval/config.py +94 -0
- hyperforge/server/__init__.py +5 -0
- hyperforge/server/cache.py +131 -0
- hyperforge/server/run.py +109 -0
- hyperforge/server/sandbox.py +60 -0
- hyperforge/server/session.py +421 -0
- hyperforge/server/settings.py +47 -0
- hyperforge/server/utils.py +57 -0
- hyperforge/server/web.py +31 -0
- hyperforge/settings.py +18 -0
- hyperforge/standalone/__init__.py +5 -0
- hyperforge/standalone/agent.py +189 -0
- hyperforge/standalone/app.py +264 -0
- hyperforge/standalone/config.py +137 -0
- hyperforge/standalone/const.py +1 -0
- hyperforge/standalone/run.py +60 -0
- hyperforge/standalone/settings.py +133 -0
- hyperforge/standalone/ui_router.py +241 -0
- hyperforge/trace.py +42 -0
- hyperforge/utils/__init__.py +112 -0
- hyperforge/utils/http.py +48 -0
- hyperforge/workflows.py +44 -0
- hyperforge-1.0.0.post19.dist-info/METADATA +95 -0
- hyperforge-1.0.0.post19.dist-info/RECORD +90 -0
- hyperforge-1.0.0.post19.dist-info/WHEEL +5 -0
- hyperforge-1.0.0.post19.dist-info/entry_points.txt +8 -0
- hyperforge-1.0.0.post19.dist-info/top_level.txt +1 -0
hyperforge/api/utils.py
ADDED
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import inspect
|
|
4
|
+
from functools import wraps
|
|
5
|
+
from typing import Any, Callable, List, TypeVar
|
|
6
|
+
|
|
7
|
+
import pydantic
|
|
8
|
+
from fastapi import HTTPException
|
|
9
|
+
from starlette.requests import Request
|
|
10
|
+
from typing_extensions import TypeGuard
|
|
11
|
+
|
|
12
|
+
from hyperforge.db import exceptions
|
|
13
|
+
from hyperforge.db.agents import AgentManager
|
|
14
|
+
|
|
15
|
+
_T = TypeVar("_T")
|
|
16
|
+
|
|
17
|
+
FLOW_PROPERTIES = [
|
|
18
|
+
"next_agent",
|
|
19
|
+
"fallback",
|
|
20
|
+
"then",
|
|
21
|
+
"else_",
|
|
22
|
+
"agents",
|
|
23
|
+
"registered_agents",
|
|
24
|
+
"agent",
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def to_strict_json_schema(
|
|
29
|
+
model: type[pydantic.BaseModel] | pydantic.TypeAdapter[Any],
|
|
30
|
+
exclude_properties: List[str] = [],
|
|
31
|
+
exclude_defs: List[str] = [],
|
|
32
|
+
) -> dict[str, Any]:
|
|
33
|
+
if inspect.isclass(model) and is_basemodel_type(model):
|
|
34
|
+
schema = model.model_json_schema()
|
|
35
|
+
elif isinstance(model, pydantic.TypeAdapter):
|
|
36
|
+
schema = model.json_schema()
|
|
37
|
+
else:
|
|
38
|
+
raise TypeError(
|
|
39
|
+
f"Non BaseModel types are only supported with Pydantic v2 - {model}"
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
for exclude in exclude_properties:
|
|
43
|
+
if "properties" in schema and exclude in schema["properties"]:
|
|
44
|
+
del schema["properties"][exclude]
|
|
45
|
+
for exclude in exclude_defs:
|
|
46
|
+
if "$defs" in schema and exclude in schema["$defs"]:
|
|
47
|
+
del schema["$defs"][exclude]
|
|
48
|
+
return _ensure_strict_json_schema(schema, path=(), root=schema)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _ensure_strict_json_schema(
|
|
52
|
+
json_schema: object,
|
|
53
|
+
*,
|
|
54
|
+
path: tuple[str, ...],
|
|
55
|
+
root: dict[str, object],
|
|
56
|
+
) -> dict[str, Any]:
|
|
57
|
+
"""Mutates the given JSON schema to ensure it conforms to the `strict` standard
|
|
58
|
+
that the API expects.
|
|
59
|
+
"""
|
|
60
|
+
if not is_dict(json_schema):
|
|
61
|
+
raise TypeError(f"Expected {json_schema} to be a dictionary; path={path}")
|
|
62
|
+
|
|
63
|
+
# defs = json_schema.get("$defs")
|
|
64
|
+
# if is_dict(defs):
|
|
65
|
+
# for def_name, def_schema in defs.items():
|
|
66
|
+
# _ensure_strict_json_schema(
|
|
67
|
+
# def_schema, path=(*path, "$defs", def_name), root=root
|
|
68
|
+
# )
|
|
69
|
+
|
|
70
|
+
definitions = json_schema.get("definitions")
|
|
71
|
+
if is_dict(definitions):
|
|
72
|
+
for definition_name, definition_schema in definitions.items():
|
|
73
|
+
_ensure_strict_json_schema(
|
|
74
|
+
definition_schema,
|
|
75
|
+
path=(*path, "definitions", definition_name),
|
|
76
|
+
root=root,
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
typ = json_schema.get("type")
|
|
80
|
+
if typ == "object" and "additionalProperties" not in json_schema:
|
|
81
|
+
json_schema["additionalProperties"] = False
|
|
82
|
+
|
|
83
|
+
# object types
|
|
84
|
+
# { 'type': 'object', 'properties': { 'a': {...} } }
|
|
85
|
+
properties = json_schema.get("properties")
|
|
86
|
+
if is_dict(properties):
|
|
87
|
+
json_schema["required"] = [prop for prop in properties.keys()]
|
|
88
|
+
json_schema["properties"] = {
|
|
89
|
+
key: _ensure_strict_json_schema(
|
|
90
|
+
prop_schema, path=(*path, "properties", key), root=root
|
|
91
|
+
)
|
|
92
|
+
for key, prop_schema in properties.items()
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
# arrays
|
|
96
|
+
# { 'type': 'array', 'items': {...} }
|
|
97
|
+
items = json_schema.get("items")
|
|
98
|
+
if is_dict(items):
|
|
99
|
+
json_schema["items"] = _ensure_strict_json_schema(
|
|
100
|
+
items, path=(*path, "items"), root=root
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
# unions
|
|
104
|
+
one_of = json_schema.get("oneOf")
|
|
105
|
+
if isinstance(one_of, list):
|
|
106
|
+
json_schema["oneOf"] = [
|
|
107
|
+
_ensure_strict_json_schema(
|
|
108
|
+
variant, path=(*path, "oneOf", str(i)), root=root
|
|
109
|
+
)
|
|
110
|
+
for i, variant in enumerate(one_of)
|
|
111
|
+
]
|
|
112
|
+
|
|
113
|
+
# unions
|
|
114
|
+
any_of = json_schema.get("anyOf")
|
|
115
|
+
if isinstance(any_of, list):
|
|
116
|
+
json_schema["anyOf"] = [
|
|
117
|
+
_ensure_strict_json_schema(
|
|
118
|
+
variant, path=(*path, "anyOf", str(i)), root=root
|
|
119
|
+
)
|
|
120
|
+
for i, variant in enumerate(any_of)
|
|
121
|
+
]
|
|
122
|
+
|
|
123
|
+
# intersections
|
|
124
|
+
all_of = json_schema.get("allOf")
|
|
125
|
+
if isinstance(all_of, list):
|
|
126
|
+
if len(all_of) == 1:
|
|
127
|
+
json_schema.update(
|
|
128
|
+
_ensure_strict_json_schema(
|
|
129
|
+
all_of[0], path=(*path, "allOf", "0"), root=root
|
|
130
|
+
)
|
|
131
|
+
)
|
|
132
|
+
json_schema.pop("allOf")
|
|
133
|
+
else:
|
|
134
|
+
json_schema["allOf"] = [
|
|
135
|
+
_ensure_strict_json_schema(
|
|
136
|
+
entry, path=(*path, "allOf", str(i)), root=root
|
|
137
|
+
)
|
|
138
|
+
for i, entry in enumerate(all_of)
|
|
139
|
+
]
|
|
140
|
+
|
|
141
|
+
# strip `None` defaults as there's no meaningful distinction here
|
|
142
|
+
# the schema will still be `nullable` and the model will default
|
|
143
|
+
# to using `None` anyway
|
|
144
|
+
if json_schema.get("default", "") is None:
|
|
145
|
+
json_schema.pop("default")
|
|
146
|
+
|
|
147
|
+
# we can't use `$ref`s if there are also other properties defined, e.g.
|
|
148
|
+
# `{"$ref": "...", "description": "my description"}`
|
|
149
|
+
#
|
|
150
|
+
# so we unravel the ref
|
|
151
|
+
# `{"type": "string", "description": "my description"}`
|
|
152
|
+
ref = json_schema.get("$ref")
|
|
153
|
+
if ref is not None:
|
|
154
|
+
assert isinstance(ref, str), f"Received non-string $ref - {ref}"
|
|
155
|
+
|
|
156
|
+
resolved = resolve_ref(root=root, ref=ref)
|
|
157
|
+
if not is_dict(resolved):
|
|
158
|
+
raise ValueError(
|
|
159
|
+
f"Expected `$ref: {ref}` to resolved to a dictionary but got {resolved}"
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
# properties from the json schema take priority over the ones on the `$ref`
|
|
163
|
+
json_schema.update({**resolved, **json_schema})
|
|
164
|
+
json_schema.pop("$ref")
|
|
165
|
+
# Since the schema expanded from `$ref` might not have `additionalProperties: false` applied,
|
|
166
|
+
# we call `_ensure_strict_json_schema` again to fix the inlined schema and ensure it's valid.
|
|
167
|
+
return _ensure_strict_json_schema(json_schema, path=path, root=root)
|
|
168
|
+
|
|
169
|
+
return json_schema
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def resolve_ref(*, root: dict[str, object], ref: str) -> object:
|
|
173
|
+
if not ref.startswith("#/"):
|
|
174
|
+
raise ValueError(f"Unexpected $ref format {ref!r}; Does not start with #/")
|
|
175
|
+
|
|
176
|
+
path = ref[2:].split("/")
|
|
177
|
+
resolved = root
|
|
178
|
+
for key in path:
|
|
179
|
+
value = resolved[key]
|
|
180
|
+
assert is_dict(value), (
|
|
181
|
+
f"encountered non-dictionary entry while resolving {ref} - {resolved}"
|
|
182
|
+
)
|
|
183
|
+
resolved = value
|
|
184
|
+
|
|
185
|
+
return resolved
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def is_basemodel_type(typ: type) -> TypeGuard[type[pydantic.BaseModel]]:
|
|
189
|
+
if not inspect.isclass(typ):
|
|
190
|
+
return False
|
|
191
|
+
return issubclass(typ, pydantic.BaseModel)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def is_dataclass_like_type(typ: type) -> bool:
|
|
195
|
+
"""Returns True if the given type likely used `@pydantic.dataclass`"""
|
|
196
|
+
return hasattr(typ, "__pydantic_config__")
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def is_dict(obj: object) -> TypeGuard[dict[str, object]]:
|
|
200
|
+
# just pretend that we know there are only `str` keys
|
|
201
|
+
# as that check is not worth the performance cost
|
|
202
|
+
return isinstance(obj, dict)
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
async def agent_has_nucliadb_memory(
|
|
206
|
+
agent_manager: AgentManager,
|
|
207
|
+
account: str,
|
|
208
|
+
agent_id: str,
|
|
209
|
+
workflow_id: str = "default",
|
|
210
|
+
) -> bool:
|
|
211
|
+
"""Check if an agent has NucliaDB memory configured.
|
|
212
|
+
|
|
213
|
+
Args:
|
|
214
|
+
agent_manager: The agent manager instance
|
|
215
|
+
account: The account ID
|
|
216
|
+
agent_id: The agent ID
|
|
217
|
+
workflow_id: The workflow ID (default: "default")
|
|
218
|
+
Returns:
|
|
219
|
+
True if the agent has NucliaDB memory configured, False otherwise
|
|
220
|
+
"""
|
|
221
|
+
# XXX: add a placeholder internal_nucliadb_url to populate legacy null memory values
|
|
222
|
+
# we don't care about the actual URL here
|
|
223
|
+
agent = await agent_manager.get_agent_config(
|
|
224
|
+
account=account,
|
|
225
|
+
agent_id=agent_id,
|
|
226
|
+
internal_nucliadb_url="fake",
|
|
227
|
+
workflow_id=workflow_id,
|
|
228
|
+
)
|
|
229
|
+
return agent.memory.nucliadb is not None
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
async def clean_up_items(items: dict[str, Any], filtered: list[str]) -> dict[str, Any]:
|
|
233
|
+
"""Cleans up the items section of the schema by removing filtered agents and drivers from the references."""
|
|
234
|
+
if "discriminator" in items and "mapping" in items["discriminator"]:
|
|
235
|
+
for name, module in list(items["discriminator"]["mapping"].items()):
|
|
236
|
+
if filtered and module.split("/")[-1] in filtered:
|
|
237
|
+
del items["discriminator"]["mapping"][name]
|
|
238
|
+
if "oneOf" in items:
|
|
239
|
+
for item in list(items["oneOf"]):
|
|
240
|
+
if "$ref" in item:
|
|
241
|
+
module_name = item["$ref"].split("/")[-1]
|
|
242
|
+
if module_name in filtered:
|
|
243
|
+
items["oneOf"].remove(item)
|
|
244
|
+
return items
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
async def cleanup_anyof(anyof: list[dict[str, Any]], filtered: list[str]):
|
|
248
|
+
"""Cleans up the anyof section of the schema by removing filtered agents and drivers from the references."""
|
|
249
|
+
mapping = {}
|
|
250
|
+
if "discriminator" in anyof[0]:
|
|
251
|
+
anyof_mapping = anyof[0]["discriminator"].get("mapping", {})
|
|
252
|
+
for name, module in anyof_mapping.items():
|
|
253
|
+
module_name = module.split("/")[-1]
|
|
254
|
+
if all(module_name not in fa for fa in filtered):
|
|
255
|
+
mapping[name] = module
|
|
256
|
+
anyof[0]["discriminator"]["mapping"] = mapping
|
|
257
|
+
if "oneOf" in anyof[0]:
|
|
258
|
+
for module in anyof[0]["oneOf"]:
|
|
259
|
+
if "$ref" in module:
|
|
260
|
+
module_name = module["$ref"].split("/")[-1]
|
|
261
|
+
if module_name in filtered:
|
|
262
|
+
anyof[0]["oneOf"].remove(module)
|
|
263
|
+
return anyof
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
async def cleanup_properties(
|
|
267
|
+
properties: dict[str, Any], filtered_agents: list[str], filtered_drivers: list[str]
|
|
268
|
+
) -> dict[str, Any]:
|
|
269
|
+
"""Cleans up the properties section of the schema by removing filtered agents and drivers from the references."""
|
|
270
|
+
filtered = filtered_agents + filtered_drivers
|
|
271
|
+
steps = ["preprocess", "context", "generation", "postprocess", "drivers"]
|
|
272
|
+
for step in steps:
|
|
273
|
+
if step not in properties:
|
|
274
|
+
continue
|
|
275
|
+
items = properties[step].get("items")
|
|
276
|
+
if items:
|
|
277
|
+
properties[step]["items"] = await clean_up_items(items, filtered)
|
|
278
|
+
return properties
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
async def cleanup_definitions(
|
|
282
|
+
definitions: dict[str, Any], filtered_agents: list[str]
|
|
283
|
+
) -> dict[str, Any]:
|
|
284
|
+
"""Cleans up the definitions section of the schema by removing filtered agents from the definitions."""
|
|
285
|
+
for item, item_schema in definitions.items():
|
|
286
|
+
if item.endswith("AgentConfig"):
|
|
287
|
+
if item in filtered_agents:
|
|
288
|
+
del definitions[item]
|
|
289
|
+
continue
|
|
290
|
+
else:
|
|
291
|
+
properties = item_schema.get("properties", {})
|
|
292
|
+
for property in FLOW_PROPERTIES:
|
|
293
|
+
if property in item_schema["properties"]:
|
|
294
|
+
if "anyOf" in properties[property]:
|
|
295
|
+
anyof = properties[property]["anyOf"]
|
|
296
|
+
if "items" in anyof[0]:
|
|
297
|
+
items = anyof[0]["items"]
|
|
298
|
+
properties[property]["anyOf"][0][
|
|
299
|
+
"items"
|
|
300
|
+
] = await clean_up_items(items, filtered_agents)
|
|
301
|
+
else:
|
|
302
|
+
properties[property]["anyOf"] = await cleanup_anyof(
|
|
303
|
+
anyof, filtered_agents
|
|
304
|
+
)
|
|
305
|
+
elif "items" in properties[property]:
|
|
306
|
+
items = properties[property]["items"]
|
|
307
|
+
properties[property]["items"] = await clean_up_items(
|
|
308
|
+
items, filtered_agents
|
|
309
|
+
)
|
|
310
|
+
elif "discriminator" in properties[property]:
|
|
311
|
+
properties[property] = await clean_up_items(
|
|
312
|
+
properties[property], filtered_agents
|
|
313
|
+
)
|
|
314
|
+
item_schema["properties"] = properties
|
|
315
|
+
definitions[item] = item_schema
|
|
316
|
+
|
|
317
|
+
return definitions
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def requires_nucliadb_memory(func: Callable) -> Callable:
|
|
321
|
+
"""Decorator to ensure the agent has NucliaDB memory configured."""
|
|
322
|
+
|
|
323
|
+
@wraps(func)
|
|
324
|
+
async def wrapper(*args, **kwargs):
|
|
325
|
+
# Extract required parameters from kwargs
|
|
326
|
+
request: Request = kwargs.get("request")
|
|
327
|
+
agent_id: str = kwargs.get("agent_id")
|
|
328
|
+
workflow_id: str = kwargs.get("workflow_id", "default")
|
|
329
|
+
x_stf_account: str = kwargs.get("x_stf_account")
|
|
330
|
+
|
|
331
|
+
if not request or not agent_id or not x_stf_account:
|
|
332
|
+
raise HTTPException(
|
|
333
|
+
status_code=500,
|
|
334
|
+
detail="Missing required parameters for memory check",
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
agent_manager: AgentManager = request.app.agent_manager
|
|
338
|
+
try:
|
|
339
|
+
if not await agent_has_nucliadb_memory(
|
|
340
|
+
agent_manager, x_stf_account, agent_id, workflow_id
|
|
341
|
+
):
|
|
342
|
+
raise HTTPException(
|
|
343
|
+
status_code=400,
|
|
344
|
+
detail="Sessions are currently only supported for Retrieval Agents Orchestrators with a Memory Knowledge Box associated",
|
|
345
|
+
)
|
|
346
|
+
except exceptions.NotFoundError:
|
|
347
|
+
raise HTTPException(
|
|
348
|
+
status_code=404,
|
|
349
|
+
detail=f"Agent '{agent_id}' or Workflow '{workflow_id}' not found for account '{x_stf_account}'",
|
|
350
|
+
)
|
|
351
|
+
|
|
352
|
+
return await func(*args, **kwargs)
|
|
353
|
+
|
|
354
|
+
return wrapper
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from . import (
|
|
2
|
+
agents,
|
|
3
|
+
interaction,
|
|
4
|
+
mcp_interaction,
|
|
5
|
+
oauth,
|
|
6
|
+
prompt,
|
|
7
|
+
schema,
|
|
8
|
+
session,
|
|
9
|
+
workflows,
|
|
10
|
+
)
|
|
11
|
+
from .router import router
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"agents",
|
|
15
|
+
"interaction",
|
|
16
|
+
"mcp_interaction",
|
|
17
|
+
"oauth",
|
|
18
|
+
"prompt",
|
|
19
|
+
"workflows",
|
|
20
|
+
"router",
|
|
21
|
+
"session",
|
|
22
|
+
"schema",
|
|
23
|
+
]
|