hyperforge-conditional 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.
@@ -0,0 +1,11 @@
1
+ from .context_agent import ContextConditional
2
+ from .generation_agent import GenerationConditional
3
+ from .postprocess_agent import PostprocessConditional
4
+ from .preprocess_agent import PreprocessConditional
5
+
6
+ __all__ = [
7
+ "ContextConditional",
8
+ "GenerationConditional",
9
+ "PreprocessConditional",
10
+ "PostprocessConditional",
11
+ ]
@@ -0,0 +1,235 @@
1
+ import asyncio
2
+ from time import time
3
+ from typing import Any, Dict, List, Literal, Optional, cast
4
+
5
+ from hyperforge.agent import Agent, AgentConfig
6
+ from hyperforge.configure import get_agent_config_klass, get_agent_klass
7
+ from hyperforge.manager import Manager
8
+ from hyperforge.memory.memory import QuestionMemory
9
+ from hyperforge.trace import trace_agent
10
+ from hyperforge.utils import WidgetType
11
+ from pydantic import BaseModel, Field, field_serializer, field_validator
12
+
13
+ from hyperforge import PROMPT_ENVIRONMENT
14
+
15
+ CONDITIONAL_AGENT = """
16
+ Given a prompt that indicates a condition , assess whether a given text fulfills it or not.
17
+ # PROMPT:
18
+ {{prompt}}
19
+
20
+
21
+ # TEXT:
22
+ {{text}}
23
+
24
+ {% if similarity %}
25
+ # SIMILAR QUERIES:
26
+ Similar queries that also fulfill that condition
27
+ {% for similar in similarity_examples %}
28
+ - {{similar}}
29
+ {% endfor -%}
30
+ {% endif %}
31
+
32
+ # Important rules to follow
33
+
34
+ {% for rule in rules %}
35
+ {{rule}}
36
+ {% endfor -%}
37
+
38
+ # Output definition:
39
+
40
+ Your output should always follow the following schema in a JSON format (Not markdon, please, start with b...).
41
+
42
+ {{json_schema}}
43
+ """
44
+
45
+ CONDITIONAL_AGENT_TEMPLATE = PROMPT_ENVIRONMENT.from_string(CONDITIONAL_AGENT)
46
+
47
+
48
+ CONDITIONAL_SCHEMA = {
49
+ "title": "yes_no",
50
+ "description": "Choose yes or no, depending on whether the condition is fulfilled",
51
+ "parameters": {
52
+ "type": "object",
53
+ "properties": {
54
+ "yes": {"type": "boolean"},
55
+ "reason": {"type": "string", "description": "reasoning behind the answer"},
56
+ },
57
+ },
58
+ }
59
+
60
+
61
+ class ConditionalAgentConfig(AgentConfig):
62
+ then: List[AgentConfig] = Field(
63
+ default_factory=list,
64
+ title="Then",
65
+ description="List of agents to run in case the condition is valid",
66
+ )
67
+ else_: List[AgentConfig] = Field(
68
+ default_factory=list,
69
+ title="Else",
70
+ description="List of agents to run in case the condition is not valid",
71
+ )
72
+ prompt: Optional[str] = Field(
73
+ default=None,
74
+ title="Condition Prompt",
75
+ description="Prompt to evaluate the condition",
76
+ json_schema_extra={
77
+ "show_in_node": True,
78
+ "widget": WidgetType.EXPANDABLE_TEXTAREA,
79
+ },
80
+ )
81
+ has_keywords: Optional[List[str]] = Field(
82
+ default=None,
83
+ title="Keywords",
84
+ description="List of keywords to evaluate the condition",
85
+ )
86
+ similarity: Optional[List[str]] = Field(
87
+ default=None,
88
+ title="Similar Queries",
89
+ description="List of similar queries to evaluate the condition",
90
+ )
91
+ on: Literal["QUESTION", "ANSWER", "CONTEXT"] = Field(
92
+ default="QUESTION",
93
+ title="Evaluate condition on",
94
+ description="Source to evaluate the condition on. ",
95
+ )
96
+ model: str = Field(
97
+ default="chatgpt-o3-mini",
98
+ title="Generative model",
99
+ description="Model used to assess the condition",
100
+ json_schema_extra={"widget": WidgetType.MODEL_SELECT},
101
+ )
102
+
103
+ @field_serializer("then", "else_")
104
+ def serialize_conditional_agent(
105
+ self, field: list[BaseModel]
106
+ ) -> Optional[List[Dict[str, Any]]]:
107
+ if field is None:
108
+ return field
109
+ return [agent.model_dump() for agent in field]
110
+
111
+ @field_validator("then", "else_", mode="before")
112
+ @classmethod
113
+ def is_conditional_agent(cls, value: list[Dict[str, Any]]) -> list[BaseModel]:
114
+ if value is None:
115
+ return value
116
+ result = []
117
+ for agent_cfg in value:
118
+ module = agent_cfg.get("module")
119
+ if module is None:
120
+ raise ValueError("Invalid agent config: missing 'module' field")
121
+
122
+ agent_config_klass = get_agent_config_klass(module)
123
+ agent_config_instance = agent_config_klass.model_validate(agent_cfg)
124
+ result.append(agent_config_instance)
125
+ return result # type: ignore
126
+
127
+
128
+ class Conditional:
129
+ then: Optional[list[Agent]] = None
130
+ else_: Optional[list[Agent]] = None
131
+ arag_step: str
132
+ config: Any
133
+ agent_id: Any
134
+
135
+ async def conditional_from_config(self, config: ConditionalAgentConfig):
136
+ agent_module = None
137
+ then_agents_obj_list = []
138
+ for then_agent in config.then:
139
+ if then_agent:
140
+ agent_module = then_agent.module
141
+ if agent_module is None:
142
+ raise Exception("No agent found")
143
+
144
+ agent_klass = get_agent_klass(agent_module)
145
+ # We dump and load to create the agent via from_config
146
+ then_agents_obj_list.append((agent_klass, then_agent))
147
+
148
+ agent_id = None
149
+ else_agents_obj_list = []
150
+ for else_agent in config.else_:
151
+ if else_agent is not None:
152
+ agent_id = else_agent.module
153
+ if agent_id is None:
154
+ raise Exception("No agent found")
155
+
156
+ agent_class = get_agent_klass(agent_id)
157
+ # We dump and load to create the agent via from_config
158
+ else_agents_obj_list.append((agent_class, else_agent))
159
+
160
+ self.then = await asyncio.gather(
161
+ *[klass.from_config(cfg) for klass, cfg in then_agents_obj_list]
162
+ )
163
+ self.else_ = (
164
+ await asyncio.gather(
165
+ *[klass.from_config(cfg) for klass, cfg in else_agents_obj_list]
166
+ )
167
+ if else_agents_obj_list
168
+ else None
169
+ )
170
+
171
+ async def make_decision(
172
+ self,
173
+ question: str,
174
+ memory: QuestionMemory,
175
+ manager: Manager,
176
+ title: Optional[str] = None,
177
+ ) -> bool:
178
+ t0 = time()
179
+ config: ConditionalAgentConfig = cast(ConditionalAgentConfig, self.config)
180
+
181
+ prompt = CONDITIONAL_AGENT_TEMPLATE.render(
182
+ prompt=config.prompt,
183
+ rules=memory.get_rules(),
184
+ text=question,
185
+ similarity_examples=config.similarity,
186
+ )
187
+
188
+ sources, input, output = await manager.execute_json(
189
+ prompt=prompt,
190
+ schema=CONDITIONAL_SCHEMA,
191
+ user_id="conditional",
192
+ model=config.model,
193
+ tracking=memory.get_tracking_info(),
194
+ )
195
+ condition = sources.get("yes", False)
196
+ reason = sources.get("reason")
197
+
198
+ await memory.add_step(
199
+ step_module=config.module,
200
+ step_title=f"{title}: Condition check",
201
+ step_value=str(condition),
202
+ step_reason=reason,
203
+ timeit=time() - t0,
204
+ input_nuclia_tokens=input,
205
+ output_nuclia_tokens=output,
206
+ step_agent_path=f"/{self.arag_step}/{self.agent_id}",
207
+ )
208
+ return condition
209
+
210
+ @trace_agent
211
+ async def __call__(
212
+ self,
213
+ memory: QuestionMemory,
214
+ manager: Manager,
215
+ ):
216
+ question = ""
217
+ if self.config.on == "QUESTION" and memory.actual_question:
218
+ question = memory.actual_question
219
+ elif self.config.on == "ANSWER" and memory.final_answer:
220
+ question = memory.final_answer
221
+ elif self.config.on == "CONTEXT":
222
+ question = "\n".join([x.summary for x in memory.contexts])
223
+
224
+ condition = await self.make_decision(
225
+ memory=memory,
226
+ manager=manager,
227
+ question=question,
228
+ )
229
+
230
+ if condition and self.then is not None:
231
+ for then_agent in self.then:
232
+ await then_agent(memory, manager) # type: ignore
233
+ elif condition is False and self.else_ is not None:
234
+ for else_agent in self.else_:
235
+ await else_agent(memory, manager) # type: ignore
@@ -0,0 +1,159 @@
1
+ from typing import Any, Dict, Literal, Optional, cast
2
+ from uuid import uuid4
3
+
4
+ from hyperforge.agent import Agent
5
+ from hyperforge.configure import agent
6
+ from hyperforge.context.agent import ContextAgent, trace_agent
7
+ from hyperforge.context.config import ContextAgentConfig
8
+ from hyperforge.manager import Manager
9
+ from hyperforge.memory.memory import Chunk, Context, QuestionMemory
10
+ from pydantic import Field
11
+ from pydantic.config import ConfigDict
12
+
13
+ from hyperforge_conditional.conditional import (
14
+ Conditional,
15
+ ConditionalAgentConfig,
16
+ )
17
+
18
+
19
+ class ContextConditionalAgentConfig(ConditionalAgentConfig, ContextAgentConfig):
20
+ model_config = ConfigDict(title="Condition")
21
+ module: Literal["context_conditional"] = "context_conditional"
22
+ on: Literal["QUESTION", "CONTEXT"] = Field( # type: ignore
23
+ default="QUESTION",
24
+ title="Evaluate condition on",
25
+ description="Source to evaluate the condition on. CONTEXT is only valid when this agent is used as a next agent in a chain.",
26
+ )
27
+
28
+
29
+ @agent(
30
+ id="context_conditional",
31
+ agent_type="context",
32
+ title="Context Conditional",
33
+ description="Use Context Conditional to get information from the internet to answer questions.",
34
+ config_schema=ContextConditionalAgentConfig,
35
+ )
36
+ class ContextConditional(
37
+ ContextAgent, Conditional, Agent[ContextConditionalAgentConfig]
38
+ ):
39
+ arag_step: str = "context"
40
+
41
+ async def inner_from_config(
42
+ self, config: ContextConditionalAgentConfig, agent_id: Optional[str] = None
43
+ ):
44
+ # Build then and else branches
45
+ await self.context_from_config(config)
46
+ await self.conditional_from_config(config)
47
+
48
+ async def __call__(self, memory: QuestionMemory, manager: Manager):
49
+ await Conditional.__call__(self, memory, manager)
50
+
51
+ @trace_agent
52
+ async def get_question_context(
53
+ self,
54
+ memory: QuestionMemory,
55
+ manager: Manager,
56
+ question_uuid: str,
57
+ question: str,
58
+ flow_id: str,
59
+ extra_context: Optional[Dict[str, Any]] = None,
60
+ ):
61
+ self.config: ContextConditionalAgentConfig # type: ignore
62
+ if extra_context is not None:
63
+ question, question_uuid = await self.rephrase(
64
+ memory=memory,
65
+ manager=manager,
66
+ question_uuid=question_uuid,
67
+ question=question,
68
+ contexts=extra_context,
69
+ model=self.config.rephrase_model,
70
+ module=self.config.module,
71
+ user_id="next_rephrase",
72
+ ident=self.agent_id,
73
+ )
74
+
75
+ conditional_subject = question
76
+ # Previous context only used when selected and extra context is provided - that is, the conditional comes after a next agent in the flow
77
+ if self.config.on == "CONTEXT" and extra_context is not None:
78
+ conditional_subject = "\n".join(extra_context.values())
79
+
80
+ # condition checking and branching
81
+ condition = await self.make_decision(
82
+ memory=memory,
83
+ manager=manager,
84
+ question=conditional_subject,
85
+ )
86
+ selected_agents: list[Agent] = [
87
+ agent for agent in (self.then if condition else self.else_) or []
88
+ ]
89
+ for selected_agent in selected_agents:
90
+ await cast(ContextAgent, selected_agent).get_question_context(
91
+ memory, manager, question_uuid, question, flow_id=flow_id
92
+ )
93
+ # If fallback is configured, we check if there are missing questions taking into account the executed agents contexts/summaries
94
+ if self.fallback is not None:
95
+ conditional_context = Context(
96
+ agent_id=self.agent_id,
97
+ original_question_uuid=memory.original_question_uuid,
98
+ actual_question_uuid=question_uuid,
99
+ question=question,
100
+ agent="Conditional",
101
+ title="Summarizing Conditional Agents Contexts",
102
+ source="conditional",
103
+ chunks=[],
104
+ )
105
+
106
+ for agent in selected_agents:
107
+ conditional_context.chunks.extend(
108
+ [
109
+ Chunk(
110
+ chunk_id=agent.agent_id,
111
+ text=summary,
112
+ origin_agent=self.config.module,
113
+ )
114
+ for summary in memory.get_agent_answer_summaries(
115
+ flow_id=flow_id, agent_id=agent.agent_id
116
+ )
117
+ ]
118
+ )
119
+ (
120
+ _,
121
+ missing_question,
122
+ _,
123
+ ) = await self.validate_ctx_and_answer(
124
+ memory,
125
+ manager,
126
+ conditional_context,
127
+ question=question,
128
+ )
129
+ if missing_question is not None and missing_question.strip() != "":
130
+ await self.fallback.get_question_context(
131
+ memory,
132
+ manager,
133
+ question_uuid=uuid4().hex,
134
+ question=missing_question,
135
+ flow_id=flow_id,
136
+ )
137
+
138
+ if self.next_agent is not None:
139
+ extra_context = extra_context or {}
140
+ for agent in ( # type: ignore
141
+ selected_agents + [self.fallback]
142
+ if self.fallback is not None
143
+ else selected_agents
144
+ ):
145
+ if agent:
146
+ answer_summaries = memory.get_agent_answer_summaries(
147
+ flow_id=flow_id, agent_id=agent.agent_id
148
+ )
149
+ if answer_summaries:
150
+ extra_context[agent.agent_id] = "\n".join(answer_summaries)
151
+
152
+ await self.next_agent.get_question_context(
153
+ memory,
154
+ manager,
155
+ question_uuid,
156
+ question,
157
+ extra_context=extra_context,
158
+ flow_id=flow_id,
159
+ )
@@ -0,0 +1,48 @@
1
+ from typing import List, Literal, Optional
2
+
3
+ from hyperforge.agent import Agent, AgentConfig
4
+ from hyperforge.configure import agent
5
+ from hyperforge.manager import Manager
6
+ from hyperforge.memory.memory import QuestionMemory
7
+ from pydantic import Field
8
+ from pydantic.config import ConfigDict
9
+
10
+ from hyperforge_conditional.conditional import (
11
+ Conditional,
12
+ ConditionalAgentConfig,
13
+ )
14
+
15
+
16
+ class GenerationConditionalAgentConfig(ConditionalAgentConfig):
17
+ model_config = ConfigDict(title="Condition")
18
+ module: Literal["generation_conditional"] = "generation_conditional"
19
+ then: List["AgentConfig"] = Field(
20
+ default_factory=list,
21
+ title="Then",
22
+ description="List of agents to run in case the condition is valid",
23
+ )
24
+ else_: List["AgentConfig"] = Field(
25
+ default_factory=list,
26
+ title="Else",
27
+ description="List of agents to run in case the condition is not valid",
28
+ )
29
+
30
+
31
+ @agent(
32
+ id="generation_conditional",
33
+ agent_type="generation",
34
+ title="Generation Conditional",
35
+ description="Conditional generation agent that decides which generation strategy to use based on the context.",
36
+ config_schema=GenerationConditionalAgentConfig,
37
+ )
38
+ class GenerationConditional(Agent[GenerationConditionalAgentConfig], Conditional):
39
+ arag_step: str = "generation"
40
+
41
+ async def inner_from_config(
42
+ self, config: GenerationConditionalAgentConfig, agent_id: Optional[str] = None
43
+ ):
44
+ # Build then and else branches
45
+ await self.conditional_from_config(config)
46
+
47
+ async def __call__(self, memory: QuestionMemory, manager: Manager):
48
+ await Conditional.__call__(self, memory, manager)
@@ -0,0 +1,47 @@
1
+ from typing import List, Literal, Optional
2
+
3
+ from hyperforge.agent import Agent, AgentConfig
4
+ from hyperforge.configure import agent
5
+ from hyperforge.manager import Manager
6
+ from hyperforge.memory.memory import QuestionMemory
7
+ from pydantic import Field
8
+ from pydantic.config import ConfigDict
9
+
10
+ from hyperforge_conditional.conditional import (
11
+ Conditional,
12
+ ConditionalAgentConfig,
13
+ )
14
+
15
+
16
+ class PostprocessConditionalAgentConfig(ConditionalAgentConfig):
17
+ model_config = ConfigDict(title="Condition")
18
+ module: Literal["post_conditional"] = "post_conditional"
19
+ then: List["AgentConfig"] = Field(
20
+ default_factory=list,
21
+ title="Then",
22
+ description="List of agents to run in case the condition is valid",
23
+ )
24
+ else_: List["AgentConfig"] = Field(
25
+ default_factory=list,
26
+ title="Else",
27
+ description="List of agents to run in case the condition is not valid",
28
+ )
29
+
30
+
31
+ @agent(
32
+ id="postprocess_conditional",
33
+ agent_type="postprocess",
34
+ title="Postprocess Conditional",
35
+ description="Agent that performs conditional postprocessing.",
36
+ config_schema=PostprocessConditionalAgentConfig,
37
+ )
38
+ class PostprocessConditional(Agent[PostprocessConditionalAgentConfig], Conditional):
39
+ arag_step: str = "postprocess"
40
+
41
+ async def inner_from_config(
42
+ self, config: PostprocessConditionalAgentConfig, agent_id: Optional[str] = None
43
+ ):
44
+ await self.conditional_from_config(config)
45
+
46
+ async def __call__(self, memory: QuestionMemory, manager: Manager):
47
+ await Conditional.__call__(self, memory, manager)
@@ -0,0 +1,47 @@
1
+ from typing import List, Literal, Optional
2
+
3
+ from hyperforge.agent import Agent, AgentConfig
4
+ from hyperforge.configure import agent
5
+ from hyperforge.manager import Manager
6
+ from hyperforge.memory.memory import QuestionMemory
7
+ from pydantic import Field
8
+ from pydantic.config import ConfigDict
9
+
10
+ from hyperforge_conditional.conditional import (
11
+ Conditional,
12
+ ConditionalAgentConfig,
13
+ )
14
+
15
+
16
+ class PreprocessConditionalAgentConfig(ConditionalAgentConfig):
17
+ model_config = ConfigDict(title="Condition")
18
+ module: Literal["pre_conditional"] = "pre_conditional"
19
+ then: List[AgentConfig] = Field(
20
+ default_factory=list,
21
+ title="Then",
22
+ description="List of agents to run in case the condition is valid",
23
+ )
24
+ else_: List[AgentConfig] = Field(
25
+ default_factory=list,
26
+ title="Else",
27
+ description="List of agents to run in case the condition is not valid",
28
+ )
29
+
30
+
31
+ @agent(
32
+ id="preprocess_conditional",
33
+ agent_type="preprocess",
34
+ title="Preprocess Conditional",
35
+ description="Conditional preprocessing agent that decides which preprocessing strategy to use based on the context.",
36
+ config_schema=PreprocessConditionalAgentConfig,
37
+ )
38
+ class PreprocessConditional(Agent[PreprocessConditionalAgentConfig], Conditional):
39
+ arag_step: str = "preprocess"
40
+
41
+ async def inner_from_config(
42
+ self, config: PreprocessConditionalAgentConfig, agent_id: Optional[str] = None
43
+ ):
44
+ await self.conditional_from_config(config)
45
+
46
+ async def __call__(self, memory: QuestionMemory, manager: Manager):
47
+ await Conditional.__call__(self, memory, manager)
@@ -0,0 +1,19 @@
1
+ Metadata-Version: 2.4
2
+ Name: hyperforge_conditional
3
+ Version: 1.0.0.post19
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
+ # Conditional Hyperforge agents
@@ -0,0 +1,10 @@
1
+ hyperforge_conditional/__init__.py,sha256=u0ZZtOSOL0xIypljmJjGiHMI-6G_oGcichP8aS9Mn1U,333
2
+ hyperforge_conditional/conditional.py,sha256=_dKSf_ctilRrz7pWpu4ZHgRFf59ZKu1IclFho_MdLHc,7513
3
+ hyperforge_conditional/context_agent.py,sha256=h0JkRpNoJ9yHIODXC9aqsk0inRGiWhLdiw31m89VWGE,6035
4
+ hyperforge_conditional/generation_agent.py,sha256=uKYqcyE-0vCqhlGrH3Vx9KRU_RisEVummvT9sX8PMmI,1669
5
+ hyperforge_conditional/postprocess_agent.py,sha256=6-a8IohFUM8oJvvcmVcKHHVcCCLEwCTuSUc_6sm1Nk8,1578
6
+ hyperforge_conditional/preprocess_agent.py,sha256=yuKOkwtgsGz4EhH6uzuTXjVP8vQDTF0HV7oUDpmvlUQ,1618
7
+ hyperforge_conditional-1.0.0.post19.dist-info/METADATA,sha256=GU-4xk1w7ksUeItEV2LJqoSlfYjU9awkFC57P6vMJdw,737
8
+ hyperforge_conditional-1.0.0.post19.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
9
+ hyperforge_conditional-1.0.0.post19.dist-info/top_level.txt,sha256=L7vLEjAFnCSkTy4XqjBicrm8-5rGiy99fCnuPmEllx0,23
10
+ hyperforge_conditional-1.0.0.post19.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_conditional