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,377 @@
1
+ import string
2
+ from functools import lru_cache
3
+ from time import time
4
+ from typing import Any, ClassVar, Dict, List, Literal, Optional, Tuple, cast
5
+ from uuid import uuid4
6
+
7
+ from hyperforge import logger
8
+ from hyperforge.configure import get_agent_klass
9
+ from hyperforge.context.config import ContextAgentConfig
10
+ from hyperforge.definition import FunctionDefinition
11
+ from hyperforge.manager import Manager
12
+ from hyperforge.memory import Context, QuestionMemory
13
+ from hyperforge.prompts import (
14
+ NEXT_REPHRASE_JSON_SCHEMA,
15
+ NEXT_REPHRASE_PROMPT_SYSTEM,
16
+ NEXT_REPHRASE_PROMPT_TEMPLATE,
17
+ VALIDATE_JSON_SCHEMA,
18
+ VALIDATE_OR_ANSWER_PROMPT_TEMPLATE,
19
+ )
20
+ from hyperforge.trace import trace_agent
21
+
22
+
23
+ @lru_cache(maxsize=64)
24
+ def generate_ctx_block_id(n: int) -> str:
25
+ """Generates block ids in the form block-AA, block-AB, ..., block-AZ, block-BA, ..., block-ZZ"""
26
+ # XXX: We use letters to identify block since uuids confuse the LLMs and with numbers it confuses the block numbers with footnote numbers
27
+ if n < 0:
28
+ raise ValueError("Number must be non-negative")
29
+ if n >= 26 * 26:
30
+ logger.warning(
31
+ "Block ID exceeds maximum limit for citations,", extra={"block_id": n}
32
+ )
33
+ n = n % (26 * 26)
34
+ letters = string.ascii_uppercase
35
+ first = letters[(n // 26)]
36
+ second = letters[n % 26]
37
+ return "block-" + first + second
38
+
39
+
40
+ async def build_context_agent(config: ContextAgentConfig) -> "ContextAgent":
41
+ agent_class = get_agent_klass(config.module)
42
+ assert issubclass(agent_class, ContextAgent), (
43
+ f"Agent {config.module} is not a ContextAgent"
44
+ )
45
+ return await agent_class.from_config(config)
46
+
47
+
48
+ class ContextAgent:
49
+ fallback: Optional["ContextAgent"] = None
50
+ next_agent: Optional["ContextAgent"] = None
51
+ agent_description: str = "Agent that provides context to answer questions."
52
+ __published_functions__: ClassVar[Dict[str, FunctionDefinition]] = {}
53
+ exposed_functions: Optional[List[str]] = None
54
+ agent_id: str
55
+
56
+ async def preload(self, manager: "Manager", memory: "QuestionMemory") -> None:
57
+ """Lifecycle hook called by SmartAgent before tool discovery.
58
+
59
+ Override in subclasses that need a live connection (e.g. MCP) to
60
+ populate ``__published_functions__`` at runtime. The default
61
+ implementation is a no-op.
62
+ """
63
+ pass
64
+
65
+ @property
66
+ def context_config(self) -> ContextAgentConfig:
67
+ return cast(ContextAgentConfig, self.config) # type: ignore
68
+
69
+ async def inner_from_config(self, config: Any, agent_id: Optional[str] = None):
70
+ await self.context_from_config(config)
71
+
72
+ async def context_from_config(self, config: ContextAgentConfig):
73
+ # Shared logic for fallback and next_agent
74
+ if config.fallback:
75
+ self.fallback = await build_context_agent(config.fallback)
76
+ if config.next_agent:
77
+ self.next_agent = await build_context_agent(config.next_agent)
78
+
79
+ async def validate_ctx_and_answer(
80
+ self,
81
+ memory: QuestionMemory,
82
+ manager: Manager,
83
+ context: Context,
84
+ question: str,
85
+ images: bool = False,
86
+ fast_answer: bool = False,
87
+ user_id: str = "rao_answer_summary",
88
+ use_stored_context_prompts: bool = False,
89
+ title: Optional[str] = None,
90
+ ) -> Tuple[Literal["yes", "no", "error"], Optional[str], Optional[str]]:
91
+ """
92
+ Validate if the context is useful to answer the question and attempt use it to answer the user's question.
93
+
94
+ Reports back if the context is useful, what is missing, any error during summarization.
95
+
96
+ It will also save the answer attempt and the citations in the context object.
97
+ """
98
+ t0 = time()
99
+ model = self.context_config.context_validation_model
100
+ module = self.context_config.module
101
+ ident = self.context_config.id if self.context_config.id else "default"
102
+
103
+ citations = None
104
+ contexts = [x.render() for x in context.chunks]
105
+ contexts.extend([x for x in context.structured])
106
+
107
+ contexts_items = {generate_ctx_block_id(i): x for i, x in enumerate(contexts)}
108
+ contexts_items.update(
109
+ {
110
+ generate_ctx_block_id(i + len(context.chunks)): x
111
+ for i, x in enumerate(context.structured)
112
+ }
113
+ )
114
+ block_id_to_chunk = {
115
+ generate_ctx_block_id(i): chunk.chunk_id
116
+ for i, chunk in enumerate(context.chunks)
117
+ }
118
+ block_id_to_chunk.update(
119
+ {
120
+ generate_ctx_block_id(i + len(context.chunks)): f"structured-{i}"
121
+ for i in range(len(context.structured))
122
+ }
123
+ )
124
+
125
+ extra_prompts = [prompt.render() for prompt in context.prompts]
126
+ prompt = VALIDATE_OR_ANSWER_PROMPT_TEMPLATE.render(
127
+ question=question,
128
+ contexts=contexts_items,
129
+ extra_prompts=extra_prompts if use_stored_context_prompts else "",
130
+ )
131
+ try:
132
+ (
133
+ response,
134
+ input_tokens,
135
+ output_tokens,
136
+ ) = await manager.execute_json(
137
+ user_id=user_id + f"-{module}",
138
+ images=context.images if images else {},
139
+ prompt=prompt,
140
+ schema=VALIDATE_JSON_SCHEMA,
141
+ model=model,
142
+ )
143
+ answer = response.get("answer", "")
144
+ missing = response.get("missing_info_query", None)
145
+ useful = response.get("useful", "yes")
146
+ reason = response.get("reason", "")
147
+ citations = response.get("citations", None)
148
+ except Exception as e:
149
+ error_message = f"Error executing validation of contexts for {module} with question '{question}': {e}"
150
+ await memory.add_step(
151
+ step_module=module,
152
+ step_title=f"{title}: Context summary error",
153
+ step_reason="Error in summary generation",
154
+ step_value="Error in summary generation",
155
+ timeit=time() - t0,
156
+ step_agent_path=f"/context/{ident}",
157
+ error=error_message,
158
+ )
159
+ return "error", question, error_message
160
+
161
+ # removing this for now
162
+ # if useful == "yes" and fast_answer:
163
+ # await memory.add_answer(answer, module="ask", agent_path=f"/context/{ident}")
164
+
165
+ if citations is not None:
166
+ cited_chunks = [
167
+ block_id_to_chunk[citation]
168
+ for citation in set(citations)
169
+ if citation in block_id_to_chunk
170
+ ]
171
+ context.citations = cited_chunks
172
+
173
+ context.summary = (
174
+ answer if "not enough data to answer this" not in answer.lower() else ""
175
+ )
176
+
177
+ await memory.add_step(
178
+ step_module=module,
179
+ step_title=f"{title}: Context validation",
180
+ step_reason=reason,
181
+ step_value="No missing information" if not missing else missing,
182
+ timeit=time() - t0,
183
+ input_nuclia_tokens=input_tokens,
184
+ output_nuclia_tokens=output_tokens,
185
+ step_agent_path=f"/context/{ident}",
186
+ )
187
+
188
+ if missing is not None and missing.strip() != "":
189
+ return useful, missing, None
190
+ return useful, None, None
191
+
192
+ async def rephrase(
193
+ self,
194
+ memory: QuestionMemory,
195
+ manager: Manager,
196
+ contexts: Dict[str, Any],
197
+ ident: str,
198
+ question: str,
199
+ question_uuid: str,
200
+ model: str = "",
201
+ module: str = "agent",
202
+ user_id: str = "next",
203
+ title: Optional[str] = None,
204
+ ) -> Tuple[str, str]:
205
+ t0 = time()
206
+ rephrased = False
207
+ prompt = NEXT_REPHRASE_PROMPT_TEMPLATE.render(
208
+ question=question,
209
+ contexts=list(contexts.values()),
210
+ info=self.agent_description,
211
+ extra_info=self.context_config.context_aware_rephrasing_prompt,
212
+ )
213
+ try:
214
+ (
215
+ response,
216
+ input_tokens,
217
+ output_tokens,
218
+ ) = await manager.execute_json(
219
+ user_id=user_id + f"-{module}",
220
+ system=NEXT_REPHRASE_PROMPT_SYSTEM,
221
+ prompt=prompt,
222
+ schema=NEXT_REPHRASE_JSON_SCHEMA,
223
+ model=model,
224
+ )
225
+ rephrased_question = response.get("rephrased_question", "")
226
+ needed = response.get("needed", True)
227
+ reason = response.get("reason", "")
228
+ except Exception as e:
229
+ error_message = f"Error executing rephrase when executing next agent {module} with question '{question}': {e}"
230
+ await memory.add_step(
231
+ step_module=module,
232
+ step_title=f"{title}: Rephrase error",
233
+ step_reason="Error in rephrase generation",
234
+ step_value="Error in rephrase generation",
235
+ timeit=time() - t0,
236
+ step_agent_path=f"/context/{ident}",
237
+ error=error_message,
238
+ )
239
+ return question, question_uuid
240
+ if (
241
+ rephrased_question.strip() != ""
242
+ and needed is True
243
+ and "not enough data to answer this" not in rephrased_question.lower()
244
+ ):
245
+ rephrased_question_uuid = uuid4().hex
246
+ rephrased = True
247
+
248
+ await memory.add_step(
249
+ step_module=module,
250
+ step_title=f"{title}: Rephrase",
251
+ step_reason=reason,
252
+ step_value="No need for rephrasing"
253
+ if rephrased is False
254
+ else rephrased_question,
255
+ timeit=time() - t0,
256
+ input_nuclia_tokens=input_tokens,
257
+ output_nuclia_tokens=output_tokens,
258
+ step_agent_path=f"/context/{ident}",
259
+ )
260
+ return (
261
+ rephrased_question if rephrased else question,
262
+ rephrased_question_uuid if rephrased else question_uuid,
263
+ )
264
+
265
+ async def _get_question_context(
266
+ self,
267
+ memory: QuestionMemory,
268
+ manager: Manager,
269
+ question_uuid: str,
270
+ question: str,
271
+ flow_id: str,
272
+ extra_context: Optional[Dict[str, Any]] = None,
273
+ ) -> List[tuple[str, str]]:
274
+ """To be implemented by child classes to get context for the question.
275
+ Should return a list of (question_uuid, question) tuples representing missing questions.
276
+ """
277
+ raise NotImplementedError()
278
+
279
+ @trace_agent
280
+ async def get_question_context(
281
+ self,
282
+ memory: QuestionMemory,
283
+ manager: Manager,
284
+ question_uuid: str,
285
+ question: str,
286
+ flow_id: str,
287
+ extra_context: Optional[Dict[str, Any]] = None,
288
+ ):
289
+ if extra_context is not None:
290
+ question, question_uuid = await self.rephrase(
291
+ memory=memory,
292
+ manager=manager,
293
+ question_uuid=question_uuid,
294
+ question=question,
295
+ contexts=extra_context,
296
+ model=self.context_config.rephrase_model,
297
+ module=self.context_config.module,
298
+ user_id="next_rephrase",
299
+ ident=self.agent_id,
300
+ )
301
+
302
+ # Missing questions should be a list of (question_uuid, question) tuples
303
+ missing_questions = await self._get_question_context(
304
+ memory, manager, question_uuid, question, flow_id=flow_id
305
+ )
306
+
307
+ if self.fallback is not None:
308
+ for missing_uuid, missing in missing_questions:
309
+ await self.fallback.get_question_context(
310
+ memory,
311
+ manager,
312
+ question_uuid=missing_uuid,
313
+ question=missing,
314
+ flow_id=flow_id,
315
+ )
316
+
317
+ if self.next_agent is not None:
318
+ extra_context = extra_context or {}
319
+ for agent in [self, self.fallback]:
320
+ if agent:
321
+ answer_summaries = memory.get_agent_answer_summaries(
322
+ flow_id=flow_id, agent_id=agent.agent_id
323
+ )
324
+ if answer_summaries:
325
+ extra_context[agent.agent_id] = "\n".join(answer_summaries)
326
+
327
+ await self.next_agent.get_question_context(
328
+ memory,
329
+ manager,
330
+ question_uuid,
331
+ question,
332
+ extra_context=extra_context,
333
+ flow_id=flow_id,
334
+ )
335
+
336
+ async def save_ctx_and_return_missing(
337
+ self,
338
+ *,
339
+ memory: QuestionMemory,
340
+ manager: Manager,
341
+ question: str,
342
+ context: Context,
343
+ flow_id: str,
344
+ use_stored_context_prompts: bool = False,
345
+ ) -> tuple[str, str] | None:
346
+ # We trigger context validation and answer attempt only if we have a fallback or next agent or pruning is enabled
347
+ if (
348
+ self.fallback is not None
349
+ or self.next_agent is not None
350
+ or self.context_config.prune_context
351
+ or use_stored_context_prompts
352
+ ) and self.context_config.context_validation_model:
353
+ (
354
+ useful,
355
+ missing,
356
+ validate_error,
357
+ ) = await self.validate_ctx_and_answer(
358
+ memory,
359
+ manager,
360
+ context,
361
+ question=question,
362
+ use_stored_context_prompts=use_stored_context_prompts,
363
+ )
364
+
365
+ if useful == "yes" or validate_error is not None:
366
+ if self.context_config.prune_context and validate_error is None:
367
+ context.prune_to_citations()
368
+ await memory.save_context(flow_id=flow_id, context=context)
369
+ missing_question = (
370
+ None
371
+ if missing is None or missing.strip() == ""
372
+ else (uuid4().hex, missing)
373
+ )
374
+ return missing_question
375
+ else:
376
+ await memory.save_context(flow_id=flow_id, context=context)
377
+ return None
@@ -0,0 +1,103 @@
1
+ from typing import Any, Dict, Optional, Tuple
2
+
3
+ from pydantic import (
4
+ AliasChoices,
5
+ BaseModel,
6
+ Field,
7
+ ValidationError,
8
+ ValidatorFunctionWrapHandler,
9
+ field_serializer,
10
+ field_validator,
11
+ )
12
+
13
+ from hyperforge.agent import AgentConfig
14
+ from hyperforge.configure import get_agent_config_klass
15
+ from hyperforge.utils import WidgetType
16
+
17
+
18
+ def context_agent_validator(value: Any, handler: ValidatorFunctionWrapHandler) -> str:
19
+ try:
20
+ return handler(value)
21
+ except ValidationError as err:
22
+ if err.errors()[0]["type"] == "string_too_long":
23
+ return handler(value[:5])
24
+ else:
25
+ raise
26
+
27
+
28
+ class ContextAgentConfig(AgentConfig):
29
+ fallback: Optional["ContextAgentConfig"] = Field(
30
+ default=None,
31
+ title="Fallback agent",
32
+ description="Agent to use in case this one fails",
33
+ json_schema_extra={
34
+ "widget": WidgetType.NOT_SHOWN,
35
+ },
36
+ )
37
+ next_agent: Optional["ContextAgentConfig"] = Field(
38
+ default=None,
39
+ title="Next agent",
40
+ description="Agent to run after executing the code",
41
+ json_schema_extra={
42
+ "widget": WidgetType.NOT_SHOWN,
43
+ },
44
+ )
45
+ context_validation_model: str = Field(
46
+ default="chatgpt-azure-4o-mini",
47
+ title="Context validation model",
48
+ description="Model used to validate the agent's generated context and generate an answer attempt to the user's question.",
49
+ json_schema_extra={"widget": WidgetType.MODEL_SELECT},
50
+ # Backward compatibility with frontend and stored configurations
51
+ validation_alias=AliasChoices("context_validation_model", "summarize_model"),
52
+ )
53
+ rephrase_model: str = Field(
54
+ default="chatgpt-azure-4o-mini",
55
+ title="Rephrase model",
56
+ description="Model used to rephrase the question based on context (only used in agents that follow others in a chain)",
57
+ json_schema_extra={"widget": WidgetType.MODEL_SELECT},
58
+ )
59
+ context_aware_rephrasing_prompt: Optional[str] = Field(
60
+ default=None,
61
+ title="Context-aware rephrasing prompt",
62
+ description="Custom prompt to give the model when rephrasing questions based on previous contexts (only used in agents that follow others in a chain)",
63
+ json_schema_extra={
64
+ "widget": WidgetType.EXPANDABLE_TEXTAREA,
65
+ },
66
+ )
67
+ prune_context: bool = Field(
68
+ default=True,
69
+ title="Prune context",
70
+ description="Whether to prune the context pieces generated by this agent before storing them in memory. Will always trigger a context validation call if enabled.",
71
+ )
72
+
73
+ published_functions: Optional[Tuple[str, ...]] = Field(
74
+ default=None,
75
+ title="Published functions",
76
+ description="List of functions published by this agent to be used by other agents in the chain",
77
+ json_schema_extra={
78
+ "widget": WidgetType.NOT_SHOWN,
79
+ },
80
+ )
81
+ # always_validate_contexts: bool = Field(
82
+ # default=False,
83
+ # title="Always validate and attempt to generate an answer",
84
+ # description="Whether to always validate the context retrieved and attempt to generate an answer to the question with this agent's context. Regardless if it is not required for pruning or for the next agent in the chain.",
85
+ # )
86
+
87
+ @field_validator("fallback", "next_agent", mode="before")
88
+ @classmethod
89
+ def is_context_agent(cls, value: Dict[str, Any]) -> BaseModel:
90
+ if value is None:
91
+ return value
92
+ module = value.get("module")
93
+ if module is None:
94
+ raise ValueError("Invalid agent config: missing 'module' field")
95
+
96
+ agent_config_klass = get_agent_config_klass(module, agent_type="context")
97
+ return agent_config_klass.model_validate(value)
98
+
99
+ @field_serializer("fallback", "next_agent")
100
+ def serialize_context_agent(self, field: BaseModel) -> Optional[Dict[str, Any]]:
101
+ if field is None:
102
+ return field
103
+ return field.model_dump()
hyperforge/database.py ADDED
@@ -0,0 +1,3 @@
1
+ import sqlalchemy as sa
2
+
3
+ metadata = sa.MetaData()
@@ -0,0 +1,6 @@
1
+ import logging
2
+
3
+ logger = logging.getLogger("hyperforge.db")
4
+
5
+
6
+ CONFIG_CACHE_KEY = "wmc_{account}_{kbid}_{task_type}"