hyperforge-smart 1.0.0.post21__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.
File without changes
@@ -0,0 +1,1001 @@
1
+ import asyncio
2
+ import logging
3
+ from dataclasses import dataclass, field
4
+ from time import time
5
+ from typing import Any, ClassVar, Dict, List, Optional, Tuple
6
+ from uuid import uuid4
7
+
8
+ from hyperforge.agent import Agent
9
+ from hyperforge.configure import agent
10
+ from hyperforge.context.agent import ContextAgent, build_context_agent
11
+ from hyperforge.definition import FunctionDefinition
12
+ from hyperforge.interaction import Feedback
13
+ from hyperforge.manager import Manager
14
+ from hyperforge.memory.memory import QuestionMemory
15
+ from hyperforge.models import Chunk, Context, TrackingInfo
16
+ from hyperforge.utils import iterate_tools_resp
17
+ from nuclia.lib.nua_responses import (
18
+ Author,
19
+ ChatModel,
20
+ Message,
21
+ Tool,
22
+ UserPrompt,
23
+ )
24
+ from pydantic import BaseModel, ConfigDict, Field
25
+ from sentry_sdk import capture_exception
26
+
27
+ from hyperforge_smart.config import SmartAgentConfig
28
+ from hyperforge_smart.prompts import (
29
+ PLAN_EXECUTE_EXECUTOR_SYSTEM_PROMPT_TEMPLATE,
30
+ PLAN_EXECUTE_PLANNER_JSON_SCHEMA,
31
+ PLAN_EXECUTE_PLANNER_PROMPT_TEMPLATE,
32
+ PLAN_EXECUTE_PLANNER_SYSTEM_PROMPT,
33
+ REACTIVE_SYSTEM_PROMPT_TEMPLATE,
34
+ )
35
+
36
+ logger = logging.getLogger(__name__)
37
+
38
+ TOOL_NAME_SEPARATOR = "__"
39
+
40
+
41
+ @dataclass
42
+ class ToolError:
43
+ """Represents a tool execution error, kept out of the final context.
44
+
45
+ Tracks which tool call (name + arguments) caused the error so the
46
+ LLM can be informed and decide on an alternative approach.
47
+ """
48
+
49
+ tool_name: str
50
+ tool_arguments: Dict[str, Any]
51
+ error: str
52
+
53
+ def __str__(self) -> str:
54
+ return self.error
55
+
56
+
57
+ TASK_COMPLETE_TOOL = Tool(
58
+ name="task_complete",
59
+ description="Call this tool when you have gathered enough information to answer the question and no more tools are needed.",
60
+ parameters={
61
+ "type": "object",
62
+ "properties": {},
63
+ },
64
+ )
65
+
66
+ USER_FEEDBACK_TOOL = Tool(
67
+ name="user_feedback",
68
+ description="Ask the user a clarifying question when you need more information to proceed.",
69
+ parameters={
70
+ "type": "object",
71
+ "properties": {
72
+ "question": {
73
+ "type": "string",
74
+ "description": "The question to ask the user",
75
+ },
76
+ },
77
+ "required": ["question"],
78
+ },
79
+ )
80
+
81
+
82
+ @dataclass
83
+ class PlanIteration:
84
+ """Record of a single planner iteration and its execution results."""
85
+
86
+ plan_steps: List[Dict[str, Any]] = field(default_factory=list)
87
+ plan_summary: str = ""
88
+ results: List[Tuple[str, Any]] = field(default_factory=list)
89
+ results_summary: str = ""
90
+
91
+
92
+ class RegisteredAgent(BaseModel):
93
+ """A registered context agent with optional description and schema for the planner."""
94
+
95
+ model_config = ConfigDict(arbitrary_types_allowed=True)
96
+
97
+ agent: "ContextAgent" = Field(
98
+ ...,
99
+ title="Agent",
100
+ description="The context agent",
101
+ )
102
+ description: Optional[str] = Field(
103
+ None,
104
+ title="Description",
105
+ description="Description of what this agent does, used by the planner",
106
+ )
107
+ available_functions: Optional[Dict[str, FunctionDefinition]] = Field(
108
+ None,
109
+ title="Exposed functions",
110
+ description="List of functions exposed by this agent",
111
+ )
112
+
113
+
114
+ @agent(
115
+ id="smart",
116
+ agent_type="context",
117
+ title="Smart Agent",
118
+ description="Use multiple agents in a smart way to gather context to answer questions.",
119
+ config_schema=SmartAgentConfig,
120
+ )
121
+ class SmartAgent(Agent[SmartAgentConfig], ContextAgent):
122
+ __published_functions__: ClassVar[Dict[str, FunctionDefinition]] = {
123
+ "smart_planner": FunctionDefinition(
124
+ name="smart_planner",
125
+ description="Execute multiple agents in a smart way to gather context to answer questions.",
126
+ parameters={
127
+ "question": {
128
+ "type": "string",
129
+ "description": "The question to answer by gathering context from registered agents.",
130
+ },
131
+ },
132
+ )
133
+ }
134
+ config: SmartAgentConfig
135
+ registered_agents: List[RegisteredAgent]
136
+
137
+ async def inner_from_config(
138
+ self, config: SmartAgentConfig, agent_id: Optional[str] = None
139
+ ):
140
+ # Build registered agents - convert the agent config to actual agent instances
141
+ registered_agents_list = []
142
+ registered_agents_exposed_functions = (
143
+ config.registered_agents_exposed_functions or {}
144
+ )
145
+ registered_agents_descriptions = config.registered_agents_descriptions or {}
146
+ for reg_agent_config in config.registered_agents or []:
147
+ # Build the actual agent from its config
148
+ agent_instance = await build_context_agent(reg_agent_config)
149
+
150
+ if agent_instance is not None:
151
+ # Create RegisteredAgent with the built instance
152
+ available_functions = None
153
+ agent_id = agent_instance.context_config.id
154
+ if agent_id is None:
155
+ agent_id = agent_instance.agent_id
156
+ exposed_functions = registered_agents_exposed_functions.get(
157
+ agent_id, None
158
+ )
159
+ description = registered_agents_descriptions.get(agent_id, "")
160
+ if isinstance(exposed_functions, list) and exposed_functions:
161
+ available_functions = {
162
+ function_id: function_definition
163
+ for function_id, function_definition in agent_instance.__published_functions__.items()
164
+ if function_id in exposed_functions
165
+ }
166
+ elif exposed_functions is None or (
167
+ isinstance(exposed_functions, list) and not exposed_functions
168
+ ):
169
+ available_functions = agent_instance.__published_functions__
170
+ registered_agents_list.append(
171
+ RegisteredAgent(
172
+ agent=agent_instance,
173
+ description=description,
174
+ available_functions=available_functions,
175
+ )
176
+ )
177
+ # FALLBACK AND NEXT AGENT HIDDEN IN THIS ITERATION
178
+ # fallback_agent = await build_context_agent(config.get("fallback"))
179
+ # next_agent = await build_context_agent(config.get("next_agent"))
180
+
181
+ await self.context_from_config(config)
182
+ self.registered_agents = registered_agents_list
183
+
184
+ def get_agent_by_id(self, agent_id: str) -> Optional[RegisteredAgent]:
185
+ """Find a registered agent by its ID."""
186
+ for reg_agent in self.registered_agents:
187
+ if reg_agent.agent.agent_id == agent_id:
188
+ return reg_agent
189
+ return None
190
+
191
+ def build_tools(self) -> List[Tool]:
192
+ """Convert all registered agent functions into Tool objects for native tool calling."""
193
+ tools: List[Tool] = []
194
+ for reg_agent in self.registered_agents:
195
+ if not reg_agent.available_functions:
196
+ continue
197
+ agent_id = reg_agent.agent.agent_id or ""
198
+ for function_id, func_def in reg_agent.available_functions.items():
199
+ tool_name = f"{function_id}{TOOL_NAME_SEPARATOR}{agent_id}"
200
+ description = func_def.description
201
+ if reg_agent.description:
202
+ description = f"{reg_agent.description} — {description}"
203
+ tools.append(
204
+ Tool(
205
+ name=tool_name,
206
+ description=description,
207
+ parameters={
208
+ "type": "object",
209
+ "properties": func_def.parameters,
210
+ "additionalProperties": False,
211
+ },
212
+ )
213
+ )
214
+ tools.append(TASK_COMPLETE_TOOL)
215
+ if self.config.enable_user_feedback:
216
+ tools.append(USER_FEEDBACK_TOOL)
217
+ return tools
218
+
219
+ def build_tools_description(self) -> str:
220
+ """Return a human-readable text block describing all available tools for the planner prompt."""
221
+ lines: List[str] = []
222
+ for reg_agent in self.registered_agents:
223
+ if not reg_agent.available_functions:
224
+ continue
225
+ agent_id = reg_agent.agent.agent_id or ""
226
+ for function_id, func_def in reg_agent.available_functions.items():
227
+ tool_name = f"{function_id}{TOOL_NAME_SEPARATOR}{agent_id}"
228
+ description = func_def.description
229
+ if reg_agent.description:
230
+ description = f"{reg_agent.description} — {description}"
231
+ param_lines = []
232
+ for param_name, param_info in (func_def.parameters or {}).items():
233
+ param_type = param_info.get("type", "any")
234
+ param_desc = param_info.get("description", "")
235
+ param_lines.append(
236
+ f" - {param_name} ({param_type}): {param_desc}"
237
+ )
238
+ params_text = (
239
+ "\n".join(param_lines) if param_lines else " (no parameters)"
240
+ )
241
+ lines.append(f"- **{tool_name}**: {description}\n{params_text}")
242
+ return "\n\n".join(lines) if lines else "(no tools available)"
243
+
244
+ def _process_results(
245
+ self,
246
+ results: List[Tuple[str, Any]],
247
+ context: Optional[Context] = None,
248
+ ) -> List[str]:
249
+ """Process tool results: optionally update context chunks, always return text summaries.
250
+
251
+ ToolError results are included in the text summaries (so the LLM
252
+ is aware of the failure) but are never stored in the context.
253
+ """
254
+ result_texts: List[str] = []
255
+ for action_info, result in results:
256
+ if isinstance(result, ToolError):
257
+ result_texts.append(f"[{action_info}]:\n{result.error}")
258
+ continue
259
+
260
+ if isinstance(result, Context):
261
+ contexts = [result]
262
+ elif isinstance(result, list) and all(
263
+ isinstance(item, Context) for item in result
264
+ ):
265
+ contexts = result
266
+ else:
267
+ contexts = []
268
+
269
+ if context is not None:
270
+ if contexts:
271
+ for ctx in contexts:
272
+ for chunk in ctx.chunks:
273
+ chunk.action = action_info
274
+ context.chunks.append(chunk)
275
+ if ctx.structured:
276
+ for structured in ctx.structured:
277
+ if structured:
278
+ context.structured.append(structured)
279
+ else:
280
+ context.chunks.append(
281
+ Chunk(
282
+ chunk_id=uuid4().hex,
283
+ text=str(result),
284
+ action=action_info,
285
+ origin_agent=self.config.module, # TODO: track origin agent in a better way for text results (this is a corner case, ideally tools return Context objects)
286
+ )
287
+ )
288
+
289
+ for ctx in contexts:
290
+ result_texts.append(f"[{action_info}]:\n{ctx.context_markdown()}")
291
+ if not contexts:
292
+ result_texts.append(f"[{action_info}]:\n{result}")
293
+
294
+ return result_texts
295
+
296
+ async def choose_tools(
297
+ self,
298
+ manager: Manager,
299
+ messages: List[Message],
300
+ tools: List[Tool],
301
+ system_override: Optional[str] = None,
302
+ tracking: TrackingInfo | None = None,
303
+ ) -> Tuple[Any, float, float]:
304
+ """Call the LLM with available tools and return its tool selections."""
305
+
306
+ system = system_override or REACTIVE_SYSTEM_PROMPT_TEMPLATE.render(
307
+ extra_instructions=self.config.extra_prompt or ""
308
+ )
309
+ model = self.config.executor_model
310
+
311
+ item = ChatModel(
312
+ question="",
313
+ user_id=f"smart_planner-{self.config.module}",
314
+ generative_model=model,
315
+ tools=tools,
316
+ user_prompt=UserPrompt(
317
+ prompt=f"{system}\n\nChoose the best tool or tools for the task. Call task_complete when you have enough information."
318
+ ),
319
+ format_prompt=False,
320
+ system=system,
321
+ chat_history=messages,
322
+ )
323
+ resp, input_tokens, output_tokens = await manager.execute_raw(
324
+ item, tracking=tracking
325
+ )
326
+ return resp, input_tokens, output_tokens
327
+
328
+ async def _report_tool_error(
329
+ self,
330
+ memory: QuestionMemory,
331
+ title: str,
332
+ error: str,
333
+ tool_name: str,
334
+ tool_arguments: Dict[str, Any],
335
+ ) -> Tuple[str, ToolError]:
336
+ """Log a tool error to memory and return a ToolError result."""
337
+ await memory.add_step(
338
+ step_module=self.config.module,
339
+ step_title=self.step_title(title),
340
+ step_reason=error,
341
+ step_agent_path=f"/context/{self.config.id or 'default'}",
342
+ step_value="Error",
343
+ timeit=0,
344
+ )
345
+ return tool_name, ToolError(
346
+ tool_name=tool_name,
347
+ tool_arguments=tool_arguments,
348
+ error=error,
349
+ )
350
+
351
+ async def execute_tool_call(
352
+ self,
353
+ memory: QuestionMemory,
354
+ manager: Manager,
355
+ tool_name: str,
356
+ tool_arguments: Dict[str, Any],
357
+ ) -> Tuple[str, Any]:
358
+ """Parse a tool name, look up the agent/function, and execute it."""
359
+ parts = tool_name.split(TOOL_NAME_SEPARATOR, 1)
360
+ if len(parts) != 2:
361
+ return await self._report_tool_error(
362
+ memory,
363
+ "Invalid tool name",
364
+ f"Invalid tool name format: {tool_name!r}",
365
+ tool_name,
366
+ tool_arguments,
367
+ )
368
+
369
+ function_id, agent_id = parts
370
+ reg_agent = self.get_agent_by_id(agent_id)
371
+
372
+ if reg_agent is None:
373
+ return await self._report_tool_error(
374
+ memory,
375
+ "Agent not found",
376
+ f"Agent {agent_id!r} not found in registered agents",
377
+ tool_name,
378
+ tool_arguments,
379
+ )
380
+
381
+ if (
382
+ not reg_agent.available_functions
383
+ or function_id not in reg_agent.available_functions
384
+ ):
385
+ return await self._report_tool_error(
386
+ memory,
387
+ "Function not found",
388
+ f"Function {function_id!r} not found in agent {agent_id!r}",
389
+ tool_name,
390
+ tool_arguments,
391
+ )
392
+
393
+ action_attr = getattr(reg_agent.agent, function_id, None)
394
+ if action_attr is None:
395
+ return await self._report_tool_error(
396
+ memory,
397
+ "Function not found",
398
+ f"Function {function_id!r} not found in agent {agent_id!r} instance",
399
+ tool_name,
400
+ tool_arguments,
401
+ )
402
+
403
+ try:
404
+ result = await action_attr(
405
+ memory=memory,
406
+ manager=manager,
407
+ **tool_arguments,
408
+ )
409
+ except TypeError as e:
410
+ return await self._report_tool_error(
411
+ memory,
412
+ "LLM Execution error",
413
+ f"Binding error executing tool {function_id!r} of agent {agent_id!r}: {e}",
414
+ tool_name,
415
+ tool_arguments,
416
+ )
417
+ except Exception as e:
418
+ logger.exception(
419
+ f"Error executing tool {function_id!r} of agent {agent_id!r}"
420
+ )
421
+ capture_exception(e)
422
+ return await self._report_tool_error(
423
+ memory,
424
+ "LLM Execution error",
425
+ f"Error executing tool {function_id!r} of agent {agent_id!r}: {e}",
426
+ tool_name,
427
+ tool_arguments,
428
+ )
429
+
430
+ action_info = f"{function_id} of {agent_id}"
431
+ if tool_arguments:
432
+ action_info += f" with parameters {tool_arguments}"
433
+ return action_info, result
434
+
435
+ async def _preload_registered_agents(
436
+ self, manager: Manager, memory: QuestionMemory
437
+ ) -> None:
438
+ """Call ``preload`` on every registered agent that supports it.
439
+
440
+ Agents whose ``__published_functions__`` dict is empty after
441
+ ``inner_from_config`` (e.g. ``MCPAgent``) declare their tools at
442
+ runtime via this hook. After preloading, ``available_functions`` on
443
+ the ``RegisteredAgent`` wrapper is refreshed so ``build_tools`` picks
444
+ up the newly discovered functions.
445
+
446
+ Agents that do not override ``preload`` (the base no-op) are unaffected.
447
+ """
448
+ for reg_agent in self.registered_agents:
449
+ await reg_agent.agent.preload(manager, memory)
450
+ if (
451
+ not reg_agent.available_functions
452
+ and reg_agent.agent.__published_functions__
453
+ ):
454
+ reg_agent.available_functions = reg_agent.agent.__published_functions__
455
+
456
+ async def smart_planner(
457
+ self,
458
+ question: str,
459
+ memory: QuestionMemory,
460
+ manager: Manager,
461
+ question_uuid: Optional[str] = None,
462
+ extra_context: Optional[Dict[str, Any]] = None,
463
+ ) -> Context:
464
+ """Entry point: dispatches to the appropriate reasoning mode."""
465
+ if question_uuid is None:
466
+ question_uuid = uuid4().hex
467
+
468
+ await self._preload_registered_agents(manager, memory)
469
+
470
+ session_context_parts: List[str] = []
471
+
472
+ if self.config.history:
473
+ qa_history, interactions = await memory.context_history()
474
+ await memory.add_step(
475
+ step_module=self.config.module,
476
+ step_title=self.step_title("History check"),
477
+ step_value="Included {} interactions of Q&A history".format(
478
+ interactions
479
+ ),
480
+ step_reason="",
481
+ timeit=0,
482
+ step_agent_path=f"/context/{self.config.id if self.config.id else 'default'}",
483
+ input_nuclia_tokens=0.0,
484
+ output_nuclia_tokens=0.0,
485
+ )
486
+ session_context_parts.append(
487
+ f"## Previous questions and answers in this session:\n{qa_history}"
488
+ )
489
+
490
+ session_context = "\n\n".join(session_context_parts)
491
+
492
+ if self.config.planning_mode == "plan_execute":
493
+ return await self._plan_and_execute(
494
+ question=question,
495
+ memory=memory,
496
+ manager=manager,
497
+ question_uuid=question_uuid,
498
+ extra_context=extra_context,
499
+ session_context=session_context,
500
+ )
501
+ return await self._reactive_loop(
502
+ question=question,
503
+ memory=memory,
504
+ manager=manager,
505
+ question_uuid=question_uuid,
506
+ extra_context=extra_context,
507
+ session_context=session_context,
508
+ )
509
+
510
+ async def _execute_tool_calls_turn(
511
+ self,
512
+ memory: QuestionMemory,
513
+ manager: Manager,
514
+ messages: List[Message],
515
+ tool_calls: List[Tuple[str, Any]],
516
+ turn_label: str,
517
+ context: Optional[Context] = None,
518
+ ) -> List[Tuple[str, Any]]:
519
+ """Handle one turn of tool calls.
520
+
521
+ If the LLM requested user feedback, sends the feedback request, records it as a
522
+ step, stores it via _process_results and returns (results, True) so the caller
523
+ can ``continue`` to the next iteration without executing other tools.
524
+
525
+ Otherwise executes all tool calls in parallel, records an execution step and
526
+ returns (results, False).
527
+ """
528
+ agent_path = f"/context/{self.config.id or 'default'}"
529
+
530
+ # --- user_feedback path ---
531
+ if any(name == "user_feedback" for name, _ in tool_calls):
532
+ for name, args in tool_calls:
533
+ if name == "user_feedback":
534
+ feedback_question: Optional[str] = (
535
+ args.get("question") if args else None
536
+ )
537
+ if feedback_question:
538
+ feedback = Feedback(
539
+ request_id=memory.get_session_id(),
540
+ question=feedback_question,
541
+ module=self.config.module,
542
+ agent_id=self.config.id or "default",
543
+ data=None,
544
+ timeout_ms=self.config.feedback_timeout,
545
+ response_schema={
546
+ "type": "object",
547
+ "properties": {"response": {"type": "string"}},
548
+ "required": ["response"],
549
+ },
550
+ )
551
+ answer = await memory.send_feedback(feedback)
552
+ feedback_text = (
553
+ answer.response
554
+ if (
555
+ answer is not None
556
+ and answer.request_id == memory.get_session_id()
557
+ )
558
+ else "(No response received)"
559
+ )
560
+ messages.append(Message(author=Author.USER, text=feedback_text))
561
+ logger.info(f"Received user feedback response: {feedback_text}")
562
+ await memory.add_step(
563
+ step_module=self.config.module,
564
+ step_title=self.step_title(f"User feedback {turn_label}"),
565
+ step_reason="User feedback requested and received.",
566
+ step_agent_path=agent_path,
567
+ step_value=f"Feedback question: {feedback_question}\nFeedback response: {feedback_text}",
568
+ timeit=0,
569
+ )
570
+ feedback_result: Tuple[str, Any] = (
571
+ "user_feedback",
572
+ feedback_text,
573
+ )
574
+ result_texts = self._process_results(
575
+ [feedback_result], context=context
576
+ )
577
+ if result_texts:
578
+ messages.append(
579
+ Message(
580
+ author=Author.NUCLIA,
581
+ text="\n\n".join(result_texts),
582
+ )
583
+ )
584
+ return [feedback_result]
585
+ return []
586
+
587
+ # --- normal tool execution path ---
588
+ results = await asyncio.gather(
589
+ *[
590
+ self.execute_tool_call(memory, manager, name, args)
591
+ for name, args in tool_calls
592
+ if name != "task_complete"
593
+ ]
594
+ )
595
+ result_texts = self._process_results(list(results), context=context)
596
+ result_summary = "; ".join(
597
+ f"{info}: {'context' if isinstance(res, Context) else type(res).__name__}"
598
+ for info, res in results
599
+ )
600
+ await memory.add_step(
601
+ step_module=self.config.module,
602
+ step_title=self.step_title(f"Execution {turn_label}"),
603
+ step_reason=f"Executed {len(results)} tool(s) and collected results",
604
+ step_agent_path=agent_path,
605
+ step_value=f"Results: {result_summary}",
606
+ timeit=0,
607
+ )
608
+ if result_texts:
609
+ messages.append(
610
+ Message(author=Author.NUCLIA, text="\n\n".join(result_texts))
611
+ )
612
+ return list(results)
613
+
614
+ async def _reactive_loop(
615
+ self,
616
+ question: str,
617
+ memory: QuestionMemory,
618
+ manager: Manager,
619
+ question_uuid: str,
620
+ extra_context: Optional[Dict[str, Any]] = None,
621
+ session_context: str = "",
622
+ ) -> Context:
623
+ t0 = time()
624
+
625
+ tools = self.build_tools()
626
+ messages: List[Message] = []
627
+ if session_context:
628
+ messages.append(
629
+ Message(
630
+ author=Author.NUCLIA,
631
+ text=(
632
+ "The following context from the current session may be relevant to answer the user's question, or can be used to rephrase the question or guide the model:\n\n"
633
+ + session_context
634
+ ),
635
+ )
636
+ )
637
+ messages.append(Message(author=Author.USER, text=question))
638
+
639
+ context = Context(
640
+ agent_id=self.config.id or "smart_agent",
641
+ original_question_uuid=memory.original_question_uuid,
642
+ actual_question_uuid=question_uuid,
643
+ question=question,
644
+ source="smart_agent",
645
+ agent="smart_agent",
646
+ title=self.config.title or "Smart Agent Results",
647
+ )
648
+
649
+ iteration = 0
650
+ finished = False
651
+ total_input_tokens = 0.0
652
+ total_output_tokens = 0.0
653
+ while not finished and iteration < self.config.max_iterations:
654
+ iteration += 1
655
+
656
+ resp, input_tokens, output_tokens = await self.choose_tools(
657
+ manager,
658
+ messages,
659
+ tools,
660
+ tracking=memory.get_tracking_info(),
661
+ )
662
+ total_input_tokens += input_tokens
663
+ total_output_tokens += output_tokens
664
+
665
+ tool_calls = list(iterate_tools_resp(resp))
666
+
667
+ tool_names = [name for name, _ in tool_calls]
668
+ tool_detail = (
669
+ ", ".join(
670
+ f"{name}({', '.join(f'{k}={v!r}' for k, v in (args or {}).items())})"
671
+ for name, args in tool_calls
672
+ )
673
+ or "none"
674
+ )
675
+
676
+ await memory.add_step(
677
+ step_module=self.config.module,
678
+ step_title=self.step_title(
679
+ f"Reactive iteration {iteration}/{self.config.max_iterations}"
680
+ ),
681
+ step_reason=f"LLM selected {len(tool_calls)} tool(s): {', '.join(tool_names) or 'none'}",
682
+ step_agent_path=f"/context/{self.config.id or 'default'}",
683
+ step_value=f"Tool calls: {tool_detail}",
684
+ timeit=0,
685
+ input_nuclia_tokens=input_tokens,
686
+ output_nuclia_tokens=output_tokens,
687
+ )
688
+
689
+ if not tool_calls:
690
+ finished = True
691
+ break
692
+
693
+ # Check for task_complete before executing
694
+ if any(name == "task_complete" for name, _ in tool_calls):
695
+ finished = True
696
+ break
697
+
698
+ # Execute tool calls (handles user_feedback and normal tool calls)
699
+ _ = await self._execute_tool_calls_turn(
700
+ memory=memory,
701
+ manager=manager,
702
+ messages=messages,
703
+ tool_calls=tool_calls,
704
+ turn_label=f"iteration {iteration}/{self.config.max_iterations}",
705
+ context=context,
706
+ )
707
+
708
+ reason = (
709
+ "Task complete signal received"
710
+ if finished
711
+ else f"Reached max iterations ({self.config.max_iterations})"
712
+ )
713
+ await memory.add_step(
714
+ step_module=self.config.module,
715
+ step_title=self.step_title("Reactive mode completed"),
716
+ step_reason=reason,
717
+ step_agent_path=f"/context/{self.config.id or 'default'}",
718
+ step_value=f"Completed after {iteration} iteration(s). Total tokens: {total_input_tokens} in / {total_output_tokens} out",
719
+ timeit=time() - t0,
720
+ input_nuclia_tokens=total_input_tokens,
721
+ output_nuclia_tokens=total_output_tokens,
722
+ )
723
+
724
+ return context
725
+
726
+ async def _call_planner(
727
+ self,
728
+ manager: Manager,
729
+ question: str,
730
+ history: List[PlanIteration],
731
+ tools_description: str,
732
+ session_context: str = "",
733
+ tracking: TrackingInfo | None = None,
734
+ ) -> Tuple[Dict[str, Any], float, float]:
735
+ """Call the planner LLM to produce a structured retrieval plan."""
736
+ prompt = PLAN_EXECUTE_PLANNER_PROMPT_TEMPLATE.render(
737
+ question=question,
738
+ tools_description=tools_description,
739
+ history=history,
740
+ extra_instructions=self.config.extra_prompt or "",
741
+ session_context=session_context,
742
+ )
743
+ full_prompt = PLAN_EXECUTE_PLANNER_SYSTEM_PROMPT + "\n\n" + prompt
744
+
745
+ # Commented until we fix the reasoning issue around json output
746
+ # if self.config.planner_reasoning:
747
+ # item = ChatModel(
748
+ # user_id=f"smart_planner_plan-{self.config.module}",
749
+ # question="",
750
+ # user_prompt=UserPrompt(prompt=full_prompt),
751
+ # generative_model=self.config.planner_model,
752
+ # format_prompt=False,
753
+ # json_schema=PLAN_EXECUTE_PLANNER_JSON_SCHEMA,
754
+ # system=PLAN_EXECUTE_PLANNER_SYSTEM_PROMPT,
755
+ # citations=False,
756
+ # reasoning=Reasoning(effort="medium"),
757
+ # max_tokens=20_000,
758
+ # )
759
+ # resp, input_tokens, output_tokens = await manager.execute_raw(item)
760
+ # if resp.object is None:
761
+ # raise Exception("No object from planner")
762
+ # return resp.object, input_tokens, output_tokens
763
+
764
+ response, input_tokens, output_tokens = await manager.execute_json(
765
+ user_id=f"smart_planner_plan-{self.config.module}",
766
+ prompt=full_prompt,
767
+ schema=PLAN_EXECUTE_PLANNER_JSON_SCHEMA,
768
+ model=self.config.planner_model,
769
+ system=PLAN_EXECUTE_PLANNER_SYSTEM_PROMPT,
770
+ tracking=tracking,
771
+ )
772
+ return response, input_tokens, output_tokens
773
+
774
+ async def _call_executor(
775
+ self,
776
+ memory: QuestionMemory,
777
+ manager: Manager,
778
+ question: str,
779
+ steps: List[Dict[str, Any]],
780
+ summary: str,
781
+ tools: List[Tool],
782
+ ) -> Tuple[List[Tuple[str, Any]], float, float]:
783
+ """Run the executor LLM turn: call tools guided by the current plan steps."""
784
+ system = PLAN_EXECUTE_EXECUTOR_SYSTEM_PROMPT_TEMPLATE.render(
785
+ question=question,
786
+ steps=steps,
787
+ summary=summary,
788
+ extra_instructions=self.config.extra_prompt or "",
789
+ )
790
+ messages: List[Message] = [
791
+ Message(author=Author.USER, text=question),
792
+ ]
793
+
794
+ all_results: List[Tuple[str, Any]] = []
795
+ total_input_tokens = 0.0
796
+ total_output_tokens = 0.0
797
+ finished = False
798
+ max_executor_turns = self.config.max_iterations
799
+
800
+ executor_turn = 0
801
+ while not finished and executor_turn < max_executor_turns:
802
+ executor_turn += 1
803
+ resp, input_tokens, output_tokens = await self.choose_tools(
804
+ manager=manager,
805
+ messages=messages,
806
+ tools=tools,
807
+ system_override=system,
808
+ tracking=memory.get_tracking_info(),
809
+ )
810
+ total_input_tokens += input_tokens
811
+ total_output_tokens += output_tokens
812
+
813
+ tool_calls = list(iterate_tools_resp(resp))
814
+
815
+ if not tool_calls or any(name == "task_complete" for name, _ in tool_calls):
816
+ finished = True
817
+ break
818
+
819
+ results = await self._execute_tool_calls_turn(
820
+ memory=memory,
821
+ manager=manager,
822
+ messages=messages,
823
+ tool_calls=tool_calls,
824
+ turn_label=f"executor turn {executor_turn}/{max_executor_turns}",
825
+ )
826
+ all_results.extend(results)
827
+
828
+ return all_results, total_input_tokens, total_output_tokens
829
+
830
+ async def _plan_and_execute(
831
+ self,
832
+ question: str,
833
+ memory: QuestionMemory,
834
+ manager: Manager,
835
+ question_uuid: str,
836
+ extra_context: Optional[Dict[str, Any]] = None,
837
+ session_context: str = "",
838
+ ) -> Context:
839
+ """Plan-execute reasoning mode: planner drafts a plan, executor runs tools, repeat."""
840
+ t0 = time()
841
+ agent_path = f"/context/{self.config.id or 'default'}"
842
+
843
+ context = Context(
844
+ agent_id=self.config.id or "smart_agent",
845
+ original_question_uuid=memory.original_question_uuid,
846
+ actual_question_uuid=question_uuid,
847
+ question=question,
848
+ source="smart_agent",
849
+ agent="smart_agent",
850
+ title=self.config.title or "Smart Agent Results",
851
+ )
852
+
853
+ history: List[PlanIteration] = []
854
+ iteration = 0
855
+ total_input_tokens = 0.0
856
+ total_output_tokens = 0.0
857
+
858
+ # Build tools once for the entire plan-execute cycle
859
+ tools = self.build_tools()
860
+ tools_description = self.build_tools_description()
861
+
862
+ while iteration < self.config.max_iterations:
863
+ iteration += 1
864
+
865
+ # PLANNER
866
+ plan_response, plan_in_tokens, plan_out_tokens = await self._call_planner(
867
+ manager=manager,
868
+ question=question,
869
+ history=history,
870
+ tools_description=tools_description,
871
+ session_context=session_context,
872
+ tracking=memory.get_tracking_info(),
873
+ )
874
+ total_input_tokens += plan_in_tokens
875
+ total_output_tokens += plan_out_tokens
876
+
877
+ status = plan_response.get("status", "done")
878
+ reasoning = plan_response.get("reasoning", "")
879
+ summary = plan_response.get("summary", "")
880
+ steps = plan_response.get("steps", [])
881
+
882
+ if steps:
883
+ plan_summary = f"{len(steps)} step(s): " + "; ".join(
884
+ s.get("description", "?") for s in steps
885
+ )
886
+ else:
887
+ plan_summary = "(no steps)"
888
+
889
+ step_detail = "\n".join(
890
+ f" {i + 1}. [{s.get('reason', '')}] {s.get('description', '?')}"
891
+ for i, s in enumerate(steps)
892
+ )
893
+ await memory.add_step(
894
+ step_module=self.config.module,
895
+ step_title=self.step_title(
896
+ f"Planner iteration {iteration}/{self.config.max_iterations}"
897
+ ),
898
+ step_reason=f"Status: {status}. Reasoning: {reasoning}",
899
+ step_agent_path=agent_path,
900
+ step_value=f"Plan: {plan_summary}\nSummary so far: {summary or '(initial)'}\nSteps:\n{step_detail}",
901
+ timeit=0,
902
+ input_nuclia_tokens=plan_in_tokens,
903
+ output_nuclia_tokens=plan_out_tokens,
904
+ )
905
+
906
+ if status == "done" or not steps:
907
+ break
908
+
909
+ # EXECUTOR
910
+ current_iteration = PlanIteration(
911
+ plan_steps=steps,
912
+ plan_summary=plan_summary,
913
+ )
914
+
915
+ (
916
+ iteration_results,
917
+ exec_in_tokens,
918
+ exec_out_tokens,
919
+ ) = await self._call_executor(
920
+ memory=memory,
921
+ manager=manager,
922
+ question=question,
923
+ steps=steps,
924
+ summary=summary,
925
+ tools=tools,
926
+ )
927
+ total_input_tokens += exec_in_tokens
928
+ total_output_tokens += exec_out_tokens
929
+
930
+ result_texts = self._process_results(iteration_results, context=context)
931
+ results_summary = (
932
+ "\n\n".join(result_texts) if result_texts else "(no results)"
933
+ )
934
+ current_iteration.results = iteration_results
935
+ current_iteration.results_summary = results_summary
936
+ history.append(current_iteration)
937
+
938
+ exec_result_summary = (
939
+ "; ".join(
940
+ f"{info}: {'context' if isinstance(res, Context) else type(res).__name__}"
941
+ for info, res in iteration_results
942
+ )
943
+ or "(no results)"
944
+ )
945
+
946
+ await memory.add_step(
947
+ step_module=self.config.module,
948
+ step_title=self.step_title(
949
+ f"Executor completed iteration {iteration}/{self.config.max_iterations}"
950
+ ),
951
+ step_reason=f"Executed plan with {len(steps)} step(s). Summary: {summary}",
952
+ step_agent_path=agent_path,
953
+ step_value=f"Executed {len(iteration_results)} tool call(s). Results: {exec_result_summary}",
954
+ timeit=0,
955
+ input_nuclia_tokens=exec_in_tokens,
956
+ output_nuclia_tokens=exec_out_tokens,
957
+ )
958
+
959
+ done_reason = (
960
+ "Planner signaled done"
961
+ if (status == "done" or not steps)
962
+ else f"Reached max iterations ({self.config.max_iterations})"
963
+ )
964
+ await memory.add_step(
965
+ step_module=self.config.module,
966
+ step_title=self.step_title("Plan-execute mode completed"),
967
+ step_reason=done_reason,
968
+ step_agent_path=agent_path,
969
+ step_value=f"Completed after {iteration} planning iteration(s). Total tokens: {total_input_tokens} in / {total_output_tokens} out",
970
+ timeit=time() - t0,
971
+ input_nuclia_tokens=total_input_tokens,
972
+ output_nuclia_tokens=total_output_tokens,
973
+ )
974
+
975
+ return context
976
+
977
+ async def _get_question_context(
978
+ self,
979
+ memory: QuestionMemory,
980
+ manager: Manager,
981
+ question_uuid: str,
982
+ question: str,
983
+ flow_id: str,
984
+ extra_context: Optional[Dict[str, Any]] = None,
985
+ ) -> List[Tuple[str, str]]:
986
+ context = await self.smart_planner(
987
+ memory=memory,
988
+ manager=manager,
989
+ question_uuid=question_uuid,
990
+ question=question,
991
+ extra_context=extra_context,
992
+ )
993
+
994
+ missing = await self.save_ctx_and_return_missing(
995
+ context=context,
996
+ question=question,
997
+ memory=memory,
998
+ manager=manager,
999
+ flow_id=flow_id,
1000
+ )
1001
+ return [missing] if missing is not None else []
@@ -0,0 +1,133 @@
1
+ from typing import Any, Dict, List, Literal, Optional, Tuple
2
+
3
+ from hyperforge.configure import get_agent_config_klass
4
+ from hyperforge.context.config import ContextAgentConfig
5
+ from hyperforge.utils import WidgetType
6
+ from pydantic import BaseModel, Field, field_serializer, field_validator
7
+ from pydantic.config import ConfigDict
8
+
9
+ PlanningMode = Literal["reactive", "plan_execute"]
10
+
11
+
12
+ class SmartAgentConfig(ContextAgentConfig):
13
+ model_config = ConfigDict(title="Smart agent")
14
+ module: Literal["smart"] = "smart"
15
+ planning_mode: PlanningMode = Field(
16
+ default="reactive",
17
+ title="Planning mode",
18
+ description=(
19
+ "How the smart agent reasons about which tools to call. "
20
+ "'reactive' (default): iterative LLM tool selection — the LLM picks tools each turn, "
21
+ "executes them, and loops until task_complete. "
22
+ "'plan_execute': a planner LLM first drafts a structured step-by-step plan, then an "
23
+ "executor follows the plan and calls tools; the planner is re-invoked to assess progress "
24
+ "and decide whether more retrieval is needed."
25
+ ),
26
+ json_schema_extra={"widget": WidgetType.ENUM_SELECT},
27
+ )
28
+ enable_user_feedback: bool = Field(
29
+ default=False,
30
+ title="Enable user feedback tool",
31
+ description="Allow the LLM to call a user_feedback tool to ask the user clarifying questions during execution.",
32
+ )
33
+ feedback_timeout: int = Field(
34
+ default=10_000,
35
+ title="Feedback timeout (ms)",
36
+ description="How long to wait for a user feedback response before giving up, in milliseconds.",
37
+ )
38
+ published_functions: Optional[Tuple[str, ...]] = Field(
39
+ default=("smart_planner",),
40
+ title="Published functions",
41
+ description="List of functions published by this agent to be used by other agents in the chain",
42
+ json_schema_extra={
43
+ "widget": WidgetType.NOT_SHOWN,
44
+ },
45
+ )
46
+ # Registered agents, list of Context agents, their descriptions and schemas to be used by the planner
47
+ registered_agents: List[ContextAgentConfig] = Field(
48
+ default_factory=list,
49
+ title="Registered agents",
50
+ description="List of context agents available for the smart agent to use",
51
+ json_schema_extra={
52
+ "widget": WidgetType.NOT_SHOWN,
53
+ },
54
+ )
55
+ registered_agents_descriptions: Optional[Dict[str, str]] = Field(
56
+ default=None,
57
+ title="Registered agents descriptions",
58
+ description="Descriptions of the registered agents for the planner",
59
+ json_schema_extra={
60
+ "widget": WidgetType.NOT_SHOWN,
61
+ },
62
+ )
63
+ registered_agents_exposed_functions: Optional[Dict[str, List[str]]] = Field(
64
+ default=None,
65
+ title="Registered agents exposed functions",
66
+ description="Exposed functions of the registered agents for the planner",
67
+ json_schema_extra={
68
+ "widget": WidgetType.NOT_SHOWN,
69
+ },
70
+ )
71
+ planner_model: str = Field(
72
+ default="chatgpt-4.1",
73
+ title="Planner model",
74
+ description="Model used to plan the actions to take",
75
+ json_schema_extra={"widget": WidgetType.MODEL_SELECT},
76
+ )
77
+ executor_model: str = Field(
78
+ default="chatgpt-4.1",
79
+ title="Executor model",
80
+ description=("Model used to select and execute the tools."),
81
+ json_schema_extra={"widget": WidgetType.MODEL_SELECT},
82
+ )
83
+ max_iterations: int = Field(
84
+ default=5,
85
+ title="Max iterations",
86
+ description="Maximum number of planning and execution iterations before stopping",
87
+ )
88
+ extra_prompt: Optional[str] = Field(
89
+ None,
90
+ title="Extra prompt",
91
+ description="Extra prompt to provide to the planner",
92
+ json_schema_extra={"widget": WidgetType.EXPANDABLE_TEXTAREA},
93
+ )
94
+
95
+ history: bool = Field(
96
+ default=False,
97
+ title="Session history",
98
+ description="Include previous Q&A history from the current session in the context provided to the planner",
99
+ )
100
+ # Commented out for now
101
+ # planner_reasoning: bool = Field(
102
+ # default=False,
103
+ # title="Planner reasoning",
104
+ # description=(
105
+ # "Enable extended reasoning for the planner LLM. "
106
+ # "When enabled, the planner uses medium reasoning effort. "
107
+ # "Only effective if the chosen planner model supports reasoning."
108
+ # ),
109
+ # )
110
+
111
+ @field_serializer("registered_agents")
112
+ def serialize_smart_agent(
113
+ self, field: list[BaseModel]
114
+ ) -> Optional[List[Dict[str, Any]]]:
115
+ if field is None:
116
+ return field
117
+ return [agent.model_dump() for agent in field]
118
+
119
+ @field_validator("registered_agents", mode="before")
120
+ @classmethod
121
+ def is_smart_agent(cls, value: list[Dict[str, Any]]) -> list[BaseModel]:
122
+ if value is None:
123
+ return value
124
+ result = []
125
+ for agent_cfg in value:
126
+ module = agent_cfg.get("module")
127
+ if module is None:
128
+ raise ValueError("Invalid agent config: missing 'module' field")
129
+
130
+ agent_config_klass = get_agent_config_klass(module)
131
+ agent_config_instance = agent_config_klass.model_validate(agent_cfg)
132
+ result.append(agent_config_instance)
133
+ return result # type: ignore
@@ -0,0 +1,142 @@
1
+ """Prompt templates and JSON schemas for the SmartAgent."""
2
+
3
+ from typing import Any, Dict
4
+
5
+ from hyperforge import PROMPT_ENVIRONMENT
6
+
7
+ # Reactive mode
8
+
9
+ REACTIVE_SYSTEM_PROMPT_TEMPLATE = PROMPT_ENVIRONMENT.from_string(
10
+ """\
11
+ You are a smart assistant that selects tools to gather information needed to answer a user's question.
12
+ Choose the best tool or tools for the task. You may call multiple tools in one turn.
13
+ Call task_complete when you have enough information to answer the question.
14
+ {% if extra_instructions %}
15
+ Extra instructions: {{ extra_instructions }}
16
+ {% endif %}"""
17
+ )
18
+
19
+ # Plan-execute mode — planner
20
+
21
+
22
+ PLAN_EXECUTE_PLANNER_SYSTEM_PROMPT = """\
23
+ You are a strategic planning assistant that coordinates context-retrieval agents to answer a user's question.
24
+
25
+ Your job is to produce a high-level retrieval plan describing WHAT information to gather and WHY.
26
+ You do NOT call tools yourself and do NOT specify exact tool names or arguments — an executor LLM
27
+ will decide how to carry out each step using the available tools.
28
+
29
+ Guidelines:
30
+ - Analyse what information has already been gathered (execution history) and what is still missing.
31
+ - Produce a minimal, targeted plan: only include steps that will meaningfully advance towards answering the question.
32
+ - If the information gathered so far is already sufficient to answer the question, set status to "done".
33
+ - Each step should describe the information to retrieve in plain language.
34
+ - Provide a concise summary of what has been accomplished so far for the executor to use as context."""
35
+
36
+ PLAN_EXECUTE_PLANNER_PROMPT_TEMPLATE = PROMPT_ENVIRONMENT.from_string(
37
+ """\
38
+ ## User question
39
+ {{ question }}
40
+
41
+ ## Available retrieval capabilities
42
+ {{ tools_description }}
43
+
44
+ {% if session_context %}
45
+ ## Session context
46
+ Previous interactions in this session that may be relevant to the question and can be used to rephrase the question or guide retrieval:
47
+ {{ session_context }}
48
+ {% endif %}
49
+
50
+ {% if history %}
51
+ ## Execution history (previous planning iterations)
52
+ {% for entry in history %}
53
+ ### Iteration {{ loop.index }}
54
+ **Plan:** {{ entry.plan_summary }}
55
+ **Results summary:** {{ entry.results_summary }}
56
+ {% endfor %}
57
+ {% else %}
58
+ ## Execution history
59
+ No tools have been called yet.
60
+ {% endif %}
61
+
62
+ {% if extra_instructions %}
63
+ ## Extra instructions
64
+ {{ extra_instructions }}
65
+ {% endif %}
66
+
67
+ Based on the above, produce the next retrieval plan or declare completion.
68
+ """
69
+ )
70
+
71
+ PLAN_EXECUTE_PLANNER_JSON_SCHEMA: Dict[str, Any] = {
72
+ "title": "retrieval_plan",
73
+ "description": "High-level retrieval plan for the executor",
74
+ "parameters": {
75
+ "type": "object",
76
+ "properties": {
77
+ "status": {
78
+ "type": "string",
79
+ "enum": ["plan", "done"],
80
+ "description": (
81
+ "'plan' if more retrieval steps are needed, "
82
+ "'done' if enough context has been gathered to answer the question."
83
+ ),
84
+ },
85
+ "reasoning": {
86
+ "type": "string",
87
+ "description": "Explanation of why this plan was chosen or why retrieval is complete.",
88
+ },
89
+ "summary": {
90
+ "type": "string",
91
+ "description": "Concise summary of what has been gathered so far across all iterations.",
92
+ },
93
+ "steps": {
94
+ "type": "array",
95
+ "description": "Ordered list of retrieval steps for the executor. Empty when status is 'done'.",
96
+ "items": {
97
+ "type": "object",
98
+ "properties": {
99
+ "description": {
100
+ "type": "string",
101
+ "description": "Plain-language description of what information to retrieve.",
102
+ },
103
+ "reason": {
104
+ "type": "string",
105
+ "description": "Why this information is needed to answer the question.",
106
+ },
107
+ },
108
+ "required": ["description", "reason"],
109
+ "additionalProperties": False,
110
+ },
111
+ },
112
+ },
113
+ "required": ["status", "reasoning", "summary", "steps"],
114
+ "additionalProperties": False,
115
+ },
116
+ }
117
+
118
+ # Plan-execute mode — executor
119
+
120
+ PLAN_EXECUTE_EXECUTOR_SYSTEM_PROMPT_TEMPLATE = PROMPT_ENVIRONMENT.from_string(
121
+ """\
122
+ You are a retrieval executor. Your job is to call the appropriate tools to gather the information
123
+ described in the retrieval plan below.
124
+
125
+ ## User question
126
+ {{ question }}
127
+
128
+ ## What has been gathered so far
129
+ {{ summary if summary else "Nothing yet." }}
130
+
131
+ ## Retrieval plan for this iteration
132
+ {% for step in steps %}
133
+ {{ loop.index }}. {{ step.description }}
134
+ Reason: {{ step.reason }}
135
+ {% endfor %}
136
+
137
+ Call the tools needed to fulfil this plan. You may call multiple tools.
138
+ Call task_complete once you have executed all planned retrieval steps.
139
+ {% if extra_instructions %}
140
+ Extra instructions: {{ extra_instructions }}
141
+ {% endif %}"""
142
+ )
@@ -0,0 +1,19 @@
1
+ Metadata-Version: 2.4
2
+ Name: hyperforge_smart
3
+ Version: 1.0.0.post21
4
+ Summary: Conditional Hyperforge agent
5
+ Author-email: Nuclia <nucliadb@nuclia.com>
6
+ License-Expression: Apache-2.0
7
+ Project-URL: Homepage, https://progress.com
8
+ Project-URL: Repository, https://github.com/nuclia/forge
9
+ Classifier: Programming Language :: Python
10
+ Classifier: Programming Language :: Python :: 3.10
11
+ Classifier: Programming Language :: Python :: 3.11
12
+ Classifier: Programming Language :: Python :: 3.12
13
+ Classifier: Programming Language :: Python :: 3 :: Only
14
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
15
+ Requires-Python: <4,>=3.10
16
+ Description-Content-Type: text/markdown
17
+ Requires-Dist: hyperforge
18
+
19
+ # Smart Hyperforge agents
@@ -0,0 +1,8 @@
1
+ hyperforge_smart/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ hyperforge_smart/agent.py,sha256=dj9pBI00OUmyWd32Sz33mWgjmPnLx2gMW_Y9AL7CN-k,38317
3
+ hyperforge_smart/config.py,sha256=6tqAcx4uIn-huTpkJ35V_IACMsQCW_OkNkNtybjvlpQ,5372
4
+ hyperforge_smart/prompts.py,sha256=YfU9uXJCWdF1vVcMfhOitTSz9Y-zwggTG0d3nagiMWg,5050
5
+ hyperforge_smart-1.0.0.post21.dist-info/METADATA,sha256=zX9PulWXM18r1tsTXpUWe1VIQXWxfgCC1t34ytEZcpo,725
6
+ hyperforge_smart-1.0.0.post21.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
7
+ hyperforge_smart-1.0.0.post21.dist-info/top_level.txt,sha256=ZYEasRyEXV85pdgDWWKBE-JMyj8XZTP5gdgpbeR8nVU,17
8
+ hyperforge_smart-1.0.0.post21.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ hyperforge_smart