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
|
@@ -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