hyperforge-conditional 1.0.0.post19__tar.gz

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,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 @@
1
+ # Conditional Hyperforge agents
@@ -0,0 +1,43 @@
1
+ [build-system]
2
+ requires = ["setuptools"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "hyperforge_conditional"
7
+ version = "1.0.0.post19"
8
+ license = "Apache-2.0"
9
+ description = "Conditional Hyperforge agent"
10
+ authors = [{ name = "Nuclia", email = "nucliadb@nuclia.com" }]
11
+ readme = "README.md"
12
+ classifiers = [
13
+ "Programming Language :: Python",
14
+ "Programming Language :: Python :: 3.10",
15
+ "Programming Language :: Python :: 3.11",
16
+ "Programming Language :: Python :: 3.12",
17
+ "Programming Language :: Python :: 3 :: Only",
18
+ "Topic :: Software Development :: Libraries :: Python Modules",
19
+ ]
20
+ requires-python = ">=3.10, <4"
21
+ dependencies = ["hyperforge"]
22
+
23
+ [dependency-groups]
24
+ dev = [
25
+ "pytest",
26
+ "pytest-benchmark",
27
+ "pytest-docker-fixtures>=1.4.2",
28
+ "pytest-lazy-fixtures",
29
+ "pytest-recording",
30
+ "pytest-asyncio",
31
+ "pytest-cov",
32
+ "pytest-mock",
33
+ "respx",
34
+ "hyperforge_static",
35
+ "hyperforge_summarize",
36
+ ]
37
+
38
+ [project.urls]
39
+ Homepage = "https://progress.com"
40
+ Repository = "https://github.com/nuclia/forge"
41
+
42
+ [tool.pytest.ini_options]
43
+ asyncio_mode = "auto"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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,14 @@
1
+ README.md
2
+ pyproject.toml
3
+ src/hyperforge_conditional/__init__.py
4
+ src/hyperforge_conditional/conditional.py
5
+ src/hyperforge_conditional/context_agent.py
6
+ src/hyperforge_conditional/generation_agent.py
7
+ src/hyperforge_conditional/postprocess_agent.py
8
+ src/hyperforge_conditional/preprocess_agent.py
9
+ src/hyperforge_conditional.egg-info/PKG-INFO
10
+ src/hyperforge_conditional.egg-info/SOURCES.txt
11
+ src/hyperforge_conditional.egg-info/dependency_links.txt
12
+ src/hyperforge_conditional.egg-info/requires.txt
13
+ src/hyperforge_conditional.egg-info/top_level.txt
14
+ tests/test_conditional.py
@@ -0,0 +1,303 @@
1
+ import os
2
+ from copy import deepcopy
3
+
4
+ import pytest
5
+ from hyperforge.engine import main as arag_main
6
+ from hyperforge.minimal_fixtures import cassette_nua_key
7
+
8
+ NUA_KEY = os.environ.get(
9
+ "NUA_KEY",
10
+ ) or cassette_nua_key("https://europe-1.nuclia.cloud/")
11
+
12
+ CONFIG = {
13
+ "drivers": [],
14
+ "rules": {
15
+ "rules": [
16
+ {"prompt": "Be polite"},
17
+ {
18
+ "prompt": "The documentation of Nuclia is hosted at https://docs.nuclia.dev"
19
+ },
20
+ ]
21
+ },
22
+ "memory": {},
23
+ "workflow": {
24
+ "id": "default",
25
+ "name": "Default workflow",
26
+ "description": "Default workflow for testing",
27
+ "parameters": {},
28
+ },
29
+ "preprocess": [],
30
+ "context": [
31
+ {
32
+ "module": "context_conditional",
33
+ "title": "",
34
+ "prompt": "The question is about Atlas",
35
+ "then": [
36
+ {
37
+ "module": "static",
38
+ "title": "Static Agent",
39
+ "context": "Atlas is Carmen's dog. He is small and very cute but a bit moody. He was a rescue dog.",
40
+ }
41
+ ],
42
+ "else_": [
43
+ {
44
+ "module": "static",
45
+ "title": "Static Agent",
46
+ "context": "RAO is a product of Progress agentic RAG, it is a powerful agent orchestrator that you are using right now.",
47
+ }
48
+ ],
49
+ },
50
+ ],
51
+ "generation": [
52
+ {"module": "summarize"},
53
+ ],
54
+ "postprocess": [],
55
+ }
56
+
57
+
58
+ @pytest.mark.asyncio
59
+ @pytest.mark.vcr(ignore_localhost=True)
60
+ async def test_conditional_static():
61
+ question_memory = await arag_main(
62
+ agent_id="default",
63
+ internal_nua=False,
64
+ external_nua_api_key=NUA_KEY,
65
+ question="Who is Atlas?",
66
+ config=CONFIG,
67
+ user_metadata={"fullname": "Carmen", "age": "99"},
68
+ loaded_modules=[
69
+ "hyperforge_conditional",
70
+ "hyperforge_static",
71
+ "hyperforge_summarize",
72
+ ],
73
+ )
74
+
75
+ keywords = ["Atlas", "Carmen", "dog"]
76
+
77
+ assert question_memory.final_answer
78
+ assert all(
79
+ keyword.lower() in question_memory.final_answer.lower() for keyword in keywords
80
+ )
81
+
82
+ question_memory = await arag_main(
83
+ agent_id="default",
84
+ internal_nua=False,
85
+ external_nua_api_key=NUA_KEY,
86
+ question="What is RAO?",
87
+ config=CONFIG,
88
+ user_metadata={"fullname": "Carmen", "age": "99"},
89
+ loaded_modules=[
90
+ "hyperforge_conditional",
91
+ "hyperforge_static",
92
+ "hyperforge_summarize",
93
+ ],
94
+ )
95
+
96
+ keywords = ["RAO", "powerful", "agent", "orchestrator"]
97
+
98
+ assert question_memory.final_answer
99
+ assert all(
100
+ keyword.lower() in question_memory.final_answer.lower() for keyword in keywords
101
+ )
102
+
103
+
104
+ AGENTS_CONTEXT_CONDITIONAL = {
105
+ "context": [
106
+ {
107
+ "module": "static",
108
+ "title": "Static Agent",
109
+ "context": "The best cardamom buns are in Denmark, the most famous place is a bakery called Juno.",
110
+ "next_agent": {
111
+ "module": "context_conditional",
112
+ "title": "",
113
+ "prompt": "The context mentions Juno",
114
+ "on": "CONTEXT",
115
+ "then": [
116
+ {
117
+ "module": "static",
118
+ "title": "Static Agent",
119
+ "context": "Juno is a bakery in Denmark famous for its cardamom buns. They are the best in the world. The also have a wide variety of other delicious pastries. And great coffee.",
120
+ }
121
+ ],
122
+ "else_": [
123
+ {
124
+ "module": "static",
125
+ "title": "Static Agent",
126
+ "context": "You can find good cardamom buns in many places. Some people say the best ones are in Fabrique in New York, but that is subjective. The best way to find good cardamom buns is to try them in different bakeries and see which one you like the most.",
127
+ }
128
+ ],
129
+ },
130
+ },
131
+ ],
132
+ "generation": [
133
+ {"module": "summarize"},
134
+ ],
135
+ }
136
+
137
+
138
+ @pytest.mark.asyncio
139
+ @pytest.mark.vcr(ignore_localhost=True)
140
+ async def test_conditional_context():
141
+ config = deepcopy(CONFIG)
142
+ config["context"] = AGENTS_CONTEXT_CONDITIONAL["context"]
143
+ config["generation"] = AGENTS_CONTEXT_CONDITIONAL["generation"]
144
+ question_memory = await arag_main(
145
+ agent_id="default",
146
+ internal_nua=False,
147
+ external_nua_api_key=NUA_KEY,
148
+ question="Where can I find good cardamom buns? give me as much info as possible",
149
+ config=config,
150
+ user_metadata={"fullname": "Carmen", "age": "99"},
151
+ loaded_modules=[
152
+ "hyperforge_conditional",
153
+ "hyperforge_static",
154
+ "hyperforge_summarize",
155
+ ],
156
+ )
157
+
158
+ assert (
159
+ question_memory.final_answer and "juno" in question_memory.final_answer.lower()
160
+ )
161
+ config["context"][0]["context"] = (
162
+ "Cardamom buns are a type of sweet roll that is flavored with cardamom. They are popular in many countries, including Sweden, Denmark, and Finland. They are often enjoyed with coffee or tea."
163
+ )
164
+
165
+ question_memory = await arag_main(
166
+ agent_id="default",
167
+ internal_nua=False,
168
+ external_nua_api_key=NUA_KEY,
169
+ question="Where can I find good cardamom buns?",
170
+ config=config,
171
+ user_metadata={"fullname": "Carmen", "age": "99"},
172
+ loaded_modules=[
173
+ "hyperforge_conditional",
174
+ "hyperforge_static",
175
+ "hyperforge_summarize",
176
+ ],
177
+ )
178
+
179
+ assert question_memory.final_answer
180
+
181
+ assert "juno" not in question_memory.final_answer.lower()
182
+
183
+
184
+ AGENTS_FALLBACK = {
185
+ "context": [
186
+ {
187
+ "module": "context_conditional",
188
+ "title": "",
189
+ "prompt": "The question is about Atlas",
190
+ "then": [
191
+ {
192
+ "module": "static",
193
+ "title": "Static Agent",
194
+ "context": "Atlas is Carmen's dog. He is small and very cute but a bit moody. He was a rescue dog.",
195
+ }
196
+ ],
197
+ "else_": [
198
+ {
199
+ "module": "static",
200
+ "title": "Static Agent",
201
+ "context": "RAO is a product of Progress agentic RAG, it is a powerful agent orchestrator that you are using right now.",
202
+ }
203
+ ],
204
+ "fallback": {
205
+ "module": "static",
206
+ "title": "Fallback Agent",
207
+ "context": "You are probably asking about where to find the best cookies in the world. The best cookies are the ones made at home with love, but if you want to buy them, you can find great cookies in many bakeries around the world. Some famous ones are Levain Bakery in New York, Lune Croissanterie in Melbourne, and Maison Pichard in Paris.",
208
+ },
209
+ },
210
+ ],
211
+ "generation": [
212
+ {"module": "summarize"},
213
+ ],
214
+ }
215
+
216
+
217
+ @pytest.mark.asyncio
218
+ @pytest.mark.vcr(ignore_localhost=True)
219
+ async def test_conditional_fallback():
220
+ config = deepcopy(CONFIG)
221
+ config["context"] = AGENTS_FALLBACK["context"]
222
+ config["generation"] = AGENTS_FALLBACK["generation"]
223
+ question_memory = await arag_main(
224
+ agent_id="default",
225
+ internal_nua=False,
226
+ external_nua_api_key=NUA_KEY,
227
+ question="Where to have the best cookies?",
228
+ config=config,
229
+ user_metadata={"fullname": "Carmen", "age": "99"},
230
+ loaded_modules=[
231
+ "hyperforge_conditional",
232
+ "hyperforge_static",
233
+ "hyperforge_summarize",
234
+ ],
235
+ )
236
+
237
+ keywords = ["cookies", "Levain Bakery", "Lune Croissanterie", "Maison Pichard"]
238
+
239
+ assert question_memory.final_answer
240
+ assert all(
241
+ keyword.lower() in question_memory.final_answer.lower() for keyword in keywords
242
+ )
243
+
244
+
245
+ AGENTS_FALLBACK_2 = {
246
+ "context": [
247
+ {
248
+ "module": "context_conditional",
249
+ "title": "",
250
+ "prompt": "The question is about Atlas",
251
+ "then": [
252
+ {
253
+ "module": "static",
254
+ "title": "Static Agent",
255
+ "context": "Atlas is Carmen's dog. He is small and very cute but a bit moody. He was a rescue dog.",
256
+ }
257
+ ],
258
+ "else_": [
259
+ {
260
+ "module": "static",
261
+ "title": "Static Agent",
262
+ "context": "RAO is a product of Progress agentic RAG, it is a powerful agent orchestrator that you are using right now.",
263
+ "fallback": {
264
+ "module": "static",
265
+ "title": "Fallback Agent",
266
+ "context": "You are probably asking about where to find the best cookies in the world. The best cookies are the ones made at home with love, but if you want to buy them, you can find great cookies in many bakeries around the world. Some famous ones are Levain Bakery in New York, Lune Croissanterie in Melbourne, and Maison Pichard in Paris.",
267
+ },
268
+ },
269
+ ],
270
+ },
271
+ ],
272
+ "generation": [
273
+ {"module": "summarize"},
274
+ ],
275
+ }
276
+
277
+
278
+ @pytest.mark.asyncio
279
+ @pytest.mark.vcr(ignore_localhost=True)
280
+ async def test_conditional_fallback_second_level():
281
+ config = deepcopy(CONFIG)
282
+ config["context"] = AGENTS_FALLBACK["context"]
283
+ config["generation"] = AGENTS_FALLBACK["generation"]
284
+ question_memory = await arag_main(
285
+ agent_id="default",
286
+ internal_nua=False,
287
+ external_nua_api_key=NUA_KEY,
288
+ question="Where to have the best cookies?",
289
+ config=config,
290
+ user_metadata={"fullname": "Carmen", "age": "99"},
291
+ loaded_modules=[
292
+ "hyperforge_conditional",
293
+ "hyperforge_static",
294
+ "hyperforge_summarize",
295
+ ],
296
+ )
297
+
298
+ keywords = ["cookies", "Levain Bakery", "Lune Croissanterie", "Maison Pichard"]
299
+
300
+ assert question_memory.final_answer
301
+ assert all(
302
+ keyword.lower() in question_memory.final_answer.lower() for keyword in keywords
303
+ )