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.
Files changed (90) hide show
  1. hyperforge/__init__.py +16 -0
  2. hyperforge/agent.py +81 -0
  3. hyperforge/api/__init__.py +20 -0
  4. hyperforge/api/app.py +155 -0
  5. hyperforge/api/authentication.py +271 -0
  6. hyperforge/api/commands.py +33 -0
  7. hyperforge/api/internal/__init__.py +4 -0
  8. hyperforge/api/internal/inspect.py +30 -0
  9. hyperforge/api/internal/router.py +3 -0
  10. hyperforge/api/logging.py +18 -0
  11. hyperforge/api/models.py +129 -0
  12. hyperforge/api/session.py +197 -0
  13. hyperforge/api/settings.py +38 -0
  14. hyperforge/api/utils.py +354 -0
  15. hyperforge/api/v1/__init__.py +23 -0
  16. hyperforge/api/v1/agents.py +531 -0
  17. hyperforge/api/v1/interaction.py +430 -0
  18. hyperforge/api/v1/mcp_content.py +311 -0
  19. hyperforge/api/v1/mcp_interaction.py +322 -0
  20. hyperforge/api/v1/oauth.py +60 -0
  21. hyperforge/api/v1/prompt.py +129 -0
  22. hyperforge/api/v1/router.py +3 -0
  23. hyperforge/api/v1/schema.py +56 -0
  24. hyperforge/api/v1/session.py +182 -0
  25. hyperforge/api/v1/utils.py +12 -0
  26. hyperforge/api/v1/workflows.py +643 -0
  27. hyperforge/arag.py +28 -0
  28. hyperforge/broker/__init__.py +52 -0
  29. hyperforge/broker/local.py +116 -0
  30. hyperforge/broker/redis.py +161 -0
  31. hyperforge/configure.py +571 -0
  32. hyperforge/context/__init__.py +0 -0
  33. hyperforge/context/agent.py +377 -0
  34. hyperforge/context/config.py +103 -0
  35. hyperforge/database.py +3 -0
  36. hyperforge/db/__init__.py +6 -0
  37. hyperforge/db/agents.py +1521 -0
  38. hyperforge/db/encryption.py +91 -0
  39. hyperforge/db/exceptions.py +26 -0
  40. hyperforge/db/settings.py +16 -0
  41. hyperforge/db/workflow_cleanup.py +69 -0
  42. hyperforge/definition.py +13 -0
  43. hyperforge/driver.py +31 -0
  44. hyperforge/dummy.py +28 -0
  45. hyperforge/engine.py +189 -0
  46. hyperforge/exceptions.py +14 -0
  47. hyperforge/feature_flag.py +105 -0
  48. hyperforge/fixtures.py +602 -0
  49. hyperforge/interaction.py +116 -0
  50. hyperforge/llm.py +75 -0
  51. hyperforge/manager.py +432 -0
  52. hyperforge/memory/__init__.py +5 -0
  53. hyperforge/memory/memory.py +974 -0
  54. hyperforge/minimal_fixtures.py +75 -0
  55. hyperforge/models.py +336 -0
  56. hyperforge/nua.py +336 -0
  57. hyperforge/openapi.py +63 -0
  58. hyperforge/prompts.py +188 -0
  59. hyperforge/pubsub.py +90 -0
  60. hyperforge/py.typed +0 -0
  61. hyperforge/redis_utils.py +82 -0
  62. hyperforge/retrieval/__init__.py +0 -0
  63. hyperforge/retrieval/agent.py +169 -0
  64. hyperforge/retrieval/config.py +94 -0
  65. hyperforge/server/__init__.py +5 -0
  66. hyperforge/server/cache.py +131 -0
  67. hyperforge/server/run.py +109 -0
  68. hyperforge/server/sandbox.py +60 -0
  69. hyperforge/server/session.py +421 -0
  70. hyperforge/server/settings.py +47 -0
  71. hyperforge/server/utils.py +57 -0
  72. hyperforge/server/web.py +31 -0
  73. hyperforge/settings.py +18 -0
  74. hyperforge/standalone/__init__.py +5 -0
  75. hyperforge/standalone/agent.py +189 -0
  76. hyperforge/standalone/app.py +264 -0
  77. hyperforge/standalone/config.py +137 -0
  78. hyperforge/standalone/const.py +1 -0
  79. hyperforge/standalone/run.py +60 -0
  80. hyperforge/standalone/settings.py +133 -0
  81. hyperforge/standalone/ui_router.py +241 -0
  82. hyperforge/trace.py +42 -0
  83. hyperforge/utils/__init__.py +112 -0
  84. hyperforge/utils/http.py +48 -0
  85. hyperforge/workflows.py +44 -0
  86. hyperforge-1.0.0.post19.dist-info/METADATA +95 -0
  87. hyperforge-1.0.0.post19.dist-info/RECORD +90 -0
  88. hyperforge-1.0.0.post19.dist-info/WHEEL +5 -0
  89. hyperforge-1.0.0.post19.dist-info/entry_points.txt +8 -0
  90. hyperforge-1.0.0.post19.dist-info/top_level.txt +1 -0
@@ -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
+ ]