hyperforge-restart 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.
- hyperforge_restart-1.0.0.post19/PKG-INFO +19 -0
- hyperforge_restart-1.0.0.post19/README.md +1 -0
- hyperforge_restart-1.0.0.post19/pyproject.toml +38 -0
- hyperforge_restart-1.0.0.post19/setup.cfg +4 -0
- hyperforge_restart-1.0.0.post19/src/hyperforge_restart/__init__.py +0 -0
- hyperforge_restart-1.0.0.post19/src/hyperforge_restart/agent.py +118 -0
- hyperforge_restart-1.0.0.post19/src/hyperforge_restart/config.py +11 -0
- hyperforge_restart-1.0.0.post19/src/hyperforge_restart.egg-info/PKG-INFO +19 -0
- hyperforge_restart-1.0.0.post19/src/hyperforge_restart.egg-info/SOURCES.txt +11 -0
- hyperforge_restart-1.0.0.post19/src/hyperforge_restart.egg-info/dependency_links.txt +1 -0
- hyperforge_restart-1.0.0.post19/src/hyperforge_restart.egg-info/requires.txt +1 -0
- hyperforge_restart-1.0.0.post19/src/hyperforge_restart.egg-info/top_level.txt +1 -0
- hyperforge_restart-1.0.0.post19/tests/test_restart.py +111 -0
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: hyperforge_restart
|
|
3
|
+
Version: 1.0.0.post19
|
|
4
|
+
Summary: Restart 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
|
+
# Rephrase Hyperforge agents
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Rephrase Hyperforge agents
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "hyperforge_restart"
|
|
7
|
+
version = "1.0.0.post19"
|
|
8
|
+
license = "Apache-2.0"
|
|
9
|
+
description = "Restart 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
|
+
# these dependencies are actually in the src/ folder (under
|
|
25
|
+
# src/nucliadb_utils/tests/), but only used when the module is imported
|
|
26
|
+
dev = [
|
|
27
|
+
"pytest",
|
|
28
|
+
"pytest-benchmark",
|
|
29
|
+
"pytest-docker-fixtures>=1.4.2",
|
|
30
|
+
"pytest-lazy-fixtures",
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
[project.urls]
|
|
34
|
+
Homepage = "https://progress.com"
|
|
35
|
+
Repository = "https://github.com/nuclia/forge"
|
|
36
|
+
|
|
37
|
+
[tool.pytest.ini_options]
|
|
38
|
+
asyncio_mode = "auto"
|
|
File without changes
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
from time import time
|
|
2
|
+
|
|
3
|
+
from hyperforge.agent import Agent
|
|
4
|
+
from hyperforge.exceptions import MaxRetries
|
|
5
|
+
from hyperforge.manager import Manager
|
|
6
|
+
from hyperforge.memory import QuestionMemory
|
|
7
|
+
from hyperforge.trace import trace_agent
|
|
8
|
+
|
|
9
|
+
from hyperforge_restart.config import RestartAgentConfig
|
|
10
|
+
from hyperforge import PROMPT_ENVIRONMENT, logger
|
|
11
|
+
|
|
12
|
+
REPHRASE_PROMPT = """
|
|
13
|
+
You are an expert assistant helping to answer complex questions using the provided context and any partial answers already available.
|
|
14
|
+
Your goal is to identify only the missing information needed to fully answer the main question.
|
|
15
|
+
|
|
16
|
+
Instructions:
|
|
17
|
+
1. Carefully review the main question and the context, which may include partial answers or relevant details.
|
|
18
|
+
2. If the main question is not fully answered by the context, generate only the specific sub-questions or clarifications needed to obtain the missing information.
|
|
19
|
+
3. Briefly explain why each new question is necessary.
|
|
20
|
+
4. If the context is sufficient to answer the main question, simply state that no additional questions are needed. Fill the questions list with an empty string. And answer true in the answered parameter.
|
|
21
|
+
Be concise and focus on actionable, relevant questions that address only the gaps in information.
|
|
22
|
+
|
|
23
|
+
# Main Question:
|
|
24
|
+
{{question}}
|
|
25
|
+
|
|
26
|
+
#Context (may include partial answers or supporting information):
|
|
27
|
+
{{context}}
|
|
28
|
+
"""
|
|
29
|
+
JSON_SCHEMA = {
|
|
30
|
+
"title": "questions",
|
|
31
|
+
"description": (
|
|
32
|
+
"An object containing a list of specific sub-questions or clarifications needed "
|
|
33
|
+
"to fully answer the main question, and a brief explanation for each. "
|
|
34
|
+
"Focus only on actionable, relevant questions that address gaps in information."
|
|
35
|
+
),
|
|
36
|
+
"type": "object",
|
|
37
|
+
"properties": {
|
|
38
|
+
"questions": {
|
|
39
|
+
"type": "array",
|
|
40
|
+
"items": {
|
|
41
|
+
"type": "string",
|
|
42
|
+
"description": "A concise, actionable sub-question or clarification needed to fill a gap in the answer.",
|
|
43
|
+
},
|
|
44
|
+
"description": "List of missing sub-questions or clarifications required to fully answer the main question.",
|
|
45
|
+
},
|
|
46
|
+
"reason": {
|
|
47
|
+
"type": "string",
|
|
48
|
+
"description": "A brief explanation of why these questions are necessary to complete the answer.",
|
|
49
|
+
},
|
|
50
|
+
"answered": {
|
|
51
|
+
"type": "boolean",
|
|
52
|
+
"description": "Indicates whether the main question can be answered with the provided context.",
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
"required": ["questions"],
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
REPHRASE_PROMPT_TEMPLATE = PROMPT_ENVIRONMENT.from_string(REPHRASE_PROMPT)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class RestartAgent(Agent[RestartAgentConfig]):
|
|
62
|
+
@trace_agent
|
|
63
|
+
async def __call__(
|
|
64
|
+
self,
|
|
65
|
+
memory: QuestionMemory,
|
|
66
|
+
manager: Manager,
|
|
67
|
+
):
|
|
68
|
+
# check how many restart options
|
|
69
|
+
|
|
70
|
+
retry = 0
|
|
71
|
+
for step in memory.steps:
|
|
72
|
+
if step.module == "restart":
|
|
73
|
+
retry += 1
|
|
74
|
+
|
|
75
|
+
question = memory.original_question
|
|
76
|
+
|
|
77
|
+
prompt = REPHRASE_PROMPT_TEMPLATE.render(
|
|
78
|
+
context=memory.contexts_minimal(), question=question
|
|
79
|
+
)
|
|
80
|
+
t0 = time()
|
|
81
|
+
# Ask the LLM to define which information is needed to answer.
|
|
82
|
+
information, input, output = await manager.execute_json(
|
|
83
|
+
model=self.config.model,
|
|
84
|
+
user_id="rephrase",
|
|
85
|
+
prompt=prompt,
|
|
86
|
+
schema=JSON_SCHEMA,
|
|
87
|
+
tracking=memory.get_tracking_info(),
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
# retrieve all paragraphs and ask to rephrase on the NUA API
|
|
91
|
+
new_questions = information.get("questions", [])
|
|
92
|
+
reason = information.get("reason", [])
|
|
93
|
+
answered = information.get("answered", False)
|
|
94
|
+
|
|
95
|
+
if retry >= self.config.retries and not answered:
|
|
96
|
+
error_message = (
|
|
97
|
+
f"Maximum retries reached ({self.config.retries}). "
|
|
98
|
+
"Unable to generate new questions to answer the original question."
|
|
99
|
+
)
|
|
100
|
+
logger.error(error_message)
|
|
101
|
+
raise MaxRetries(error_message)
|
|
102
|
+
if answered:
|
|
103
|
+
logger.info("No additional questions needed to answer the main question.")
|
|
104
|
+
memory.restart = False
|
|
105
|
+
return
|
|
106
|
+
await memory.add_step(
|
|
107
|
+
step_module="restart",
|
|
108
|
+
step_title=self.step_title("Rephrase"),
|
|
109
|
+
step_value=f"New questions: {', '.join(new_questions)}",
|
|
110
|
+
step_reason=reason,
|
|
111
|
+
timeit=time() - t0,
|
|
112
|
+
step_agent_path=f"/postprocess/{self.config.id if self.config.id else 'default'}",
|
|
113
|
+
input_nuclia_tokens=input,
|
|
114
|
+
output_nuclia_tokens=output,
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
memory.add_context_questions(new_questions)
|
|
118
|
+
memory.restart = True
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
from typing import Literal
|
|
2
|
+
|
|
3
|
+
from hyperforge.agent import AgentConfig
|
|
4
|
+
from pydantic.config import ConfigDict
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class RestartAgentConfig(AgentConfig):
|
|
8
|
+
model_config = ConfigDict(title="Restart")
|
|
9
|
+
module: Literal["restart"] = "restart"
|
|
10
|
+
model: str
|
|
11
|
+
retries: int = 2
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: hyperforge_restart
|
|
3
|
+
Version: 1.0.0.post19
|
|
4
|
+
Summary: Restart 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
|
+
# Rephrase Hyperforge agents
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
src/hyperforge_restart/__init__.py
|
|
4
|
+
src/hyperforge_restart/agent.py
|
|
5
|
+
src/hyperforge_restart/config.py
|
|
6
|
+
src/hyperforge_restart.egg-info/PKG-INFO
|
|
7
|
+
src/hyperforge_restart.egg-info/SOURCES.txt
|
|
8
|
+
src/hyperforge_restart.egg-info/dependency_links.txt
|
|
9
|
+
src/hyperforge_restart.egg-info/requires.txt
|
|
10
|
+
src/hyperforge_restart.egg-info/top_level.txt
|
|
11
|
+
tests/test_restart.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
hyperforge
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
hyperforge_restart
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
from unittest.mock import AsyncMock, MagicMock
|
|
2
|
+
from uuid import uuid4
|
|
3
|
+
|
|
4
|
+
import pytest
|
|
5
|
+
from hyperforge.exceptions import MaxRetries
|
|
6
|
+
from hyperforge.manager import Manager
|
|
7
|
+
from hyperforge.memory.memory import EphemeralSessionMemory
|
|
8
|
+
from hyperforge.models import MemoryConfig, Rules
|
|
9
|
+
from hyperforge_restart.agent import RestartAgent
|
|
10
|
+
from hyperforge_restart.config import RestartAgentConfig
|
|
11
|
+
|
|
12
|
+
pytestmark = pytest.mark.asyncio
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def make_memory(restart_steps: int = 0):
|
|
16
|
+
session = EphemeralSessionMemory.from_config(
|
|
17
|
+
config=MemoryConfig(), agent_id="test", workflow_id="test", rules=Rules()
|
|
18
|
+
)
|
|
19
|
+
session.init("test-session")
|
|
20
|
+
memory = session.start_question("What is the capital of France?")
|
|
21
|
+
# Inject fake restart steps into memory
|
|
22
|
+
for _ in range(restart_steps):
|
|
23
|
+
step = MagicMock()
|
|
24
|
+
step.module = "restart"
|
|
25
|
+
memory.steps.append(step)
|
|
26
|
+
return memory
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def make_manager(answered: bool, questions: list[str] | None = None):
|
|
30
|
+
manager = MagicMock(spec=Manager)
|
|
31
|
+
manager.execute_json = AsyncMock(
|
|
32
|
+
return_value=(
|
|
33
|
+
{
|
|
34
|
+
"questions": questions or ["What language do they speak in France?"],
|
|
35
|
+
"reason": "Need to confirm language spoken.",
|
|
36
|
+
"answered": answered,
|
|
37
|
+
},
|
|
38
|
+
10,
|
|
39
|
+
20,
|
|
40
|
+
)
|
|
41
|
+
)
|
|
42
|
+
return manager
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
async def test_restart_agent_answered():
|
|
46
|
+
"""When the LLM says the question is already answered, restart should be False and no step added."""
|
|
47
|
+
config = RestartAgentConfig(model="gpt-4o", retries=2)
|
|
48
|
+
agent = RestartAgent(config=config)
|
|
49
|
+
memory = make_memory(restart_steps=0)
|
|
50
|
+
manager = make_manager(answered=True)
|
|
51
|
+
|
|
52
|
+
await agent(memory=memory, manager=manager)
|
|
53
|
+
|
|
54
|
+
assert memory.restart is False
|
|
55
|
+
assert not any(s.module == "restart" for s in memory.steps)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
async def test_restart_agent_not_answered_first_retry():
|
|
59
|
+
"""When not answered and retries not exhausted, a restart step should be added."""
|
|
60
|
+
config = RestartAgentConfig(model="gpt-4o", retries=2)
|
|
61
|
+
agent = RestartAgent(config=config)
|
|
62
|
+
memory = make_memory(restart_steps=0)
|
|
63
|
+
manager = make_manager(answered=False, questions=["What language do they speak?"])
|
|
64
|
+
|
|
65
|
+
await agent(memory=memory, manager=manager)
|
|
66
|
+
|
|
67
|
+
assert memory.restart is True
|
|
68
|
+
restart_steps = [s for s in memory.steps if s.module == "restart"]
|
|
69
|
+
assert len(restart_steps) == 1
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
async def test_restart_agent_not_answered_last_allowed_retry():
|
|
73
|
+
"""On the last allowed retry (retry < retries), a restart step should still be added."""
|
|
74
|
+
config = RestartAgentConfig(model="gpt-4o", retries=2)
|
|
75
|
+
agent = RestartAgent(config=config)
|
|
76
|
+
# One restart step already in memory, retries=2 so retry=1 < 2
|
|
77
|
+
memory = make_memory(restart_steps=1)
|
|
78
|
+
manager = make_manager(answered=False, questions=["Another clarification?"])
|
|
79
|
+
|
|
80
|
+
await agent(memory=memory, manager=manager)
|
|
81
|
+
|
|
82
|
+
assert memory.restart is True
|
|
83
|
+
restart_steps = [s for s in memory.steps if s.module == "restart"]
|
|
84
|
+
assert len(restart_steps) == 2
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
async def test_restart_agent_max_retries_raises():
|
|
88
|
+
"""When retries are exhausted and question is not answered, MaxRetries should be raised."""
|
|
89
|
+
config = RestartAgentConfig(model="gpt-4o", retries=2)
|
|
90
|
+
agent = RestartAgent(config=config)
|
|
91
|
+
# Two restart steps already means retry == retries
|
|
92
|
+
memory = make_memory(restart_steps=2)
|
|
93
|
+
manager = make_manager(answered=False)
|
|
94
|
+
|
|
95
|
+
with pytest.raises(MaxRetries):
|
|
96
|
+
await agent(memory=memory, manager=manager)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
async def test_restart_agent_context_questions_updated():
|
|
100
|
+
"""New questions from the LLM should be added to memory context questions."""
|
|
101
|
+
config = RestartAgentConfig(model="gpt-4o", retries=3)
|
|
102
|
+
agent = RestartAgent(config=config)
|
|
103
|
+
memory = make_memory(restart_steps=0)
|
|
104
|
+
new_questions = ["What year did it become the capital?", "What is the population?"]
|
|
105
|
+
manager = make_manager(answered=False, questions=new_questions)
|
|
106
|
+
|
|
107
|
+
await agent(memory=memory, manager=manager)
|
|
108
|
+
|
|
109
|
+
stored_questions = list(memory.context_questions.values())
|
|
110
|
+
for q in new_questions:
|
|
111
|
+
assert q in stored_questions
|