hyperforge-remi 1.0.0.post20__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,3 @@
1
+ from .agent import RemiAgent
2
+
3
+ __all__ = ["RemiAgent"]
@@ -0,0 +1,177 @@
1
+ import asyncio
2
+
3
+ from hyperforge.agent import Agent
4
+ from hyperforge.configure import agent
5
+ from hyperforge.manager import Manager
6
+ from hyperforge.memory import Chunk, Context, QuestionMemory
7
+ from hyperforge.trace import trace_agent
8
+ from nuclia_models.predict.remi import RemiResponse
9
+
10
+ from hyperforge import logger
11
+ from hyperforge_remi.config import (
12
+ ContextGranularity,
13
+ RemiAgentConfig,
14
+ )
15
+
16
+ MAX_CONTEXTS_REMI = 60
17
+
18
+
19
+ @agent(
20
+ id="remi",
21
+ agent_type="postprocess",
22
+ title="REMI Evaluation",
23
+ description="Agent that performs REMI evaluation.",
24
+ config_schema=RemiAgentConfig,
25
+ )
26
+ class RemiAgent(Agent[RemiAgentConfig]):
27
+ @trace_agent
28
+ async def __call__(
29
+ self,
30
+ memory: QuestionMemory,
31
+ manager: Manager,
32
+ ):
33
+ error = None
34
+ # For each context in memory add context query, summary and answer onto a text and the initial question
35
+ if memory.final_answer is None:
36
+ raise Exception("No final answer")
37
+ if memory.original_question is None:
38
+ raise Exception("No original question")
39
+
40
+ # Only answer relevance and groundedness for now
41
+ # If we also did context relevance, we would combine into a single remi call
42
+ tasks = [
43
+ manager.remi(
44
+ question=memory.original_question,
45
+ answer=memory.final_answer,
46
+ contexts=None,
47
+ )
48
+ ]
49
+ contexts = (
50
+ memory.list_contexts_minimal()
51
+ if self.config.context_granularity == ContextGranularity.PARTIAL_ANSWERS
52
+ else memory.list_chunks_markdown()
53
+ )
54
+ if len(contexts) > MAX_CONTEXTS_REMI:
55
+ contexts = contexts[:MAX_CONTEXTS_REMI]
56
+ error = f"Too many contexts for groundedness evaluation, truncated to {MAX_CONTEXTS_REMI}. "
57
+
58
+ if memory.is_answered is True:
59
+ tasks.append(
60
+ manager.remi(
61
+ question=None,
62
+ answer=memory.final_answer,
63
+ contexts=contexts,
64
+ )
65
+ )
66
+ results = await asyncio.gather(*tasks, return_exceptions=True)
67
+ ev_answer_rel: RemiResponse | BaseException = results[0]
68
+ if memory.is_answered is True:
69
+ ev_groundedness: RemiResponse | BaseException | None = results[1]
70
+ else:
71
+ logger.info(
72
+ "Forcing REMi groundedness to 0 on question flagged as not answered"
73
+ )
74
+ ev_groundedness = RemiResponse(
75
+ groundedness=[0] * max(len(contexts), 1), time=0
76
+ )
77
+
78
+ response_str = ""
79
+ if (
80
+ isinstance(ev_answer_rel, BaseException)
81
+ or not ev_answer_rel
82
+ or not ev_answer_rel.answer_relevance
83
+ ):
84
+ msg = "Error evaluating answer relevance. "
85
+ logger.warning(
86
+ msg + str(ev_answer_rel)
87
+ if isinstance(ev_answer_rel, BaseException)
88
+ else ""
89
+ )
90
+ error = error + msg if error else msg
91
+ else:
92
+ response_str += (
93
+ f"Answer relevance: {ev_answer_rel.answer_relevance.score}/5. "
94
+ )
95
+
96
+ # Max aggregation for groundedness, could be configurable
97
+ if (
98
+ isinstance(ev_groundedness, BaseException)
99
+ or not ev_groundedness
100
+ or not ev_groundedness.groundedness
101
+ ):
102
+ msg = "Error evaluating answer groundedness. "
103
+ logger.warning(
104
+ msg + str(ev_groundedness)
105
+ if isinstance(ev_groundedness, BaseException)
106
+ else ""
107
+ )
108
+ error = error + msg if error else msg
109
+ else:
110
+ groundedness = max(
111
+ [g if g is not None else 0 for g in ev_groundedness.groundedness]
112
+ )
113
+ response_str += f"Answer groundedness: {groundedness}/5."
114
+
115
+ remi_chunks = []
116
+
117
+ # Use chunks directly if context granularity is chunk (markdown)
118
+ if self.config.context_granularity != ContextGranularity.PARTIAL_ANSWERS:
119
+ chunk_idx = 0
120
+ for context in memory.contexts:
121
+ agent_name = context.agent_id if context.agent_id else context.agent
122
+ if context.chunks:
123
+ for chunk in context.chunks:
124
+ if chunk_idx >= min(
125
+ MAX_CONTEXTS_REMI, len(ev_groundedness.groundedness)
126
+ ):
127
+ break
128
+ score = ev_groundedness.groundedness[chunk_idx]
129
+ g_score = score if score is not None else 0
130
+
131
+ c = Chunk(
132
+ chunk_id=chunk.chunk_id or f"remi_chunk_{chunk_idx}",
133
+ title=f"Groundedness {g_score}/5 - [{agent_name}] {chunk.title or 'Untitled'}",
134
+ text=f"**Groundedness: {g_score}/5**\n\n{chunk.text}",
135
+ origin_agent=self.config.module,
136
+ )
137
+ remi_chunks.append(c)
138
+ chunk_idx += 1
139
+ else:
140
+ for i, context in enumerate(memory.contexts[:MAX_CONTEXTS_REMI]):
141
+ if i < len(ev_groundedness.groundedness):
142
+ score = ev_groundedness.groundedness[i]
143
+ g_score = score if score is not None else 0
144
+ agent_name = (
145
+ context.agent_id if context.agent_id else context.agent
146
+ )
147
+
148
+ evaluated_text = (
149
+ context.answer_summary_markdown()
150
+ if context.summary.strip()
151
+ else context.context_markdown()
152
+ )
153
+ c = Chunk(
154
+ chunk_id=context.id or f"remi_{i}",
155
+ title=f"Groundedness {g_score}/5 - [{agent_name}] {context.title or 'Untitled'}",
156
+ text=f"**Groundedness: {g_score}/5**\n\n{evaluated_text}",
157
+ origin_agent=self.config.module,
158
+ )
159
+ remi_chunks.append(c)
160
+
161
+ if remi_chunks:
162
+ remi_context = Context(
163
+ original_question_uuid=memory.original_question_uuid,
164
+ actual_question_uuid=memory.actual_question_uuid,
165
+ agent="remi",
166
+ agent_id="remi",
167
+ title="REMi Evaluation Breakdown",
168
+ summary=response_str.strip()
169
+ if response_str.strip()
170
+ else f"Evaluated {len(remi_chunks)} contexts for groundedness.",
171
+ question=memory.original_question or "",
172
+ source="remi",
173
+ chunks=remi_chunks,
174
+ )
175
+ if error:
176
+ remi_context.summary += f"\nErrors: {error}"
177
+ await memory.save_context("postprocess", remi_context)
@@ -0,0 +1,23 @@
1
+ from enum import Enum
2
+ from typing import Literal
3
+
4
+ from hyperforge.agent import AgentConfig
5
+ from pydantic import Field
6
+ from pydantic.config import ConfigDict
7
+
8
+
9
+ class ContextGranularity(str, Enum):
10
+ FULL = "full"
11
+ PARTIAL_ANSWERS = "partial_answers"
12
+
13
+
14
+ class RemiAgentConfig(AgentConfig):
15
+ model_config = ConfigDict(title="REMi evaluation")
16
+ module: Literal["remi"] = "remi"
17
+ context_granularity: ContextGranularity = Field(
18
+ default=ContextGranularity.FULL,
19
+ title="Granularity of the contexts pieces",
20
+ description="Granularity of the context pieces sent to REMi for groundedness evaluation. "
21
+ "If 'partial_answers', the evaluation will use agent-level answer attempts to the question when available for a speedier analysis. "
22
+ "If 'full', the evaluation will use all individual text chunks that each agent generated for a more detailed analysis.",
23
+ )
@@ -0,0 +1,19 @@
1
+ Metadata-Version: 2.4
2
+ Name: hyperforge_remi
3
+ Version: 1.0.0.post20
4
+ Summary: Remi 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
+ # Remi Hyperforge agents
@@ -0,0 +1,7 @@
1
+ hyperforge_remi/__init__.py,sha256=eLmHV2JTI1cj_H9Xw7SAK3GnsSLfbIyUzCnbO13lO4E,54
2
+ hyperforge_remi/agent.py,sha256=eIg6y9Sci8dnL6aPZiZp4UmOkNNmkwvv2NleYOlvOAA,7175
3
+ hyperforge_remi/config.py,sha256=sWarPV5_SzGhpVrTBo46XS5TdVq3cbH2h4u9bdMvgLE,905
4
+ hyperforge_remi-1.0.0.post20.dist-info/METADATA,sha256=eeBfA1unaJyE0Lv9bRbC03A-cAbR21G_F8dbg_5dRT0,716
5
+ hyperforge_remi-1.0.0.post20.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
6
+ hyperforge_remi-1.0.0.post20.dist-info/top_level.txt,sha256=-SZGKrIg_uD9_QQD_eZwFrnvu2wb26cAEKbC1mIH07o,16
7
+ hyperforge_remi-1.0.0.post20.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_remi