hyperforge-generate 1.0.0.post24__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_generate
3
+ Version: 1.0.0.post24
4
+ Summary: External 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
+ # Generate Hyperforge agents
@@ -0,0 +1 @@
1
+ # Generate Hyperforge agents
@@ -0,0 +1,40 @@
1
+ [build-system]
2
+ requires = ["setuptools"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "hyperforge_generate"
7
+ version = "1.0.0.post24"
8
+ license = "Apache-2.0"
9
+ description = "External 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
+ ]
34
+
35
+ [project.urls]
36
+ Homepage = "https://progress.com"
37
+ Repository = "https://github.com/nuclia/forge"
38
+
39
+ [tool.pytest.ini_options]
40
+ asyncio_mode = "auto"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,101 @@
1
+ from time import time
2
+ from uuid import uuid4
3
+
4
+ from hyperforge.agent import Agent
5
+ from hyperforge.configure import agent
6
+ from hyperforge.manager import Manager
7
+ from hyperforge.memory import QuestionMemory
8
+ from hyperforge.trace import trace_agent
9
+ from nuclia.lib.nua_responses import ChatModel, UserPrompt
10
+
11
+ from hyperforge import PROMPT_ENVIRONMENT
12
+ from hyperforge_generate.config import GenerateAgentConfig
13
+
14
+ GENERATE_PROMPT = """
15
+ Generate the following request using **only** the provided context. Refrain from incorporating any outside knowledge. If the context is insufficient to answer the question comprehensively, respond with: "Not enough data to generate this."
16
+
17
+
18
+ {% if rules -%}
19
+ # Generation Rules
20
+ {% for rule in rules -%}
21
+ - {{rule}}
22
+ {% endfor -%}
23
+ {% endif -%}
24
+
25
+ {{prompt}}
26
+
27
+ {{context}}
28
+
29
+ MAIN QUESTION: {{question}}
30
+
31
+ # Notes
32
+ - Use the context provided without being overly selective.
33
+ - Please try to generate if possible, even if it requires to make a bit of a deduction.
34
+ - If they are images attached, look through them carefully.
35
+ - For rules related to charts or images, pay extra attention to extracting details and interpreting data presented visually. Think about it carefully before giving inaccurate interpretations
36
+
37
+ """
38
+
39
+ GENERATE_PROMPT_TEMPLATE = PROMPT_ENVIRONMENT.from_string(GENERATE_PROMPT)
40
+
41
+
42
+ @agent(
43
+ id="generate",
44
+ agent_type="generation",
45
+ title="Generate Answer",
46
+ description="Generate answers based on provided context.",
47
+ config_schema=GenerateAgentConfig,
48
+ )
49
+ class GenerateAgent(Agent[GenerateAgentConfig]):
50
+ __root_agent__ = True
51
+
52
+ @trace_agent
53
+ async def __call__(
54
+ self,
55
+ memory: QuestionMemory,
56
+ manager: Manager,
57
+ ):
58
+ # For each context in memory add context query, summary and answer onto a text and the initial question
59
+ prompt = GENERATE_PROMPT_TEMPLATE.render(
60
+ question=memory.original_question,
61
+ context=memory.contexts_minimal(),
62
+ prompt=self.config.prompt,
63
+ rules=memory.generation_rules,
64
+ )
65
+ t0 = time()
66
+
67
+ chat_model = ChatModel(
68
+ user_id="generate",
69
+ question="",
70
+ user_prompt=UserPrompt(prompt=prompt),
71
+ format_prompt=False,
72
+ generative_model=self.config.model,
73
+ max_tokens=2000,
74
+ tracking=memory.get_tracking_info(),
75
+ )
76
+
77
+ agent_path = f"/generation/{self.config.id if self.config.id else 'default'}"
78
+ resp, input_tokens, output_tokens = await manager.execute_raw(
79
+ chat_model, memory=memory, module="generate", agent_path=agent_path
80
+ )
81
+
82
+ generated_text = resp.answer or ""
83
+
84
+ memory.generated_texts[uuid4().hex] = generated_text
85
+
86
+ await memory.add_generated_text(
87
+ self.config.id if self.config.id else "default", generated_text
88
+ )
89
+ # We assume that JSON generation always produces an answer
90
+ memory.is_answered = True
91
+
92
+ await memory.add_step(
93
+ step_module=self.config.module,
94
+ step_title=self.step_title("Generate"),
95
+ step_value="",
96
+ step_reason="",
97
+ step_agent_path=agent_path,
98
+ timeit=time() - t0,
99
+ input_nuclia_tokens=input_tokens,
100
+ output_nuclia_tokens=output_tokens,
101
+ )
@@ -0,0 +1,26 @@
1
+ from typing import Literal, Optional
2
+
3
+ from hyperforge.agent import AgentConfig
4
+ from hyperforge.utils import WidgetType
5
+ from pydantic import Field
6
+ from pydantic.config import ConfigDict
7
+
8
+
9
+ class GenerateAgentConfig(AgentConfig):
10
+ model_config = ConfigDict(title="Generate")
11
+ module: Literal["generate"] = "generate"
12
+ prompt: Optional[str] = Field(
13
+ None,
14
+ json_schema_extra={
15
+ "show_in_node": True,
16
+ "widget": WidgetType.EXPANDABLE_TEXTAREA,
17
+ },
18
+ )
19
+ model: str = Field(
20
+ default="chatgpt-azure-4o-mini",
21
+ title="Generative model",
22
+ description="Model used to generate the response",
23
+ json_schema_extra={"widget": WidgetType.MODEL_SELECT},
24
+ )
25
+ images: bool = False
26
+ generate_image: bool = False
@@ -0,0 +1,19 @@
1
+ Metadata-Version: 2.4
2
+ Name: hyperforge_generate
3
+ Version: 1.0.0.post24
4
+ Summary: External 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
+ # Generate Hyperforge agents
@@ -0,0 +1,11 @@
1
+ README.md
2
+ pyproject.toml
3
+ src/hyperforge_generate/__init__.py
4
+ src/hyperforge_generate/agent.py
5
+ src/hyperforge_generate/config.py
6
+ src/hyperforge_generate.egg-info/PKG-INFO
7
+ src/hyperforge_generate.egg-info/SOURCES.txt
8
+ src/hyperforge_generate.egg-info/dependency_links.txt
9
+ src/hyperforge_generate.egg-info/requires.txt
10
+ src/hyperforge_generate.egg-info/top_level.txt
11
+ tests/test_generate.py
@@ -0,0 +1,83 @@
1
+ from collections import OrderedDict
2
+ from unittest.mock import AsyncMock, MagicMock
3
+
4
+ import pytest
5
+ from hyperforge.manager import Manager
6
+ from hyperforge.memory import Chunk, Context
7
+ from hyperforge.memory.memory import EphemeralSessionMemory
8
+ from hyperforge.models import MemoryConfig, Rules
9
+ from hyperforge_generate.agent import GenerateAgent
10
+ from hyperforge_generate.config import GenerateAgentConfig
11
+
12
+ pytestmark = pytest.mark.asyncio
13
+
14
+
15
+ async def test_generate_agent():
16
+ # 1. Create a mocked manager
17
+ manager = MagicMock(spec=Manager)
18
+
19
+ # Mock execute_raw response.
20
+ # resp must be an object with an `answer` attribute.
21
+ resp_mock = MagicMock()
22
+ resp_mock.answer = "This is a generated response based on context."
23
+ manager.execute_raw = AsyncMock(return_value=(resp_mock, 10, 20))
24
+
25
+ # 2. Create the configuration for GenerateAgent
26
+ config = GenerateAgentConfig(
27
+ prompt="Synthesize an answer.", model="chatgpt-azure-4o-mini"
28
+ )
29
+
30
+ # 3. Create the GenerateAgent instance
31
+ agent = GenerateAgent(config=config)
32
+
33
+ # 4. Set up EphemeralSessionMemory and QuestionMemory
34
+ session = EphemeralSessionMemory.from_config(
35
+ config=MemoryConfig(), agent_id="test", workflow_id="test", rules=Rules()
36
+ )
37
+ session.init("test-session")
38
+ memory = session.start_question("What is the capital of France?")
39
+
40
+ # Let's add some contexts and rules to memory to verify prompt rendering
41
+ context_obj = Context(
42
+ id="context-1",
43
+ original_question_uuid="question-1",
44
+ actual_question_uuid="question-1",
45
+ question="What is the capital of France?",
46
+ source="test-source",
47
+ agent="static",
48
+ title="Static Context",
49
+ chunks=[Chunk(chunk_id="chunk-1", text="The capital of France is Paris.")],
50
+ )
51
+ memory.contexts.append(context_obj)
52
+
53
+ # Add generation rules
54
+ memory.generation_rules = OrderedDict([("Only use the context.", "")])
55
+
56
+ # 5. Call the agent
57
+ await agent(memory=memory, manager=manager)
58
+
59
+ # 6. Assertions
60
+ # Verify execute_raw was called with expected arguments
61
+ assert manager.execute_raw.called
62
+ call_args = manager.execute_raw.call_args[0]
63
+ chat_model = call_args[0]
64
+
65
+ # Verify that prompt template rendered correctly containing context, question, and rules
66
+ assert "The capital of France is Paris." in chat_model.user_prompt.prompt
67
+ assert "What is the capital of France?" in chat_model.user_prompt.prompt
68
+ assert "Only use the context." in chat_model.user_prompt.prompt
69
+ assert "Synthesize an answer." in chat_model.user_prompt.prompt
70
+
71
+ # Verify the generated text was added to memory
72
+ assert memory.is_answered is True
73
+ assert len(memory.generated_texts) == 2
74
+ for val in memory.generated_texts.values():
75
+ assert val == "This is a generated response based on context."
76
+
77
+ # Verify a step was added
78
+ assert len(memory.steps) == 1
79
+ step = memory.steps[0]
80
+ assert step.module == "generate"
81
+ assert step.title == "Generate: Generate"
82
+ assert step.input_nuclia_tokens == 10
83
+ assert step.output_nuclia_tokens == 20