hyperforge 1.0.0.post19__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.
- hyperforge/__init__.py +16 -0
- hyperforge/agent.py +81 -0
- hyperforge/api/__init__.py +20 -0
- hyperforge/api/app.py +155 -0
- hyperforge/api/authentication.py +271 -0
- hyperforge/api/commands.py +33 -0
- hyperforge/api/internal/__init__.py +4 -0
- hyperforge/api/internal/inspect.py +30 -0
- hyperforge/api/internal/router.py +3 -0
- hyperforge/api/logging.py +18 -0
- hyperforge/api/models.py +129 -0
- hyperforge/api/session.py +197 -0
- hyperforge/api/settings.py +38 -0
- hyperforge/api/utils.py +354 -0
- hyperforge/api/v1/__init__.py +23 -0
- hyperforge/api/v1/agents.py +531 -0
- hyperforge/api/v1/interaction.py +430 -0
- hyperforge/api/v1/mcp_content.py +311 -0
- hyperforge/api/v1/mcp_interaction.py +322 -0
- hyperforge/api/v1/oauth.py +60 -0
- hyperforge/api/v1/prompt.py +129 -0
- hyperforge/api/v1/router.py +3 -0
- hyperforge/api/v1/schema.py +56 -0
- hyperforge/api/v1/session.py +182 -0
- hyperforge/api/v1/utils.py +12 -0
- hyperforge/api/v1/workflows.py +643 -0
- hyperforge/arag.py +28 -0
- hyperforge/broker/__init__.py +52 -0
- hyperforge/broker/local.py +116 -0
- hyperforge/broker/redis.py +161 -0
- hyperforge/configure.py +571 -0
- hyperforge/context/__init__.py +0 -0
- hyperforge/context/agent.py +377 -0
- hyperforge/context/config.py +103 -0
- hyperforge/database.py +3 -0
- hyperforge/db/__init__.py +6 -0
- hyperforge/db/agents.py +1521 -0
- hyperforge/db/encryption.py +91 -0
- hyperforge/db/exceptions.py +26 -0
- hyperforge/db/settings.py +16 -0
- hyperforge/db/workflow_cleanup.py +69 -0
- hyperforge/definition.py +13 -0
- hyperforge/driver.py +31 -0
- hyperforge/dummy.py +28 -0
- hyperforge/engine.py +189 -0
- hyperforge/exceptions.py +14 -0
- hyperforge/feature_flag.py +105 -0
- hyperforge/fixtures.py +602 -0
- hyperforge/interaction.py +116 -0
- hyperforge/llm.py +75 -0
- hyperforge/manager.py +432 -0
- hyperforge/memory/__init__.py +5 -0
- hyperforge/memory/memory.py +974 -0
- hyperforge/minimal_fixtures.py +75 -0
- hyperforge/models.py +336 -0
- hyperforge/nua.py +336 -0
- hyperforge/openapi.py +63 -0
- hyperforge/prompts.py +188 -0
- hyperforge/pubsub.py +90 -0
- hyperforge/py.typed +0 -0
- hyperforge/redis_utils.py +82 -0
- hyperforge/retrieval/__init__.py +0 -0
- hyperforge/retrieval/agent.py +169 -0
- hyperforge/retrieval/config.py +94 -0
- hyperforge/server/__init__.py +5 -0
- hyperforge/server/cache.py +131 -0
- hyperforge/server/run.py +109 -0
- hyperforge/server/sandbox.py +60 -0
- hyperforge/server/session.py +421 -0
- hyperforge/server/settings.py +47 -0
- hyperforge/server/utils.py +57 -0
- hyperforge/server/web.py +31 -0
- hyperforge/settings.py +18 -0
- hyperforge/standalone/__init__.py +5 -0
- hyperforge/standalone/agent.py +189 -0
- hyperforge/standalone/app.py +264 -0
- hyperforge/standalone/config.py +137 -0
- hyperforge/standalone/const.py +1 -0
- hyperforge/standalone/run.py +60 -0
- hyperforge/standalone/settings.py +133 -0
- hyperforge/standalone/ui_router.py +241 -0
- hyperforge/trace.py +42 -0
- hyperforge/utils/__init__.py +112 -0
- hyperforge/utils/http.py +48 -0
- hyperforge/workflows.py +44 -0
- hyperforge-1.0.0.post19.dist-info/METADATA +95 -0
- hyperforge-1.0.0.post19.dist-info/RECORD +90 -0
- hyperforge-1.0.0.post19.dist-info/WHEEL +5 -0
- hyperforge-1.0.0.post19.dist-info/entry_points.txt +8 -0
- hyperforge-1.0.0.post19.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,974 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from collections import OrderedDict
|
|
3
|
+
from datetime import datetime, timezone
|
|
4
|
+
from typing import Awaitable, Callable, Dict, List, Optional, Tuple, Union, cast
|
|
5
|
+
from uuid import uuid4
|
|
6
|
+
|
|
7
|
+
from nuclia.lib.nua_responses import Author, Image, Message
|
|
8
|
+
from nucliadb_models import (
|
|
9
|
+
InputMessage,
|
|
10
|
+
InputMessageContent,
|
|
11
|
+
MessageType,
|
|
12
|
+
)
|
|
13
|
+
from nucliadb_models.conversation import Conversation
|
|
14
|
+
from nucliadb_models.resource import (
|
|
15
|
+
Resource,
|
|
16
|
+
ResourceField,
|
|
17
|
+
)
|
|
18
|
+
from nucliadb_models.search import FindOptions, FindRequest, KnowledgeboxFindResults
|
|
19
|
+
from nucliadb_sdk.v2 import NucliaDBAsync
|
|
20
|
+
from nucliadb_sdk.v2.exceptions import NotFoundError
|
|
21
|
+
from redis.asyncio import Redis
|
|
22
|
+
|
|
23
|
+
from hyperforge.interaction import (
|
|
24
|
+
AnswerOperation,
|
|
25
|
+
AragAnswer,
|
|
26
|
+
Feedback,
|
|
27
|
+
OAuthAuthenticateURL,
|
|
28
|
+
StreamingChunk,
|
|
29
|
+
)
|
|
30
|
+
from hyperforge.models import (
|
|
31
|
+
Answer,
|
|
32
|
+
AnswerCitations,
|
|
33
|
+
Chunk,
|
|
34
|
+
Context,
|
|
35
|
+
HistoryQuestionAnswer,
|
|
36
|
+
MemoryConfig,
|
|
37
|
+
Rule,
|
|
38
|
+
Rules,
|
|
39
|
+
Source,
|
|
40
|
+
Step,
|
|
41
|
+
TrackingInfo,
|
|
42
|
+
Visualization,
|
|
43
|
+
)
|
|
44
|
+
from hyperforge.pubsub import UserToAgentInteraction
|
|
45
|
+
from hyperforge.server.cache import (
|
|
46
|
+
Cache,
|
|
47
|
+
CachedNucliaDBSource,
|
|
48
|
+
CachedSessionQA,
|
|
49
|
+
NoCache,
|
|
50
|
+
ValkeyCache,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
logger = logging.getLogger("arag.memory")
|
|
54
|
+
|
|
55
|
+
# NucliaDB storage
|
|
56
|
+
QUESTION_ANSWERS_FIELD: str = "qas"
|
|
57
|
+
CONTEXT_FIELD: str = "context"
|
|
58
|
+
STEPS_FIELD: str = "steps"
|
|
59
|
+
USER_INFO_FIELD: str = "user_info"
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class BaseSessionMemory:
|
|
63
|
+
# Session ID
|
|
64
|
+
id: str = "invalid"
|
|
65
|
+
|
|
66
|
+
agent_id: str = ""
|
|
67
|
+
workflow_id: str = ""
|
|
68
|
+
kbid: Optional[str] = None
|
|
69
|
+
|
|
70
|
+
# User information dictionary
|
|
71
|
+
# Stored as JSON field
|
|
72
|
+
user_info: Dict[str, str]
|
|
73
|
+
|
|
74
|
+
# Configuration State
|
|
75
|
+
rules: List[Union[Rule, str]]
|
|
76
|
+
|
|
77
|
+
@classmethod
|
|
78
|
+
def from_config(
|
|
79
|
+
cls, config: MemoryConfig, agent_id: str, workflow_id: str, rules: Rules
|
|
80
|
+
):
|
|
81
|
+
memory = cls(config, agent_id, workflow_id, NoCache())
|
|
82
|
+
memory.rules = rules.rules
|
|
83
|
+
return memory
|
|
84
|
+
|
|
85
|
+
def __init__(
|
|
86
|
+
self, config: MemoryConfig, agent_id: str, workflow_id: str, cache: Cache
|
|
87
|
+
):
|
|
88
|
+
self.cache = cache
|
|
89
|
+
self.agent_id = agent_id
|
|
90
|
+
self.workflow_id = workflow_id
|
|
91
|
+
self.user_info = {}
|
|
92
|
+
self.rules = []
|
|
93
|
+
nucliadb_config = config.nucliadb
|
|
94
|
+
self.kbid = nucliadb_config.kbid if nucliadb_config is not None else None
|
|
95
|
+
|
|
96
|
+
def init(self, session: str):
|
|
97
|
+
self.id = session
|
|
98
|
+
|
|
99
|
+
async def set_source(self, source: Source):
|
|
100
|
+
pass
|
|
101
|
+
|
|
102
|
+
async def get_source(self, source_id: str) -> Source | None:
|
|
103
|
+
return None
|
|
104
|
+
|
|
105
|
+
def context_user_info(self) -> str:
|
|
106
|
+
result = ""
|
|
107
|
+
for key, value in self.user_info.items():
|
|
108
|
+
result += f"- {key}: {value}\n"
|
|
109
|
+
return result
|
|
110
|
+
|
|
111
|
+
async def search_in_questions(self, question: str, all: bool):
|
|
112
|
+
return KnowledgeboxFindResults(total=0, resources={})
|
|
113
|
+
|
|
114
|
+
async def get_chat_history(self) -> List[Message]:
|
|
115
|
+
qas = await self.qa_history()
|
|
116
|
+
result = []
|
|
117
|
+
for qa in qas:
|
|
118
|
+
result.append(
|
|
119
|
+
Message(
|
|
120
|
+
author=Author.USER,
|
|
121
|
+
text=qa.question,
|
|
122
|
+
)
|
|
123
|
+
)
|
|
124
|
+
result.append(
|
|
125
|
+
Message(
|
|
126
|
+
author=Author.NUCLIA,
|
|
127
|
+
text=qa.answer,
|
|
128
|
+
)
|
|
129
|
+
)
|
|
130
|
+
return result
|
|
131
|
+
|
|
132
|
+
async def qa_history(self) -> list[HistoryQuestionAnswer]:
|
|
133
|
+
return []
|
|
134
|
+
|
|
135
|
+
async def context_history(self) -> Tuple[str, int]:
|
|
136
|
+
result = ""
|
|
137
|
+
interactions = 0
|
|
138
|
+
for qa in await self.qa_history():
|
|
139
|
+
result += f"- Question: {qa.question}\n"
|
|
140
|
+
result += f"- Answer: {qa.answer}\n"
|
|
141
|
+
interactions += 1
|
|
142
|
+
return result, interactions
|
|
143
|
+
|
|
144
|
+
def start_question(
|
|
145
|
+
self,
|
|
146
|
+
question: str,
|
|
147
|
+
actions: Optional[List[str]] = None,
|
|
148
|
+
question_id: str | None = None,
|
|
149
|
+
headers: Dict[str, str] = {},
|
|
150
|
+
arguments: Dict[str, str] = {},
|
|
151
|
+
streaming: bool = False,
|
|
152
|
+
) -> "QuestionMemory":
|
|
153
|
+
return QuestionMemory(
|
|
154
|
+
self,
|
|
155
|
+
question,
|
|
156
|
+
actions,
|
|
157
|
+
question_id=question_id,
|
|
158
|
+
headers=headers,
|
|
159
|
+
arguments=arguments,
|
|
160
|
+
streaming=streaming,
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
async def save(self, question: "QuestionMemory") -> None:
|
|
164
|
+
pass
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
class NoMemorySessionMemory(BaseSessionMemory):
|
|
168
|
+
def __init__(
|
|
169
|
+
self, config: MemoryConfig, agent_id: str, workflow_id: str, cache: Cache
|
|
170
|
+
):
|
|
171
|
+
self.cache = cache
|
|
172
|
+
self.user_info = {}
|
|
173
|
+
self.rules = []
|
|
174
|
+
self.agent_id = agent_id
|
|
175
|
+
self.workflow_id = workflow_id
|
|
176
|
+
self.debug = False
|
|
177
|
+
|
|
178
|
+
def start_question(
|
|
179
|
+
self,
|
|
180
|
+
question: str,
|
|
181
|
+
actions: Optional[List[str]] = None,
|
|
182
|
+
question_id: str | None = None,
|
|
183
|
+
headers: Dict[str, str] = {},
|
|
184
|
+
arguments: Dict[str, str] = {},
|
|
185
|
+
streaming: bool = False,
|
|
186
|
+
) -> "QuestionMemory":
|
|
187
|
+
return QuestionMemory(
|
|
188
|
+
self,
|
|
189
|
+
question,
|
|
190
|
+
actions,
|
|
191
|
+
question_id=question_id,
|
|
192
|
+
headers=headers,
|
|
193
|
+
arguments=arguments,
|
|
194
|
+
streaming=streaming,
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
async def save(self, question: "QuestionMemory") -> None:
|
|
198
|
+
pass
|
|
199
|
+
|
|
200
|
+
async def qa_history(self) -> list[HistoryQuestionAnswer]:
|
|
201
|
+
return []
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
class EphemeralSessionMemory(BaseSessionMemory):
|
|
205
|
+
@classmethod
|
|
206
|
+
def from_config(
|
|
207
|
+
cls,
|
|
208
|
+
config: MemoryConfig,
|
|
209
|
+
agent_id: str,
|
|
210
|
+
workflow_id: str,
|
|
211
|
+
rules: Rules,
|
|
212
|
+
client: Optional[Redis] = None,
|
|
213
|
+
):
|
|
214
|
+
if client is None:
|
|
215
|
+
memory = cls(config, agent_id, workflow_id, NoCache())
|
|
216
|
+
else:
|
|
217
|
+
memory = cls(config, agent_id, workflow_id, ValkeyCache(client=client))
|
|
218
|
+
|
|
219
|
+
memory.rules = rules.rules
|
|
220
|
+
return memory
|
|
221
|
+
|
|
222
|
+
def __init__(
|
|
223
|
+
self, config: MemoryConfig, agent_id: str, workflow_id: str, cache: Cache
|
|
224
|
+
):
|
|
225
|
+
self.cache = cache
|
|
226
|
+
self.agent_id = agent_id
|
|
227
|
+
self.workflow_id = workflow_id
|
|
228
|
+
self.user_info = {}
|
|
229
|
+
self.rules = []
|
|
230
|
+
self.debug = False
|
|
231
|
+
self.interactions: List[QuestionMemory] = []
|
|
232
|
+
|
|
233
|
+
async def set_source(self, source: Source):
|
|
234
|
+
entry = CachedNucliaDBSource(
|
|
235
|
+
cache=self.cache, agent_id=self.agent_id, source=source.id
|
|
236
|
+
)
|
|
237
|
+
await entry.set(source)
|
|
238
|
+
|
|
239
|
+
async def get_source(self, source_id: str) -> Source | None:
|
|
240
|
+
entry = CachedNucliaDBSource(
|
|
241
|
+
cache=self.cache, agent_id=self.agent_id, source=source_id
|
|
242
|
+
)
|
|
243
|
+
return await entry.get()
|
|
244
|
+
|
|
245
|
+
def start_question(
|
|
246
|
+
self,
|
|
247
|
+
question: str,
|
|
248
|
+
actions: Optional[List[str]] = None,
|
|
249
|
+
question_id: str | None = None,
|
|
250
|
+
headers: Dict[str, str] = {},
|
|
251
|
+
arguments: Dict[str, str] = {},
|
|
252
|
+
streaming: bool = False,
|
|
253
|
+
) -> "QuestionMemory":
|
|
254
|
+
return QuestionMemory(
|
|
255
|
+
self,
|
|
256
|
+
question,
|
|
257
|
+
actions,
|
|
258
|
+
question_id=question_id,
|
|
259
|
+
headers=headers,
|
|
260
|
+
arguments=arguments,
|
|
261
|
+
streaming=streaming,
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
async def save(self, question: "QuestionMemory") -> None:
|
|
265
|
+
self.interactions.append(question)
|
|
266
|
+
qa = HistoryQuestionAnswer(
|
|
267
|
+
question=question.original_question,
|
|
268
|
+
answer=question.final_answer or "",
|
|
269
|
+
)
|
|
270
|
+
await CachedSessionQA(self.cache, self.agent_id, self.id).append(qa)
|
|
271
|
+
|
|
272
|
+
async def qa_history(self) -> list[HistoryQuestionAnswer]:
|
|
273
|
+
cache_entry = CachedSessionQA(self.cache, self.agent_id, self.id)
|
|
274
|
+
cached_qa = await cache_entry.get()
|
|
275
|
+
if cached_qa is not None:
|
|
276
|
+
return cached_qa
|
|
277
|
+
|
|
278
|
+
return [
|
|
279
|
+
HistoryQuestionAnswer(
|
|
280
|
+
question=interaction.original_question,
|
|
281
|
+
answer=interaction.final_answer or "",
|
|
282
|
+
)
|
|
283
|
+
for interaction in self.interactions
|
|
284
|
+
]
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
class SessionMemory(BaseSessionMemory):
|
|
288
|
+
def __init__(
|
|
289
|
+
self, config: MemoryConfig, agent_id: str, workflow_id: str, cache: Cache
|
|
290
|
+
):
|
|
291
|
+
self.agent_id = agent_id
|
|
292
|
+
self.workflow_id = workflow_id
|
|
293
|
+
if config.nucliadb is not None:
|
|
294
|
+
self.url = config.nucliadb.url
|
|
295
|
+
self.key = config.nucliadb.key
|
|
296
|
+
self.kbid = config.nucliadb.kbid
|
|
297
|
+
|
|
298
|
+
if config.nucliadb.internal:
|
|
299
|
+
self.nucliadb_writer = NucliaDBAsync(
|
|
300
|
+
url=self.url.format(component="writer"),
|
|
301
|
+
api_key=self.key,
|
|
302
|
+
headers={"X-NUCLIADB-ROLES": "WRITER"},
|
|
303
|
+
)
|
|
304
|
+
self.nucliadb_reader = NucliaDBAsync(
|
|
305
|
+
url=self.url.format(component="reader"),
|
|
306
|
+
api_key=self.key,
|
|
307
|
+
headers={"X-NUCLIADB-ROLES": "READER"},
|
|
308
|
+
)
|
|
309
|
+
self.nucliadb_search = NucliaDBAsync(
|
|
310
|
+
url=self.url.format(component="search"),
|
|
311
|
+
api_key=self.key,
|
|
312
|
+
headers={"X-NUCLIADB-ROLES": "READER"},
|
|
313
|
+
)
|
|
314
|
+
else:
|
|
315
|
+
self.nucliadb_writer = NucliaDBAsync(url=self.url, api_key=self.key)
|
|
316
|
+
self.nucliadb_reader = NucliaDBAsync(url=self.url, api_key=self.key)
|
|
317
|
+
self.nucliadb_search = NucliaDBAsync(url=self.url, api_key=self.key)
|
|
318
|
+
|
|
319
|
+
self.cache = cache
|
|
320
|
+
self.user_info = {}
|
|
321
|
+
self.rules = []
|
|
322
|
+
|
|
323
|
+
def init(self, session: str):
|
|
324
|
+
self.id = session
|
|
325
|
+
|
|
326
|
+
@property
|
|
327
|
+
def debug(self):
|
|
328
|
+
return logger.level
|
|
329
|
+
|
|
330
|
+
@debug.setter
|
|
331
|
+
def debug(self, debug: bool):
|
|
332
|
+
if debug:
|
|
333
|
+
logger.setLevel(logging.INFO)
|
|
334
|
+
else:
|
|
335
|
+
logger.setLevel(logging.ERROR)
|
|
336
|
+
|
|
337
|
+
async def set_source(self, source: Source):
|
|
338
|
+
if self.kbid:
|
|
339
|
+
entry = CachedNucliaDBSource(
|
|
340
|
+
cache=self.cache, agent_id=self.kbid, source=source.id
|
|
341
|
+
)
|
|
342
|
+
await entry.set(source)
|
|
343
|
+
|
|
344
|
+
async def get_source(self, source_id: str) -> Source | None:
|
|
345
|
+
if not self.kbid:
|
|
346
|
+
return None
|
|
347
|
+
|
|
348
|
+
entry = CachedNucliaDBSource(
|
|
349
|
+
cache=self.cache, agent_id=self.kbid, source=source_id
|
|
350
|
+
)
|
|
351
|
+
return await entry.get()
|
|
352
|
+
|
|
353
|
+
async def search_in_questions(self, question: str, all: bool):
|
|
354
|
+
request = FindRequest(
|
|
355
|
+
query=question,
|
|
356
|
+
fields=["c/questions"],
|
|
357
|
+
min_score=0.9,
|
|
358
|
+
features=[FindOptions.SEMANTIC, FindOptions.KEYWORD],
|
|
359
|
+
)
|
|
360
|
+
if not all and self.id:
|
|
361
|
+
request.resource_filters = [self.id]
|
|
362
|
+
return await self.nucliadb_search.find(kbid=self.kbid, content=request)
|
|
363
|
+
|
|
364
|
+
async def qa_history(self) -> list[HistoryQuestionAnswer]:
|
|
365
|
+
if self.kbid is None:
|
|
366
|
+
return []
|
|
367
|
+
|
|
368
|
+
# Check if session Q&A is cached
|
|
369
|
+
cache_entry = CachedSessionQA(self.cache, self.kbid, self.id)
|
|
370
|
+
cached_qa = await cache_entry.get()
|
|
371
|
+
if cached_qa is not None:
|
|
372
|
+
return cached_qa
|
|
373
|
+
|
|
374
|
+
# Retrieve from memory
|
|
375
|
+
qas: list[HistoryQuestionAnswer] = []
|
|
376
|
+
try:
|
|
377
|
+
resource: Optional[
|
|
378
|
+
Resource
|
|
379
|
+
] = await self.nucliadb_reader.get_resource_by_id(
|
|
380
|
+
kbid=self.kbid, rid=self.id, query_params={"show": ["values"]}
|
|
381
|
+
)
|
|
382
|
+
except NotFoundError:
|
|
383
|
+
resource = None
|
|
384
|
+
|
|
385
|
+
if (
|
|
386
|
+
resource is not None
|
|
387
|
+
and resource.data is not None
|
|
388
|
+
and resource.data.conversations is not None
|
|
389
|
+
):
|
|
390
|
+
memory = resource.data.conversations.get(QUESTION_ANSWERS_FIELD)
|
|
391
|
+
if memory is None or memory.value is None or memory.value.pages is None:
|
|
392
|
+
raise NotImplementedError()
|
|
393
|
+
else:
|
|
394
|
+
for page_id in range(memory.value.pages):
|
|
395
|
+
page: Optional[
|
|
396
|
+
ResourceField
|
|
397
|
+
] = await self.nucliadb_reader.get_resource_field(
|
|
398
|
+
kbid=self.kbid,
|
|
399
|
+
rid=self.id,
|
|
400
|
+
field_type="conversation",
|
|
401
|
+
field_id=QUESTION_ANSWERS_FIELD,
|
|
402
|
+
query_params={"page": page_id + 1},
|
|
403
|
+
)
|
|
404
|
+
if page is not None and page.value is not None:
|
|
405
|
+
conversation_value: Conversation = cast(
|
|
406
|
+
Conversation, page.value
|
|
407
|
+
)
|
|
408
|
+
question = None
|
|
409
|
+
|
|
410
|
+
for message in conversation_value.messages or []:
|
|
411
|
+
if message.type_ == MessageType.QUESTION:
|
|
412
|
+
question = message
|
|
413
|
+
elif (
|
|
414
|
+
message.type_ == MessageType.ANSWER
|
|
415
|
+
and question is not None
|
|
416
|
+
and question.content.text
|
|
417
|
+
and message.content.text
|
|
418
|
+
):
|
|
419
|
+
qas.append(
|
|
420
|
+
HistoryQuestionAnswer(
|
|
421
|
+
question=question.content.text,
|
|
422
|
+
answer=message.content.text,
|
|
423
|
+
)
|
|
424
|
+
)
|
|
425
|
+
if len(qas) != 0:
|
|
426
|
+
await cache_entry.append_all(qas)
|
|
427
|
+
|
|
428
|
+
return qas
|
|
429
|
+
|
|
430
|
+
async def save(self, question: "QuestionMemory") -> None:
|
|
431
|
+
# # Find or create memory resource (should already be created by create_session())
|
|
432
|
+
# try:
|
|
433
|
+
# resource: Optional[
|
|
434
|
+
# Resource
|
|
435
|
+
# ] = await self.nucliadb_reader.get_resource_by_slug(
|
|
436
|
+
# kbid=self.kbid, slug=self.session
|
|
437
|
+
# )
|
|
438
|
+
# except NotFoundError:
|
|
439
|
+
# resource = None
|
|
440
|
+
|
|
441
|
+
# if resource is None:
|
|
442
|
+
# await self.nucliadb_writer.create_resource(
|
|
443
|
+
# kbid=self.kbid, content=CreateResourcePayload(slug=self.session)
|
|
444
|
+
# )
|
|
445
|
+
# resource = await self.nucliadb_reader.get_resource_by_id(
|
|
446
|
+
# kbid=self.kbid, slug=self.session
|
|
447
|
+
# )
|
|
448
|
+
|
|
449
|
+
# # Save steps
|
|
450
|
+
# steps_message = []
|
|
451
|
+
# for step in self.steps:
|
|
452
|
+
# steps_message.append(
|
|
453
|
+
# InputMessage(
|
|
454
|
+
# ident=uuid4().hex,
|
|
455
|
+
# who=step.module,
|
|
456
|
+
# type=MessageType.UNSET,
|
|
457
|
+
# content=InputMessageContent(
|
|
458
|
+
# text=step.markdown(), format=MessageFormat.KEEP_MARKDOWN
|
|
459
|
+
# ),
|
|
460
|
+
# )
|
|
461
|
+
# )
|
|
462
|
+
|
|
463
|
+
# await self.nucliadb_writer.add_conversation_message(
|
|
464
|
+
# kbid=self.kbid,
|
|
465
|
+
# rid=self.session,
|
|
466
|
+
# slug=STEPS_FIELD,
|
|
467
|
+
# content=steps_message,
|
|
468
|
+
# )
|
|
469
|
+
|
|
470
|
+
# # Save context
|
|
471
|
+
# contexts_message = []
|
|
472
|
+
# for context in self.contexts:
|
|
473
|
+
# contexts_message.append(
|
|
474
|
+
# InputMessage(
|
|
475
|
+
# ident=uuid4().hex,
|
|
476
|
+
# who="",
|
|
477
|
+
# type=MessageType.UNSET,
|
|
478
|
+
# content=InputMessageContent(
|
|
479
|
+
# text="", format=MessageFormat.KEEP_MARKDOWN
|
|
480
|
+
# ),
|
|
481
|
+
# )
|
|
482
|
+
# )
|
|
483
|
+
|
|
484
|
+
# await self.nucliadb_writer.add_conversation_message(
|
|
485
|
+
# kbid=self.kbid,
|
|
486
|
+
# rid=self.session,
|
|
487
|
+
# slug=CONTEXT_FIELD,
|
|
488
|
+
# content=contexts_message,
|
|
489
|
+
# )
|
|
490
|
+
|
|
491
|
+
# Save Q & A
|
|
492
|
+
if question.final_answer:
|
|
493
|
+
if self.kbid:
|
|
494
|
+
qa = HistoryQuestionAnswer(
|
|
495
|
+
question=question.original_question, answer=question.final_answer
|
|
496
|
+
)
|
|
497
|
+
await CachedSessionQA(self.cache, self.kbid, self.id).append(qa)
|
|
498
|
+
|
|
499
|
+
content = [
|
|
500
|
+
InputMessage(
|
|
501
|
+
who="user",
|
|
502
|
+
to=["agent"],
|
|
503
|
+
timestamp=question.started_at,
|
|
504
|
+
ident=f"q-{question.original_question_uuid}",
|
|
505
|
+
type=MessageType.QUESTION,
|
|
506
|
+
content=InputMessageContent(text=question.original_question),
|
|
507
|
+
),
|
|
508
|
+
InputMessage(
|
|
509
|
+
who="agent",
|
|
510
|
+
to=["user"],
|
|
511
|
+
timestamp=datetime.now(timezone.utc),
|
|
512
|
+
ident=f"a-{question.original_question_uuid}",
|
|
513
|
+
type=MessageType.ANSWER,
|
|
514
|
+
content=InputMessageContent(text=question.final_answer),
|
|
515
|
+
),
|
|
516
|
+
]
|
|
517
|
+
await self.nucliadb_writer.add_conversation_message(
|
|
518
|
+
kbid=self.kbid,
|
|
519
|
+
rid=self.id,
|
|
520
|
+
field_id=QUESTION_ANSWERS_FIELD,
|
|
521
|
+
content=content,
|
|
522
|
+
)
|
|
523
|
+
|
|
524
|
+
|
|
525
|
+
class QuestionMemory:
|
|
526
|
+
session: BaseSessionMemory
|
|
527
|
+
|
|
528
|
+
headers: Dict[str, str]
|
|
529
|
+
arguments: Dict[str, str]
|
|
530
|
+
|
|
531
|
+
# Main RAG Block
|
|
532
|
+
started_at: datetime
|
|
533
|
+
original_actions: List[str]
|
|
534
|
+
original_question: str
|
|
535
|
+
original_question_uuid: str
|
|
536
|
+
final_answer: Optional[str] = None
|
|
537
|
+
final_answer_citations: Optional[AnswerCitations] = None
|
|
538
|
+
final_answer_urls: List[str]
|
|
539
|
+
answers: list[tuple[str, Optional[AnswerCitations]]]
|
|
540
|
+
generated_texts: Dict[str, str]
|
|
541
|
+
# Data visualizations generated
|
|
542
|
+
data_visualizations: List[Visualization]
|
|
543
|
+
|
|
544
|
+
# Whether after generation, we consider the question answered (for example we might have generated a response but the response was "not enough data to answer this")
|
|
545
|
+
is_answered: bool = False
|
|
546
|
+
|
|
547
|
+
# Callback information to return to user
|
|
548
|
+
callback_fn: Optional[Callable[[AragAnswer], Awaitable[None]]] = None
|
|
549
|
+
feedback_fn: Optional[
|
|
550
|
+
Callable[[Feedback], Awaitable[UserToAgentInteraction | None]]
|
|
551
|
+
] = None
|
|
552
|
+
oauth_fn: Optional[Callable[[OAuthAuthenticateURL], Awaitable[None]]] = None
|
|
553
|
+
oauth_callback_fn: Optional[Callable[[str, str], Awaitable[str | None]]] = None
|
|
554
|
+
|
|
555
|
+
# Short term memory
|
|
556
|
+
steps: List[Step]
|
|
557
|
+
contexts: List[Context]
|
|
558
|
+
agent_contexts: Dict[str, Dict[str, List[Context]]]
|
|
559
|
+
|
|
560
|
+
# Flow controls
|
|
561
|
+
restart: bool = False
|
|
562
|
+
secure: Optional[bool] = None
|
|
563
|
+
streaming: bool = False
|
|
564
|
+
|
|
565
|
+
future_questions: OrderedDict[str, str]
|
|
566
|
+
context_questions: OrderedDict[str, str]
|
|
567
|
+
actual_question: Optional[str] = None
|
|
568
|
+
actual_question_uuid: Optional[str] = None
|
|
569
|
+
|
|
570
|
+
generation_rules: OrderedDict[str, str]
|
|
571
|
+
actual_action: Optional[str] = None
|
|
572
|
+
actual_action_uuid: Optional[str] = None
|
|
573
|
+
|
|
574
|
+
def __init__(
|
|
575
|
+
self,
|
|
576
|
+
session: BaseSessionMemory,
|
|
577
|
+
question: str,
|
|
578
|
+
actions: Optional[List[str]] = None,
|
|
579
|
+
question_id: str | None = None,
|
|
580
|
+
headers: Dict[str, str] | None = None,
|
|
581
|
+
arguments: Dict[str, str] | None = None,
|
|
582
|
+
streaming: bool = False,
|
|
583
|
+
):
|
|
584
|
+
self.session = session
|
|
585
|
+
self.started_at = datetime.now(timezone.utc)
|
|
586
|
+
|
|
587
|
+
# Start of a new question by the user
|
|
588
|
+
self.original_question = question
|
|
589
|
+
if actions is not None:
|
|
590
|
+
self.original_actions = actions
|
|
591
|
+
self.restart = True
|
|
592
|
+
if not question_id:
|
|
593
|
+
question_id = uuid4().hex
|
|
594
|
+
self.original_question_uuid = question_id
|
|
595
|
+
|
|
596
|
+
self.headers = headers if headers is not None else {}
|
|
597
|
+
self.arguments = arguments if arguments is not None else {}
|
|
598
|
+
self.streaming = streaming
|
|
599
|
+
self.contexts = []
|
|
600
|
+
self.steps = []
|
|
601
|
+
|
|
602
|
+
self.context_questions = OrderedDict()
|
|
603
|
+
self.original_actions = []
|
|
604
|
+
self.future_questions = OrderedDict()
|
|
605
|
+
self.answers: list[tuple[str, Optional[AnswerCitations]]] = []
|
|
606
|
+
self.generated_texts = {}
|
|
607
|
+
self.data_visualizations = []
|
|
608
|
+
self.final_answer_urls = []
|
|
609
|
+
self.agent_contexts = {}
|
|
610
|
+
|
|
611
|
+
self.set_actual_question(question, question_id)
|
|
612
|
+
|
|
613
|
+
def get_session_id(self) -> str:
|
|
614
|
+
"""Returns the session ID for the current question. The session ID is a unique identifier that is shared across all questions and interactions that belong to the same session. This can be used to group related interactions together, and to keep track of the conversation history in a coherent way."""
|
|
615
|
+
return self.session.id
|
|
616
|
+
|
|
617
|
+
def get_agent_id(self) -> str:
|
|
618
|
+
"""Returns the agent ID for the current question. The agent ID is a unique identifier that is shared across all questions and interactions that belong to the same agent. This can be used to group related interactions together, and to keep track of the conversation history in a coherent way."""
|
|
619
|
+
return self.session.agent_id
|
|
620
|
+
|
|
621
|
+
def get_workflow_id(self) -> str:
|
|
622
|
+
"""Returns the workflow ID for the current question. The workflow ID is a unique identifier that is shared across all questions and interactions that belong to the same workflow. This can be used to group related interactions together, and to keep track of the conversation history in a coherent way."""
|
|
623
|
+
return self.session.workflow_id
|
|
624
|
+
|
|
625
|
+
def context_user_info(self) -> str:
|
|
626
|
+
"""Returns a string with user information that can be used in the context of the agent. This can include information such as user preferences, user history, or any other relevant information about the user that can help the agent to generate a more personalized and accurate response."""
|
|
627
|
+
return self.session.context_user_info()
|
|
628
|
+
|
|
629
|
+
def get_rules(self) -> list[Rule | str]:
|
|
630
|
+
"""Returns the rules for the current question. The rules are a unique identifier that is shared across all questions and interactions that belong to the same set of rules. This can be used to group related interactions together, and to keep track of the conversation history in a coherent way."""
|
|
631
|
+
return self.session.rules
|
|
632
|
+
|
|
633
|
+
async def search_in_questions(
|
|
634
|
+
self, question: str, all: bool = False
|
|
635
|
+
) -> KnowledgeboxFindResults:
|
|
636
|
+
"""Searches for similar questions in the conversation history. This can be used to find relevant information that has been previously discussed in the conversation, and to provide a more accurate and personalized response."""
|
|
637
|
+
return await self.session.search_in_questions(question, all)
|
|
638
|
+
|
|
639
|
+
def user_info(self) -> Dict[str, str]:
|
|
640
|
+
"""Returns a string with user information that can be used in the context of the agent. This can include information such as user preferences, user history, or any other relevant information about the user that can help the agent to generate a more personalized and accurate response."""
|
|
641
|
+
return self.session.user_info
|
|
642
|
+
|
|
643
|
+
async def set_session_source(self, source: Source):
|
|
644
|
+
"""Sets the source of the session. This can be used to keep track of where the information in the conversation is coming from, and to provide more context to the agent when generating a response."""
|
|
645
|
+
return await self.session.set_source(source)
|
|
646
|
+
|
|
647
|
+
async def get_session_source(self, source_id: str) -> Optional[Source]:
|
|
648
|
+
"""Gets the source of the session. This can be used to keep track of where the information in the conversation is coming from, and to provide more context to the agent when generating a response."""
|
|
649
|
+
return await self.session.get_source(source_id)
|
|
650
|
+
|
|
651
|
+
async def context_history(self) -> Tuple[str, int]:
|
|
652
|
+
"""Returns a string with the context history of the conversation. This can include information such as previous questions and answers, relevant information that has been previously discussed in the conversation, or any other relevant information that can help the agent to generate a more accurate and personalized response."""
|
|
653
|
+
return await self.session.context_history()
|
|
654
|
+
|
|
655
|
+
async def get_chat_history(self) -> list[Message]:
|
|
656
|
+
"""Returns a list of tuples with the chat history of the conversation. Each tuple contains a question and an answer. This can be used to keep track of the conversation history in a more structured way, and to provide more context to the agent when generating a response."""
|
|
657
|
+
return await self.session.get_chat_history()
|
|
658
|
+
|
|
659
|
+
def stats(self):
|
|
660
|
+
return {
|
|
661
|
+
"session": self.session,
|
|
662
|
+
"contexts": [context.stats() for context in self.contexts],
|
|
663
|
+
"context_questions": self.context_questions,
|
|
664
|
+
"original_question": self.original_question,
|
|
665
|
+
"original_actions": self.original_actions,
|
|
666
|
+
"final_answer": self.final_answer,
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
async def save_context(self, flow_id: str, context: Context):
|
|
670
|
+
context.original_question_uuid = self.original_question_uuid
|
|
671
|
+
context.actual_question_uuid = self.actual_question_uuid
|
|
672
|
+
self.contexts.append(context)
|
|
673
|
+
if self.agent_contexts.get(flow_id) is None:
|
|
674
|
+
self.agent_contexts[flow_id] = {}
|
|
675
|
+
if self.agent_contexts[flow_id].get(context.agent_id) is None:
|
|
676
|
+
self.agent_contexts[flow_id][context.agent_id] = []
|
|
677
|
+
self.agent_contexts[flow_id][context.agent_id].append(context)
|
|
678
|
+
if self.callback_fn is not None:
|
|
679
|
+
await self.callback_fn(AragAnswer(context=context))
|
|
680
|
+
|
|
681
|
+
async def save_image_urls(self, image_urls: List[str]):
|
|
682
|
+
# We need to extend but deduplicate bedore extending, taking into account that we only check the part before ?eph-token
|
|
683
|
+
# TODO: in the next iteration we should handle this better, i.e. savin the token separately
|
|
684
|
+
existing_urls = {url.split("?eph-token")[0] for url in self.final_answer_urls}
|
|
685
|
+
new_urls = [
|
|
686
|
+
url for url in image_urls if url.split("?eph-token")[0] not in existing_urls
|
|
687
|
+
]
|
|
688
|
+
self.final_answer_urls.extend(new_urls)
|
|
689
|
+
|
|
690
|
+
def get_agent_contexts(self, flow_id: str, agent_id: str) -> List[Context]:
|
|
691
|
+
flow_contexts = self.agent_contexts.get(flow_id, {})
|
|
692
|
+
return flow_contexts.get(agent_id, [])
|
|
693
|
+
|
|
694
|
+
def get_agent_answer_summaries(self, flow_id: str, agent_id: str) -> List[str]:
|
|
695
|
+
flow_contexts = self.agent_contexts.get(flow_id, {})
|
|
696
|
+
contexts = flow_contexts.get(agent_id, [])
|
|
697
|
+
return [
|
|
698
|
+
context.summary for context in contexts if context.summary.strip() != ""
|
|
699
|
+
]
|
|
700
|
+
|
|
701
|
+
def list_contexts_markdown(self) -> list[str]:
|
|
702
|
+
contexts_str = []
|
|
703
|
+
for context in self.contexts:
|
|
704
|
+
result = ""
|
|
705
|
+
if context.citations_id is not None:
|
|
706
|
+
if context.title:
|
|
707
|
+
result += f"## [{context.citations_id}] {context.title}\n\n"
|
|
708
|
+
else:
|
|
709
|
+
result += f"## [{context.citations_id}]\n\n"
|
|
710
|
+
else:
|
|
711
|
+
if context.title:
|
|
712
|
+
result += f"## {context.title}\n\n"
|
|
713
|
+
result += f"{context.context_markdown()}"
|
|
714
|
+
contexts_str.append(result)
|
|
715
|
+
return contexts_str
|
|
716
|
+
|
|
717
|
+
def list_chunks_markdown(self) -> list[str]:
|
|
718
|
+
chunks_str = []
|
|
719
|
+
for context in self.contexts:
|
|
720
|
+
for chunk in context.chunks:
|
|
721
|
+
result = ""
|
|
722
|
+
if context.citations_id is not None:
|
|
723
|
+
if chunk.title:
|
|
724
|
+
result += f"## [{context.citations_id}] {chunk.title}\n\n"
|
|
725
|
+
else:
|
|
726
|
+
result += f"## [{context.citations_id}]\n\n"
|
|
727
|
+
else:
|
|
728
|
+
if chunk.title:
|
|
729
|
+
result += f"## {chunk.title}\n\n"
|
|
730
|
+
result += f"{chunk.text}\n\n"
|
|
731
|
+
chunks_str.append(result)
|
|
732
|
+
return chunks_str
|
|
733
|
+
|
|
734
|
+
def contexts_markdown(self) -> str:
|
|
735
|
+
"""
|
|
736
|
+
Returns the concatenated contexts as a single string. Includes full context (i.e: all the chunk texts)
|
|
737
|
+
"""
|
|
738
|
+
return "\n\n".join(self.list_contexts_markdown())
|
|
739
|
+
|
|
740
|
+
def list_contexts_minimal(
|
|
741
|
+
self,
|
|
742
|
+
) -> list[str]:
|
|
743
|
+
contexts_str = []
|
|
744
|
+
for context in self.contexts:
|
|
745
|
+
result = ""
|
|
746
|
+
if context.citations_id is not None:
|
|
747
|
+
if context.title:
|
|
748
|
+
result += f"## [{context.citations_id}] {context.title}\n\n"
|
|
749
|
+
else:
|
|
750
|
+
result += f"## [{context.citations_id}]\n\n"
|
|
751
|
+
else:
|
|
752
|
+
if context.title:
|
|
753
|
+
result += f"## {context.title}\n\n"
|
|
754
|
+
if context.summary.strip() != "":
|
|
755
|
+
result += f"{context.answer_summary_markdown()}"
|
|
756
|
+
else:
|
|
757
|
+
result += f"{context.context_markdown()}"
|
|
758
|
+
contexts_str.append(result)
|
|
759
|
+
return contexts_str
|
|
760
|
+
|
|
761
|
+
def contexts_minimal(self) -> str:
|
|
762
|
+
"""
|
|
763
|
+
Returns the concatenated minimal contexts as a single string.
|
|
764
|
+
Minimal contexts include summaries if available, otherwise full context (i.e: the chunk texts)
|
|
765
|
+
"""
|
|
766
|
+
return "\n\n".join(self.list_contexts_minimal())
|
|
767
|
+
|
|
768
|
+
def get_prompt_texts(self) -> List[str]:
|
|
769
|
+
prompts = []
|
|
770
|
+
for context in self.contexts:
|
|
771
|
+
for prompt in context.prompts:
|
|
772
|
+
prompts.append(prompt.render())
|
|
773
|
+
return prompts
|
|
774
|
+
|
|
775
|
+
async def add_generated_text(self, generation_id: str, generated_text: str):
|
|
776
|
+
self.generated_texts[generation_id] = generated_text
|
|
777
|
+
if self.callback_fn is not None:
|
|
778
|
+
await self.callback_fn(AragAnswer(generated_text=generated_text))
|
|
779
|
+
|
|
780
|
+
async def add_step(
|
|
781
|
+
self,
|
|
782
|
+
step_module: str,
|
|
783
|
+
step_title: str,
|
|
784
|
+
step_agent_path: str,
|
|
785
|
+
timeit: float,
|
|
786
|
+
input_nuclia_tokens: Optional[float] = None,
|
|
787
|
+
output_nuclia_tokens: Optional[float] = None,
|
|
788
|
+
step_value: Optional[str] = None,
|
|
789
|
+
step_reason: Optional[str] = None,
|
|
790
|
+
error: Optional[str] = None,
|
|
791
|
+
):
|
|
792
|
+
new_step = Step(
|
|
793
|
+
original_question_uuid=self.original_question_uuid,
|
|
794
|
+
actual_question_uuid=self.actual_question_uuid,
|
|
795
|
+
module=step_module,
|
|
796
|
+
title=step_title,
|
|
797
|
+
agent_path=step_agent_path,
|
|
798
|
+
value=step_value,
|
|
799
|
+
reason=step_reason,
|
|
800
|
+
timeit=timeit,
|
|
801
|
+
input_nuclia_tokens=input_nuclia_tokens,
|
|
802
|
+
output_nuclia_tokens=output_nuclia_tokens,
|
|
803
|
+
error=error,
|
|
804
|
+
)
|
|
805
|
+
self.steps.append(new_step)
|
|
806
|
+
if self.callback_fn is not None:
|
|
807
|
+
await self.callback_fn(AragAnswer(step=new_step))
|
|
808
|
+
|
|
809
|
+
def set_actual_question(self, question: str, uuid: Optional[str] = None):
|
|
810
|
+
self.actual_question = question
|
|
811
|
+
if uuid is None:
|
|
812
|
+
uuid = uuid4().hex
|
|
813
|
+
self.actual_question_uuid = uuid
|
|
814
|
+
# We dont need to set the question in the context
|
|
815
|
+
# This makes the original question to be added to the context questions
|
|
816
|
+
# self._set_question(question, uuid)
|
|
817
|
+
|
|
818
|
+
def _set_question(self, question: str, uuid: str):
|
|
819
|
+
self.context_questions[uuid] = question
|
|
820
|
+
|
|
821
|
+
def add_future_questions(self, questions: List[str]):
|
|
822
|
+
for question in questions:
|
|
823
|
+
self.future_questions[uuid4().hex] = question
|
|
824
|
+
|
|
825
|
+
def add_context_questions(self, questions: List[str]):
|
|
826
|
+
for question in questions:
|
|
827
|
+
self.context_questions[uuid4().hex] = question
|
|
828
|
+
|
|
829
|
+
def get_questions(self) -> List[Tuple[str, str]]:
|
|
830
|
+
"""Returns context questions if they exist, otherwise returns the original question."""
|
|
831
|
+
if len(self.context_questions) > 0:
|
|
832
|
+
return list(self.context_questions.items())
|
|
833
|
+
return [(self.original_question_uuid, self.original_question)]
|
|
834
|
+
|
|
835
|
+
async def add_answer(
|
|
836
|
+
self,
|
|
837
|
+
answer: str,
|
|
838
|
+
module: str,
|
|
839
|
+
agent_path: str,
|
|
840
|
+
citations: Optional[AnswerCitations] = None,
|
|
841
|
+
visualization: Visualization | None = None,
|
|
842
|
+
chunks: Optional[list[Chunk]] = None,
|
|
843
|
+
structured: Optional[list[str]] = None,
|
|
844
|
+
images: Optional[Dict[str, Image]] = None,
|
|
845
|
+
image_urls: Optional[list[str]] = None,
|
|
846
|
+
):
|
|
847
|
+
answer_obj = Answer(
|
|
848
|
+
answer=answer,
|
|
849
|
+
module=module,
|
|
850
|
+
agent_path=agent_path,
|
|
851
|
+
original_question_uuid=self.original_question_uuid,
|
|
852
|
+
actual_question_uuid=self.actual_question_uuid,
|
|
853
|
+
citations=citations,
|
|
854
|
+
chunks=chunks,
|
|
855
|
+
data_visualizations=[visualization] if visualization else None,
|
|
856
|
+
structured=structured,
|
|
857
|
+
images=images,
|
|
858
|
+
image_urls=image_urls,
|
|
859
|
+
)
|
|
860
|
+
self.answers.append((answer, citations))
|
|
861
|
+
if visualization is not None:
|
|
862
|
+
self.data_visualizations.append(visualization)
|
|
863
|
+
|
|
864
|
+
if self.callback_fn is not None:
|
|
865
|
+
answer_obj_to_send = AragAnswer(
|
|
866
|
+
possible_answer=answer_obj,
|
|
867
|
+
)
|
|
868
|
+
await self.callback_fn(answer_obj_to_send)
|
|
869
|
+
|
|
870
|
+
async def add_final_answer(self):
|
|
871
|
+
if len(self.answers) == 0:
|
|
872
|
+
logger.info("No answers found")
|
|
873
|
+
return
|
|
874
|
+
else:
|
|
875
|
+
answer, citations = self.answers[-1]
|
|
876
|
+
self.final_answer = answer
|
|
877
|
+
self.final_answer_citations = citations
|
|
878
|
+
|
|
879
|
+
async def send_final_answer(self):
|
|
880
|
+
if len(self.answers) == 0:
|
|
881
|
+
logger.info("No answers to send")
|
|
882
|
+
return
|
|
883
|
+
else:
|
|
884
|
+
answer, citations = self.answers[-1]
|
|
885
|
+
self.final_answer = answer
|
|
886
|
+
answer_obj = AragAnswer(
|
|
887
|
+
answer=answer,
|
|
888
|
+
original_question_uuid=self.original_question_uuid,
|
|
889
|
+
answer_citations=citations,
|
|
890
|
+
)
|
|
891
|
+
if self.callback_fn is not None:
|
|
892
|
+
await self.callback_fn(answer_obj)
|
|
893
|
+
|
|
894
|
+
def show_intermediate_steps(self):
|
|
895
|
+
pass
|
|
896
|
+
|
|
897
|
+
async def send_oauth(self, oauth: OAuthAuthenticateURL) -> None:
|
|
898
|
+
if self.oauth_fn is not None:
|
|
899
|
+
return await self.oauth_fn(oauth)
|
|
900
|
+
return None
|
|
901
|
+
|
|
902
|
+
async def send_feedback(self, feedback: Feedback) -> UserToAgentInteraction | None:
|
|
903
|
+
if self.feedback_fn is not None:
|
|
904
|
+
return await self.feedback_fn(feedback)
|
|
905
|
+
return None
|
|
906
|
+
|
|
907
|
+
async def recv_oauth_callback(
|
|
908
|
+
self, question_id: str, oauth_uuid: str
|
|
909
|
+
) -> str | None:
|
|
910
|
+
"""Receive OAuth callback credentials."""
|
|
911
|
+
if self.oauth_callback_fn is not None:
|
|
912
|
+
return await self.oauth_callback_fn(question_id, oauth_uuid)
|
|
913
|
+
return None
|
|
914
|
+
|
|
915
|
+
def set_callback_fn(self, callback: Callable[[AragAnswer], Awaitable[None]]):
|
|
916
|
+
self.callback_fn = callback
|
|
917
|
+
|
|
918
|
+
async def emit_streaming_chunk(
|
|
919
|
+
self,
|
|
920
|
+
chunk: StreamingChunk | None = None,
|
|
921
|
+
*,
|
|
922
|
+
reasoning: bool = False,
|
|
923
|
+
agent_request: str | None = None,
|
|
924
|
+
) -> None:
|
|
925
|
+
"""Emit any streaming event through the callback."""
|
|
926
|
+
if self.callback_fn is None:
|
|
927
|
+
return
|
|
928
|
+
|
|
929
|
+
answer = AragAnswer(
|
|
930
|
+
operation=AnswerOperation.REASONING
|
|
931
|
+
if reasoning
|
|
932
|
+
else AnswerOperation.ANSWER_CHUNK,
|
|
933
|
+
)
|
|
934
|
+
if agent_request is not None:
|
|
935
|
+
answer.agent_request = agent_request
|
|
936
|
+
if chunk is not None:
|
|
937
|
+
if reasoning:
|
|
938
|
+
answer.reasoning = chunk
|
|
939
|
+
else:
|
|
940
|
+
answer.streaming_response_chunk = chunk
|
|
941
|
+
await self.callback_fn(answer)
|
|
942
|
+
|
|
943
|
+
def set_oauth_fn(self, oauth: Callable[[OAuthAuthenticateURL], Awaitable[None]]):
|
|
944
|
+
self.oauth_fn = oauth
|
|
945
|
+
|
|
946
|
+
def set_oauth_callback_fn(
|
|
947
|
+
self,
|
|
948
|
+
oauth_callback: Callable[[str, str], Awaitable[str | None]],
|
|
949
|
+
):
|
|
950
|
+
self.oauth_callback_fn = oauth_callback
|
|
951
|
+
|
|
952
|
+
def set_feedback_fn(
|
|
953
|
+
self, feedback: Callable[[Feedback], Awaitable[UserToAgentInteraction | None]]
|
|
954
|
+
):
|
|
955
|
+
self.feedback_fn = feedback
|
|
956
|
+
|
|
957
|
+
async def save(self):
|
|
958
|
+
await self.session.save(self)
|
|
959
|
+
|
|
960
|
+
def get_tracking_info(self) -> TrackingInfo:
|
|
961
|
+
"""Returns tracking context for propagation to manager calls."""
|
|
962
|
+
return TrackingInfo(
|
|
963
|
+
rao_id=self.get_agent_id(),
|
|
964
|
+
session=self.get_tracking_session(),
|
|
965
|
+
message=self.original_question_uuid,
|
|
966
|
+
)
|
|
967
|
+
|
|
968
|
+
def get_tracking_session(self) -> str:
|
|
969
|
+
"""Returns a composite session key combining workflow and session IDs for activity tracking."""
|
|
970
|
+
return f"{self.get_workflow_id()}_{self.get_session_id()}"
|
|
971
|
+
|
|
972
|
+
def get_streaming(self) -> bool:
|
|
973
|
+
"""Returns whether streaming is enabled for this interaction."""
|
|
974
|
+
return self.streaming
|