hyperforge-remi 1.0.0.post20__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_remi-1.0.0.post20/PKG-INFO +19 -0
- hyperforge_remi-1.0.0.post20/README.md +1 -0
- hyperforge_remi-1.0.0.post20/pyproject.toml +40 -0
- hyperforge_remi-1.0.0.post20/setup.cfg +4 -0
- hyperforge_remi-1.0.0.post20/src/hyperforge_remi/__init__.py +3 -0
- hyperforge_remi-1.0.0.post20/src/hyperforge_remi/agent.py +177 -0
- hyperforge_remi-1.0.0.post20/src/hyperforge_remi/config.py +23 -0
- hyperforge_remi-1.0.0.post20/src/hyperforge_remi.egg-info/PKG-INFO +19 -0
- hyperforge_remi-1.0.0.post20/src/hyperforge_remi.egg-info/SOURCES.txt +11 -0
- hyperforge_remi-1.0.0.post20/src/hyperforge_remi.egg-info/dependency_links.txt +1 -0
- hyperforge_remi-1.0.0.post20/src/hyperforge_remi.egg-info/requires.txt +1 -0
- hyperforge_remi-1.0.0.post20/src/hyperforge_remi.egg-info/top_level.txt +1 -0
- hyperforge_remi-1.0.0.post20/tests/test_remi.py +236 -0
|
@@ -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 @@
|
|
|
1
|
+
# Remi Hyperforge agents
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "hyperforge_remi"
|
|
7
|
+
version = "1.0.0.post20"
|
|
8
|
+
license = "Apache-2.0"
|
|
9
|
+
description = "Remi 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,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,11 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
src/hyperforge_remi/__init__.py
|
|
4
|
+
src/hyperforge_remi/agent.py
|
|
5
|
+
src/hyperforge_remi/config.py
|
|
6
|
+
src/hyperforge_remi.egg-info/PKG-INFO
|
|
7
|
+
src/hyperforge_remi.egg-info/SOURCES.txt
|
|
8
|
+
src/hyperforge_remi.egg-info/dependency_links.txt
|
|
9
|
+
src/hyperforge_remi.egg-info/requires.txt
|
|
10
|
+
src/hyperforge_remi.egg-info/top_level.txt
|
|
11
|
+
tests/test_remi.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
hyperforge
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
hyperforge_remi
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import re
|
|
3
|
+
|
|
4
|
+
import pytest
|
|
5
|
+
from hyperforge.engine import main as arag_main
|
|
6
|
+
from hyperforge.minimal_fixtures import cassette_nua_key
|
|
7
|
+
from hyperforge_remi.config import ContextGranularity
|
|
8
|
+
|
|
9
|
+
NUA_KEY = os.environ.get(
|
|
10
|
+
"NUA_KEY",
|
|
11
|
+
) or cassette_nua_key("https://europe-1.nuclia.cloud/")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
pytestmark = [
|
|
15
|
+
pytest.mark.vcr(ignore_localhost=True, ignore_hosts=["europe-1.nuclia.cloud"]),
|
|
16
|
+
pytest.mark.asyncio,
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
ANSWERED_QUESTION = "What is Progress Agentic RAG?"
|
|
20
|
+
UNANSWERED_QUESTION = (
|
|
21
|
+
"Como usar max_magic y dime como cambiará este parametro en el futuro"
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
CONFIG = {
|
|
25
|
+
"drivers": [
|
|
26
|
+
{
|
|
27
|
+
"provider": "nucliadb",
|
|
28
|
+
"identifier": "nuclia-docs",
|
|
29
|
+
"name": "nuclia-docs",
|
|
30
|
+
"config": {
|
|
31
|
+
"url": "https://europe-1.nuclia.cloud/api",
|
|
32
|
+
"manager": "https://europe-1.nuclia.cloud/api",
|
|
33
|
+
"kbid": "df8b4c24-2807-4888-ad6c-ae97357a638b",
|
|
34
|
+
"key": "eyJhbGciOiJSUzI1NiIsImtpZCI6InNhIiwidHlwIjoiSldUIn0.eyJpc3MiOiJodHRwczovL2V1cm9wZS0xLm51Y2xpYS5jbG91ZC8iLCJpYXQiOjE3NTkyNTQ3NjQsInN1YiI6ImUwNGUwMzcyLTYwNDgtNDY5ZC04NWExLWI1ZTM1MjBmMzdlZiIsImp0aSI6IjgzMzRhY2NlLTIwMTUtNGY0MS05M2U5LTczNzk1MTg2NDdiZiIsImV4cCI6MTc5MDc5MDc2MCwia2V5IjoiMmYxMzYyNTItNjNiMy00NzA1LTg1MjQtMDhmYWJjOWUzMjUyIiwia2lkIjoiN2RmMzY2NDctOTdiOC00NzU0LWExNjUtZWZkY2ZlMDRkMzI2In0.kwHAfx9RRTI-G3S64X0iisr0iAyXRKNRhnN4C67MkLSxeu1AOAnVV8EIQuu4jpXW7O4FkSsthFXEv9ZxlRRh_CaS0z_TjPzIzDPeE6eIKskZ70Q7c-pDe949WE9DZiDyy9_dwKsdX5cnvYpKorp0ROm-GvRXrdHaTZKDSYWht3gvEtm6-0j9C1gx2BzKr2coizUAIde_qjSpLOojO4S-k8P8I9dsQFagdcrjxgGWgrAzjhAs_qkqlRmP0QP6S7ToN0nrbHmtKKb0lWmcpVvlAfH95CM20YUs7IAqU_t7-_V6mm43FstRgGeiHkoapo8nPVJtXMBSlaM7GSz0Kxf2TWQwi94mTEQLdA8CblX0skMCfIHFwbcbm1Vf-2C6LywAsSmTYAwsVPpqeQcVZdrfLMhddCjZKUFCNLSurCSb4TuN79GZicPCJDT-VEBMlNH8ayHOyRib5RyqvgXUwGN9zyM-ma7RrVk4eEwSk7923bn_9GTk-s5tYw_exbYsQ1Qa84GA6NzgJ_kNQmgJwb2zW1V5ddCpYd5k6lNEdPRk0JQKlCC2zTmSvnRcLxfDPi4SZFdLLdtG0j2hIl_QNTEC_3VtqJds4FMofy7TkmUObdbEmXjdAsOxkqj2ntGOsaBNiCI_w47BbPvG_V1LsBHDrrIo0Wo1fgAhUbtWV7Dd5J4",
|
|
35
|
+
"filters": [],
|
|
36
|
+
"description": "Documentation of the Nuclia API, recipies, reference",
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
],
|
|
40
|
+
"rules": {
|
|
41
|
+
"rules": [
|
|
42
|
+
{
|
|
43
|
+
"prompt": "Be polite",
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
"prompt": "The documentation of Nuclia is hosted at https://docs.nuclia.dev",
|
|
47
|
+
},
|
|
48
|
+
]
|
|
49
|
+
},
|
|
50
|
+
"memory": {},
|
|
51
|
+
"workflow": {
|
|
52
|
+
"id": "default",
|
|
53
|
+
"name": "Default workflow",
|
|
54
|
+
"description": "Default workflow for testing",
|
|
55
|
+
"parameters": {},
|
|
56
|
+
},
|
|
57
|
+
"preprocess": [],
|
|
58
|
+
"context": [
|
|
59
|
+
{
|
|
60
|
+
"module": "basic_ask",
|
|
61
|
+
"title": "Nuclia Docs Retrieval Agent",
|
|
62
|
+
"sources": ["nuclia-docs"],
|
|
63
|
+
"prune_context": False,
|
|
64
|
+
},
|
|
65
|
+
],
|
|
66
|
+
"generation": [
|
|
67
|
+
{"module": "summarize", "title": "Summarize agent"},
|
|
68
|
+
],
|
|
69
|
+
"postprocess": [{"module": "remi"}],
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
# Match on body since we send parallel requests and otherwise they get played back in a different order
|
|
74
|
+
@pytest.mark.vcr(match_on=["method", "scheme", "host", "port", "path", "query", "body"])
|
|
75
|
+
@pytest.mark.parametrize(
|
|
76
|
+
"granularity", (ContextGranularity.PARTIAL_ANSWERS, ContextGranularity.FULL)
|
|
77
|
+
)
|
|
78
|
+
async def test_remi(granularity: ContextGranularity):
|
|
79
|
+
CONFIG["postprocess"][0] = {"module": "remi", "context_granularity": granularity} # type: ignore
|
|
80
|
+
question_memory = await arag_main(
|
|
81
|
+
agent_id="default",
|
|
82
|
+
internal_nua=False,
|
|
83
|
+
external_nua_api_key=NUA_KEY,
|
|
84
|
+
question=ANSWERED_QUESTION,
|
|
85
|
+
config=CONFIG,
|
|
86
|
+
loaded_modules=[
|
|
87
|
+
"hyperforge_remi",
|
|
88
|
+
"hyperforge_summarize",
|
|
89
|
+
"hyperforge_nucliadb",
|
|
90
|
+
],
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
assert question_memory.final_answer is not None
|
|
94
|
+
# verify the step logic is actually validating the context summary instead
|
|
95
|
+
remi_contexts = [ctx for ctx in question_memory.contexts if ctx.agent == "remi"]
|
|
96
|
+
assert len(remi_contexts) > 0, "Missing REMi structured context"
|
|
97
|
+
remi_ctx = remi_contexts[-1]
|
|
98
|
+
assert remi_ctx.title == "REMi Evaluation Breakdown"
|
|
99
|
+
|
|
100
|
+
assert remi_ctx.summary is not None
|
|
101
|
+
relevance_match = re.search(r"Answer relevance: (\d+)/5\.", remi_ctx.summary)
|
|
102
|
+
assert relevance_match is not None, remi_ctx.summary
|
|
103
|
+
relevance_score = int(relevance_match.group(1))
|
|
104
|
+
assert relevance_score > 1, f"Relevance score {relevance_score} should be > 1"
|
|
105
|
+
|
|
106
|
+
groundedness_match = re.search(r"Answer groundedness: (\d+)/5\.", remi_ctx.summary)
|
|
107
|
+
assert groundedness_match is not None, remi_ctx.summary
|
|
108
|
+
groundedness_score = int(groundedness_match.group(1))
|
|
109
|
+
assert groundedness_score > 1, (
|
|
110
|
+
f"Groundedness score {groundedness_score} should be > 1"
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
assert len(remi_ctx.chunks) > 0, "Missing evaluated chunks in REMi context"
|
|
114
|
+
assert remi_ctx.chunks[0].title is not None
|
|
115
|
+
assert "Groundedness" in remi_ctx.chunks[0].title, (
|
|
116
|
+
f"Missing score in title: {remi_ctx.chunks[0].title}"
|
|
117
|
+
)
|
|
118
|
+
assert "**Groundedness:" in remi_ctx.chunks[0].text, (
|
|
119
|
+
"Missing score text inside chunk text"
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
if granularity == ContextGranularity.FULL:
|
|
123
|
+
# original text chunks check
|
|
124
|
+
original_chunks = sum(
|
|
125
|
+
len(ctx.chunks) for ctx in question_memory.contexts if ctx.agent != "remi"
|
|
126
|
+
)
|
|
127
|
+
assert len(remi_ctx.chunks) == original_chunks
|
|
128
|
+
|
|
129
|
+
orig_ids = {
|
|
130
|
+
c.chunk_id
|
|
131
|
+
for ctx in question_memory.contexts
|
|
132
|
+
if ctx.agent != "remi"
|
|
133
|
+
for c in ctx.chunks
|
|
134
|
+
if c.chunk_id
|
|
135
|
+
}
|
|
136
|
+
for chunk in remi_ctx.chunks:
|
|
137
|
+
assert chunk.chunk_id in orig_ids or "remi_chunk_" in (chunk.chunk_id or "")
|
|
138
|
+
else:
|
|
139
|
+
# One chunk per context evaluated
|
|
140
|
+
ctx_list = [ctx for ctx in question_memory.contexts if ctx.agent != "remi"]
|
|
141
|
+
original_contexts = min(len(ctx_list), 60)
|
|
142
|
+
assert len(remi_ctx.chunks) == original_contexts, (
|
|
143
|
+
f"remi_ctx: {len(remi_ctx.chunks)}, orig: {original_contexts}. ctx_list: {[c.agent for c in ctx_list]}"
|
|
144
|
+
)
|
|
145
|
+
orig_ids = {c.id for c in ctx_list if c.id}
|
|
146
|
+
for chunk in remi_ctx.chunks:
|
|
147
|
+
assert chunk.chunk_id in orig_ids or (chunk.chunk_id or "").startswith(
|
|
148
|
+
"remi_"
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
assert "Errors:" not in remi_ctx.summary
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
@pytest.mark.vcr(match_on=["method", "scheme", "host", "port", "path", "query", "body"])
|
|
155
|
+
@pytest.mark.parametrize(
|
|
156
|
+
"granularity", [ContextGranularity.PARTIAL_ANSWERS, ContextGranularity.FULL]
|
|
157
|
+
)
|
|
158
|
+
async def test_remi_not_enough_data(granularity: ContextGranularity):
|
|
159
|
+
CONFIG["postprocess"][0] = {"module": "remi", "context_granularity": granularity} # type: ignore
|
|
160
|
+
question_memory = await arag_main(
|
|
161
|
+
agent_id="default",
|
|
162
|
+
internal_nua=False,
|
|
163
|
+
external_nua_api_key=NUA_KEY,
|
|
164
|
+
question=UNANSWERED_QUESTION,
|
|
165
|
+
config=CONFIG,
|
|
166
|
+
loaded_modules=[
|
|
167
|
+
"hyperforge_remi",
|
|
168
|
+
"hyperforge_summarize",
|
|
169
|
+
"hyperforge_nucliadb",
|
|
170
|
+
],
|
|
171
|
+
)
|
|
172
|
+
assert question_memory.final_answer is not None
|
|
173
|
+
assert "not enough data to answer this" in question_memory.final_answer.lower()
|
|
174
|
+
assert question_memory.is_answered is False
|
|
175
|
+
|
|
176
|
+
remi_contexts = [ctx for ctx in question_memory.contexts if ctx.agent == "remi"]
|
|
177
|
+
# If there are context chunks, REMi generates a structured context
|
|
178
|
+
original_chunks = sum(
|
|
179
|
+
len(ctx.chunks) for ctx in question_memory.contexts if ctx.agent != "remi"
|
|
180
|
+
)
|
|
181
|
+
if original_chunks > 0:
|
|
182
|
+
assert len(remi_contexts) > 0, "Missing REMi structured context"
|
|
183
|
+
remi_ctx = remi_contexts[-1]
|
|
184
|
+
assert remi_ctx.title == "REMi Evaluation Breakdown"
|
|
185
|
+
|
|
186
|
+
assert remi_ctx.summary is not None
|
|
187
|
+
relevance_match = re.search(r"Answer relevance: (\d+)/5\.", remi_ctx.summary)
|
|
188
|
+
assert relevance_match is not None, remi_ctx.summary
|
|
189
|
+
relevance_score = int(relevance_match.group(1))
|
|
190
|
+
assert relevance_score < 1, f"Relevance score {relevance_score} should be < 1"
|
|
191
|
+
|
|
192
|
+
groundedness_match = re.search(
|
|
193
|
+
r"Answer groundedness: (\d+)/5\.", remi_ctx.summary
|
|
194
|
+
)
|
|
195
|
+
assert groundedness_match is not None, remi_ctx.summary
|
|
196
|
+
groundedness_score = int(groundedness_match.group(1))
|
|
197
|
+
assert groundedness_score == 0, (
|
|
198
|
+
f"Groundedness score {groundedness_score} should be 0 when the question is not answered"
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
assert len(remi_ctx.chunks) > 0, "Missing evaluated chunks in REMi context"
|
|
202
|
+
assert remi_ctx.chunks[0].title is not None
|
|
203
|
+
assert "Groundedness" in remi_ctx.chunks[0].title, (
|
|
204
|
+
f"Missing score in title: {remi_ctx.chunks[0].title}"
|
|
205
|
+
)
|
|
206
|
+
assert "**Groundedness:" in remi_ctx.chunks[0].text, (
|
|
207
|
+
"Missing score text inside chunk text"
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
if granularity == ContextGranularity.FULL:
|
|
211
|
+
assert len(remi_ctx.chunks) == original_chunks
|
|
212
|
+
orig_ids = {
|
|
213
|
+
c.chunk_id
|
|
214
|
+
for ctx in question_memory.contexts
|
|
215
|
+
if ctx.agent != "remi"
|
|
216
|
+
for c in ctx.chunks
|
|
217
|
+
if c.chunk_id
|
|
218
|
+
}
|
|
219
|
+
for chunk in remi_ctx.chunks:
|
|
220
|
+
assert chunk.chunk_id in orig_ids or "remi_chunk_" in (
|
|
221
|
+
chunk.chunk_id or ""
|
|
222
|
+
)
|
|
223
|
+
else:
|
|
224
|
+
# One chunk per context evaluated
|
|
225
|
+
ctx_list = [ctx for ctx in question_memory.contexts if ctx.agent != "remi"]
|
|
226
|
+
original_contexts = min(len(ctx_list), 60)
|
|
227
|
+
assert len(remi_ctx.chunks) == original_contexts, (
|
|
228
|
+
f"remi_ctx: {len(remi_ctx.chunks)}, orig: {original_contexts}. ctx_list: {[c.agent for c in ctx_list]}"
|
|
229
|
+
)
|
|
230
|
+
orig_ids = {c.id for c in ctx_list if c.id}
|
|
231
|
+
for chunk in remi_ctx.chunks:
|
|
232
|
+
assert chunk.chunk_id in orig_ids or (chunk.chunk_id or "").startswith(
|
|
233
|
+
"remi_"
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
assert "Errors:" not in remi_ctx.summary
|