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,75 @@
1
+ """Lightweight pytest fixtures for hyperforge agents.
2
+
3
+ This module only depends on pytest and the stdlib — no docker, database, or
4
+ nucliadb deps. Safe to use as a pytest plugin in any agent repo:
5
+
6
+ # tests/conftest.py
7
+ pytest_plugins = ["hyperforge.minimal_fixtures"]
8
+ """
9
+
10
+ import base64
11
+ import json
12
+ import logging
13
+
14
+ import pytest
15
+
16
+
17
+ def cassette_nua_key(iss: str) -> str:
18
+ """Return a minimal parseable JWT stub for cassette-replay runs.
19
+
20
+ validate_nua() decodes the middle part of the JWT to extract the ``iss``
21
+ field before making any HTTP call. When cassettes are present VCR
22
+ intercepts that HTTP call, so the key doesn't need to be real — it just
23
+ needs to parse.
24
+ """
25
+ payload = base64.b64encode(json.dumps({"iss": iss}).encode()).decode().rstrip("=")
26
+ return f"cassette.{payload}.stub"
27
+
28
+
29
+ class _VCRTaskExceptionFilter(logging.Filter):
30
+ """Suppress 'Task exception was never retrieved' asyncio errors from vcrpy.
31
+
32
+ vcrpy's httpx stub creates a background task (_record_responses) that can
33
+ fail with an AssertionError due to a vcrpy/httpx version incompatibility.
34
+ The exception is noisy but harmless in test runs.
35
+ """
36
+
37
+ def filter(self, record: logging.LogRecord) -> bool:
38
+ return not (
39
+ record.levelno == logging.ERROR
40
+ and "Task exception was never retrieved" in record.getMessage()
41
+ and "_record_responses" in record.getMessage()
42
+ )
43
+
44
+
45
+ @pytest.fixture(scope="module")
46
+ def vcr_config():
47
+ return {
48
+ # Replaces the actual token with 'DUMMY' in the recorded YAML
49
+ "filter_headers": [
50
+ ("Authorization", "DUMMY"),
51
+ ("x-nuclia-nuakey", "DUMMY"),
52
+ ("x-stf-nuakey", "DUMMY"),
53
+ ("x-goog-api-key", "DUMMY"),
54
+ ],
55
+ # Redacts specific query parameters like API keys
56
+ "filter_query_parameters": ["api_key", "access_token", "key"],
57
+ # Redacts fields in POST request bodies (e.g., login forms)
58
+ "filter_post_data_parameters": ["password", "client_secret"],
59
+ # Decodes compressed responses so they are human-readable in the cassette
60
+ "decode_compressed_response": True,
61
+ }
62
+
63
+
64
+ @pytest.fixture(autouse=True, scope="session")
65
+ def suppress_test_noise() -> None:
66
+ """Suppress known-noisy log lines that add no diagnostic value in tests."""
67
+ logging.getLogger("hyperforge.memory").setLevel(logging.WARNING)
68
+ logging.getLogger("mcp.server.streamable_http").setLevel(logging.WARNING)
69
+ logging.getLogger("hyperforge.server").setLevel(logging.WARNING)
70
+ logging.getLogger("httpx").setLevel(logging.ERROR)
71
+ logging.getLogger("httpcore.connection").setLevel(logging.ERROR)
72
+ logging.getLogger("httpcore.http11").setLevel(logging.ERROR)
73
+ logging.getLogger("asyncio").setLevel(logging.INFO)
74
+
75
+ logging.getLogger("asyncio").addFilter(_VCRTaskExceptionFilter())
hyperforge/models.py ADDED
@@ -0,0 +1,336 @@
1
+ from __future__ import annotations
2
+
3
+ import uuid
4
+ from typing import (
5
+ Any,
6
+ Dict,
7
+ List,
8
+ Literal,
9
+ Optional,
10
+ Union,
11
+ )
12
+
13
+ from nuclia.lib.nua_responses import Image, StoredLearningConfiguration
14
+ from nucliadb_models.resource import (
15
+ ConversationFieldData,
16
+ FileFieldData,
17
+ GenericFieldData,
18
+ LinkFieldData,
19
+ TextFieldData,
20
+ )
21
+ from nucliadb_models.search import CatalogFacetsResponse
22
+ from pydantic import BaseModel, Field
23
+
24
+ from hyperforge import PROMPT_ENVIRONMENT, logger
25
+
26
+
27
+ class Metadata:
28
+ pass
29
+
30
+
31
+ class KnowledgeGraph:
32
+ pass
33
+
34
+
35
+ class Reason:
36
+ pass
37
+
38
+
39
+ class NucliaDBMemoryConfig(BaseModel):
40
+ key: Optional[str] = None
41
+ url: str
42
+ kbid: str
43
+ internal: bool = True
44
+
45
+
46
+ class MemoryConfig(BaseModel):
47
+ nucliadb: Optional[NucliaDBMemoryConfig] = None
48
+
49
+
50
+ class Rule(BaseModel):
51
+ prompt: Optional[str] = None
52
+
53
+
54
+ class Rules(BaseModel):
55
+ rules: List[Union[Rule, str]] = Field(
56
+ default_factory=list,
57
+ description="List of rules that the workflow should follow. Each rule can be a string or a Rule object with a prompt.",
58
+ )
59
+
60
+
61
+ class Facets(BaseModel):
62
+ chunks: Dict[str, int]
63
+ fields: Dict[str, int]
64
+
65
+
66
+ class Source(BaseModel):
67
+ id: str
68
+ description: str
69
+ labels: Dict[str, List[str]]
70
+ facets_native: CatalogFacetsResponse
71
+ paragraph_facets: Dict[str, int]
72
+ learning_configuration: StoredLearningConfiguration
73
+
74
+
75
+ class CitationMetadata(BaseModel):
76
+ context_id: str = Field(
77
+ description="ID of the context this citation refers to",
78
+ )
79
+ origin_urls: list[str] = Field(
80
+ default_factory=list,
81
+ description="List of origin URLs that this citation refers to",
82
+ )
83
+ chunk_index: Optional[int] = Field(
84
+ default=None,
85
+ description="Index of the chunk in the context's chunks list. This is only set for chunk-level citations.",
86
+ )
87
+
88
+
89
+ class AnswerCitations(BaseModel):
90
+ metadata: dict[str, CitationMetadata] = Field(
91
+ default_factory=dict,
92
+ description="Map of citation_id to citation metadata. block-AA",
93
+ )
94
+
95
+
96
+ class VegaLiteVisualization(BaseModel):
97
+ type: Literal["vega_lite"] = "vega_lite"
98
+ vega_lite_obj: Dict[str, Any] = Field(
99
+ default_factory=dict,
100
+ description="The Vega-Lite Object defining the visualization. Previously validated against the Vega-Lite schema.",
101
+ )
102
+
103
+ # If we do server-side rendering in the future, we can add fields like:
104
+ # svg: Optional[str] = ...
105
+
106
+
107
+ # For once we add more visualization types, we can use a Discriminator
108
+ # Visualization = Annotated[Union[VegaLiteVisualization,NewType], Discriminator("type")]
109
+ # For now, we only have one type.
110
+ Visualization = Union[VegaLiteVisualization]
111
+
112
+
113
+ class Step(BaseModel):
114
+ original_question_uuid: Optional[str]
115
+ actual_question_uuid: Optional[str]
116
+ module: str
117
+ title: str
118
+ value: Optional[str] = None
119
+ agent_path: str
120
+ reason: Optional[str] = None
121
+ timeit: float
122
+ input_nuclia_tokens: Optional[float]
123
+ output_nuclia_tokens: Optional[float]
124
+ error: Optional[str] = None
125
+
126
+ def __str__(self):
127
+ return f"({self.timeit:.2f}s) {self.module}: {self.title} \n {self.value} \n {self.reason} \n NT:({self.input_nuclia_tokens}:{self.output_nuclia_tokens})"
128
+
129
+ def markdown(self):
130
+ return f"""
131
+ ## {self.title}
132
+
133
+ {self.value}
134
+
135
+ - reason: {self.reason}
136
+ - timeit: {self.timeit}
137
+ - input_tokens: {self.input_nuclia_tokens}
138
+ - output_tokens: {self.output_nuclia_tokens}
139
+ """
140
+
141
+
142
+ class ChunkImages(BaseModel):
143
+ table: Optional[str]
144
+ chunk: Optional[str]
145
+ page: Optional[str]
146
+
147
+
148
+ FieldTypes = Union[
149
+ TextFieldData,
150
+ ConversationFieldData,
151
+ FileFieldData,
152
+ LinkFieldData,
153
+ GenericFieldData,
154
+ ]
155
+
156
+
157
+ class Chunk(BaseModel):
158
+ chunk_id: str
159
+ title: Optional[str] = None
160
+ source: Optional[str] = None
161
+ text: str
162
+ labels: List[str] = Field(default_factory=list)
163
+ url: List[str] = Field(default_factory=list)
164
+ metadata: Optional[Dict[str, Any]] = None
165
+ action: Optional[str] = Field(
166
+ default=None,
167
+ description="agent and function called to get this chunk.",
168
+ )
169
+ origin_url: Optional[str] = Field(
170
+ default=None,
171
+ description="URL at the origin of the resource from which this chunk was extracted.",
172
+ )
173
+ origin_agent: Optional[str] = Field(
174
+ default=None,
175
+ description="Agent that originated this chunk. This is useful to keep track of the provenance of the information ",
176
+ )
177
+
178
+ def render(
179
+ self,
180
+ citations_id: Optional[str] = None,
181
+ ) -> str:
182
+ if citations_id:
183
+ lines = [f"## Chunk: [{citations_id}] {self.title or self.chunk_id}"]
184
+ else:
185
+ lines = [f"## Chunk: {self.title or self.chunk_id}"]
186
+ if self.action:
187
+ lines.append(f"Result of running: {self.action}")
188
+ if self.labels:
189
+ lines.append(f"Tags: {', '.join(self.labels)}")
190
+ if self.url:
191
+ lines.append(f"URLs: {', '.join(self.url)}")
192
+ lines.append(f"``` {self.text} ```\n")
193
+ return "\n".join(lines)
194
+
195
+
196
+ class Prompt(BaseModel):
197
+ prompt: str
198
+ resources: List[str] = Field(default_factory=list)
199
+ links: List[str] = Field(default_factory=list)
200
+ description: Optional[str] = None
201
+
202
+ def render(self) -> str:
203
+ lines = ["## Prompt"]
204
+ if self.description:
205
+ lines.append(f"Description: {self.description}")
206
+ if self.resources:
207
+ lines.append(f"Resources: {', '.join(self.resources)}")
208
+ if self.links:
209
+ lines.append(f"Links: {', '.join(self.links)}")
210
+ lines.append(f"```PROMPT\n{self.prompt}\n```\n")
211
+ return "\n".join(lines)
212
+
213
+
214
+ class Answer(BaseModel):
215
+ answer: str
216
+ original_question_uuid: Optional[str]
217
+ actual_question_uuid: Optional[str]
218
+ module: str
219
+ agent_path: str
220
+ data_visualizations: Optional[list[Visualization]] = None
221
+ citations: Optional[AnswerCitations] = None
222
+ chunks: Optional[list[Chunk]] = None
223
+ structured: Optional[list[str]] = None
224
+ images: Optional[Dict[str, Image]] = None
225
+ image_urls: Optional[list[str]] = None
226
+
227
+
228
+ CONTEXT_TEMPLATE = """
229
+
230
+ {% if con.citations_id is not none -%}
231
+ {% for chunk in con.chunks %}
232
+ {{chunk.render(citations_id=con.citations_id ~ "-" ~ loop.index0)}}
233
+ {% endfor -%}
234
+ {% else -%}
235
+ {% for chunk in con.chunks %}
236
+ {{chunk.render()}}
237
+ {% endfor -%}
238
+ {% endif -%}
239
+
240
+ {% if con.structured | length > 0 -%}
241
+ ## Extra structured info:
242
+ {% for structured in con.structured %}
243
+ {{structured}}
244
+ {% endfor -%}
245
+ {% endif -%}
246
+ """
247
+
248
+ CONTEXT_PROMPT_TEMPLATE = PROMPT_ENVIRONMENT.from_string(CONTEXT_TEMPLATE)
249
+
250
+
251
+ class Context(BaseModel):
252
+ id: str = Field(
253
+ default_factory=lambda: uuid.uuid4().hex,
254
+ description="Unique identifier for this context instance",
255
+ )
256
+ original_question_uuid: Optional[str]
257
+ actual_question_uuid: Optional[str]
258
+ question: str
259
+ chunks: List[Chunk] = Field(default_factory=list)
260
+ images: Dict[str, Image] = Field(default_factory=dict)
261
+ prompts: List[Prompt] = Field(default_factory=list)
262
+ structured: List[str] = Field(default_factory=list)
263
+ source: str
264
+ agent: str
265
+ # XXX: This is not actually a summary, but an answer attempt for now!
266
+ summary: str = Field(
267
+ default="",
268
+ description="Partial or full answer to the question, generated by the context validation step inside a context agent.",
269
+ )
270
+ agent_id: str = ""
271
+ title: Optional[str] = None
272
+ missing: Optional[str] = None
273
+ citations: list[str] | None = Field(
274
+ default=None,
275
+ description="List of chunk IDs that were considered relevant in the context validation step.",
276
+ )
277
+ citations_id: Optional[str] = Field(
278
+ default=None,
279
+ description="Block ID used for citations in this context.",
280
+ )
281
+ image_urls: List[str] = Field(
282
+ default_factory=list,
283
+ description="List of image URLs associated with this context.",
284
+ )
285
+
286
+ def answer_summary_markdown(self) -> str:
287
+ return "# {question}\n\n {summary}".format(
288
+ question=self.question, summary=self.summary
289
+ )
290
+
291
+ def context_markdown(self) -> str:
292
+ return CONTEXT_PROMPT_TEMPLATE.render(con=self)
293
+
294
+ def stats(self) -> Dict[str, int | str | None]:
295
+ return {
296
+ "chunks": len(self.chunks),
297
+ "images": len(self.images),
298
+ "structured": len(self.structured),
299
+ "source": self.source,
300
+ "question": self.question,
301
+ "agent": self.agent,
302
+ "summary": self.summary,
303
+ "title": self.title,
304
+ "missing": self.missing,
305
+ }
306
+
307
+ def prune_to_citations(self) -> None:
308
+ if self.citations is None:
309
+ logger.warning(
310
+ "Cannot prune context as no citations are available.",
311
+ extra={
312
+ "agent": self.agent,
313
+ "source": self.source,
314
+ "agent_id": self.agent_id,
315
+ },
316
+ )
317
+ return
318
+ self.chunks = [
319
+ chunk for chunk in self.chunks if chunk.chunk_id in self.citations
320
+ ]
321
+ self.structured = [
322
+ s
323
+ for i, s in enumerate(self.structured)
324
+ if f"structured-{i}" in self.citations
325
+ ]
326
+
327
+
328
+ class HistoryQuestionAnswer(BaseModel):
329
+ question: str
330
+ answer: str
331
+
332
+
333
+ class TrackingInfo(BaseModel):
334
+ rao_id: str
335
+ session: str
336
+ message: str