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
hyperforge/fixtures.py
ADDED
|
@@ -0,0 +1,602 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import logging
|
|
3
|
+
import os
|
|
4
|
+
import pathlib
|
|
5
|
+
import socket
|
|
6
|
+
from unittest.mock import patch
|
|
7
|
+
from uuid import uuid4
|
|
8
|
+
|
|
9
|
+
import alembic.command
|
|
10
|
+
import alembic.config
|
|
11
|
+
import databases
|
|
12
|
+
import nucliadb_sdk
|
|
13
|
+
import pytest
|
|
14
|
+
import requests
|
|
15
|
+
import uvicorn
|
|
16
|
+
from cryptography.fernet import Fernet
|
|
17
|
+
from httpx import AsyncClient
|
|
18
|
+
from httpx._transports.asgi import ASGITransport
|
|
19
|
+
from nuclia.config import NuaKey, Selection
|
|
20
|
+
from nuclia.data import get_auth
|
|
21
|
+
from nuclia.sdk import NucliaPredict
|
|
22
|
+
from nucliadb_models.resource import KnowledgeBoxObj
|
|
23
|
+
from nucliadb_sdk import NucliaDB, NucliaDBAsync
|
|
24
|
+
from nucliadb_sdk.tests.fixtures import NucliaFixture
|
|
25
|
+
from pytest_docker_fixtures import images # type: ignore # type: ignore
|
|
26
|
+
from pytest_docker_fixtures.containers.pg import pg_image # type: ignore
|
|
27
|
+
from pytest_docker_fixtures.containers.valkey import valkey_image # type: ignore
|
|
28
|
+
from redis.asyncio import Redis
|
|
29
|
+
from sqlalchemy import create_engine
|
|
30
|
+
from sqlalchemy_utils import ( # type: ignore
|
|
31
|
+
create_database,
|
|
32
|
+
database_exists,
|
|
33
|
+
drop_database,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
from hyperforge.api.app import HTTPApplication
|
|
37
|
+
from hyperforge.api.settings import Settings
|
|
38
|
+
from hyperforge.broker.redis import RedisBroker
|
|
39
|
+
from hyperforge.db.agents import AgentManager
|
|
40
|
+
from hyperforge.db.settings import DataManagerSettings
|
|
41
|
+
from hyperforge.models import MemoryConfig, NucliaDBMemoryConfig, Rules
|
|
42
|
+
from hyperforge.server.cache import ValkeyCache
|
|
43
|
+
from hyperforge.server.session import SessionManager
|
|
44
|
+
from hyperforge.server.settings import Settings as ServerSettings
|
|
45
|
+
from hyperforge.utils.http import SafeTransport
|
|
46
|
+
|
|
47
|
+
_dir = pathlib.Path(__file__).parent.absolute()
|
|
48
|
+
_package_path = _dir.parent.parent.absolute()
|
|
49
|
+
|
|
50
|
+
NUA = os.environ.get("NUA_KEY", "DUMMY")
|
|
51
|
+
|
|
52
|
+
images.settings["nucliadb"]["env"]["NUA_API_KEY"] = NUA
|
|
53
|
+
images.settings["nucliadb"]["env"]["DUMMY_PREDICT"] = "False"
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
NUCLIA_Make_dataset = (
|
|
57
|
+
"https://storage.googleapis.com/ncl-testbed-gcp-stage-1/test_nucliadb/make.export"
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
NUCLIA_Make_article = "https://storage.googleapis.com/ncl-testbed-gcp-stage-1/test_nucliadb/articles.export"
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def free_port() -> int:
|
|
64
|
+
sock = socket.socket()
|
|
65
|
+
sock.bind(("", 0))
|
|
66
|
+
port = sock.getsockname()[1]
|
|
67
|
+
sock.close()
|
|
68
|
+
return port
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
async def init_fixture(
|
|
72
|
+
nucliadb: NucliaFixture,
|
|
73
|
+
dataset_slug: str,
|
|
74
|
+
dataset_location: str,
|
|
75
|
+
semantic_model: str = "en-2024-04-24",
|
|
76
|
+
generative_model: str = "chatgpt-azure-4o",
|
|
77
|
+
kbid: str | None = None,
|
|
78
|
+
):
|
|
79
|
+
async with AsyncClient() as client:
|
|
80
|
+
resp = await client.get(
|
|
81
|
+
f"http://{nucliadb.host}:{nucliadb.port}/api/v1/config-check",
|
|
82
|
+
headers={"X-NUCLIADB-ROLES": "READER"},
|
|
83
|
+
)
|
|
84
|
+
assert resp.status_code == 200, "NUA KEY not configured"
|
|
85
|
+
assert resp.json()["nua_api_key"]["valid"], "NUA KEY not valid"
|
|
86
|
+
sdk = nucliadb_sdk.NucliaDB(region="on-prem", url=nucliadb.url)
|
|
87
|
+
slug = dataset_slug
|
|
88
|
+
learning_configuration = {
|
|
89
|
+
"semantic_model": semantic_model,
|
|
90
|
+
"semantic_models": [semantic_model],
|
|
91
|
+
"semantic_vector_similarity": "dot",
|
|
92
|
+
"semantic_vector_size": 768,
|
|
93
|
+
"semantic_threshold": 0.47,
|
|
94
|
+
"semantic_matryoshka_dims": [],
|
|
95
|
+
"semantic_model_configs": {
|
|
96
|
+
semantic_model: {
|
|
97
|
+
"similarity": 0,
|
|
98
|
+
"size": 768,
|
|
99
|
+
"threshold": 0.47,
|
|
100
|
+
"max_tokens": 2048,
|
|
101
|
+
"matryoshka_dims": [],
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
"generative_model": generative_model,
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if kbid is not None:
|
|
108
|
+
auth = get_auth()
|
|
109
|
+
auth._config.nuas_token = [
|
|
110
|
+
NuaKey(
|
|
111
|
+
client_id="nucliadb",
|
|
112
|
+
region="europe-1",
|
|
113
|
+
account="nuclia",
|
|
114
|
+
token=NUA,
|
|
115
|
+
account_type="service",
|
|
116
|
+
)
|
|
117
|
+
]
|
|
118
|
+
auth._config.default = Selection(nua="nucliadb")
|
|
119
|
+
np = NucliaPredict()
|
|
120
|
+
np.del_config(
|
|
121
|
+
kbid,
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
kb_obj = sdk.create_knowledge_box(
|
|
125
|
+
uuid=kbid, slug=slug, learning_configuration=learning_configuration
|
|
126
|
+
)
|
|
127
|
+
kbid = kb_obj.uuid
|
|
128
|
+
|
|
129
|
+
import_resp = requests.get(dataset_location) # noqa: ASYNC210
|
|
130
|
+
assert import_resp.status_code == 200, (
|
|
131
|
+
f"Error pulling dataset {dataset_location}:{import_resp.status_code}"
|
|
132
|
+
)
|
|
133
|
+
import_data = import_resp.content
|
|
134
|
+
|
|
135
|
+
import_id = sdk.start_import(kbid=kbid, content=import_data).import_id
|
|
136
|
+
assert sdk.import_status(kbid=kbid, import_id=import_id).status.value == "finished"
|
|
137
|
+
|
|
138
|
+
return kbid
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
@pytest.fixture(scope="session")
|
|
142
|
+
def make_dataset(nucliadb: NucliaFixture):
|
|
143
|
+
|
|
144
|
+
kbid = asyncio.run(
|
|
145
|
+
init_fixture(
|
|
146
|
+
nucliadb,
|
|
147
|
+
"conv",
|
|
148
|
+
NUCLIA_Make_dataset,
|
|
149
|
+
kbid="00000000-0000-0000-0000-000000000002",
|
|
150
|
+
)
|
|
151
|
+
)
|
|
152
|
+
yield kbid
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
@pytest.fixture(scope="session")
|
|
156
|
+
def article_dataset(nucliadb: NucliaFixture):
|
|
157
|
+
kbid = asyncio.run(
|
|
158
|
+
init_fixture(
|
|
159
|
+
nucliadb,
|
|
160
|
+
"conv",
|
|
161
|
+
NUCLIA_Make_article,
|
|
162
|
+
"multilingual-2024-05-06",
|
|
163
|
+
"chatgpt-4.1",
|
|
164
|
+
kbid="00000000-0000-0000-0000-000000000001",
|
|
165
|
+
)
|
|
166
|
+
)
|
|
167
|
+
yield kbid
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
@pytest.fixture
|
|
171
|
+
async def arag_settings(sdk_async: NucliaDBAsync, valkey_url: str):
|
|
172
|
+
yield Settings(
|
|
173
|
+
running_environment="test",
|
|
174
|
+
valkey_url=valkey_url,
|
|
175
|
+
valkey_cluster_mode=False,
|
|
176
|
+
memory_reader_nucliadb=sdk_async.base_url,
|
|
177
|
+
memory_writer_nucliadb=sdk_async.base_url,
|
|
178
|
+
memory_search_nucliadb=sdk_async.base_url,
|
|
179
|
+
dummy_idp=True,
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
images.settings["postgresql"].update(
|
|
184
|
+
{
|
|
185
|
+
"version": "16.1",
|
|
186
|
+
"env": {
|
|
187
|
+
"POSTGRES_PASSWORD": "postgres",
|
|
188
|
+
"POSTGRES_DB": "postgres",
|
|
189
|
+
"POSTGRES_USER": "postgres",
|
|
190
|
+
},
|
|
191
|
+
}
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
@pytest.fixture(scope="session")
|
|
196
|
+
def pg():
|
|
197
|
+
host, port = pg_image.run()
|
|
198
|
+
yield host, port
|
|
199
|
+
pg_image.stop()
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
@pytest.fixture(scope="session")
|
|
203
|
+
def pg_dsn(pg):
|
|
204
|
+
host, port = pg
|
|
205
|
+
dsn = f"postgresql://postgres:postgres@{host}:{port}/test_db"
|
|
206
|
+
|
|
207
|
+
if database_exists(dsn):
|
|
208
|
+
drop_database(dsn)
|
|
209
|
+
create_database(dsn)
|
|
210
|
+
config = alembic.config.Config(str(_package_path) + "/alembic.ini")
|
|
211
|
+
config.set_main_option("sqlalchemy.url", dsn)
|
|
212
|
+
alembic.command.upgrade(config, "head")
|
|
213
|
+
yield dsn
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
@pytest.fixture(scope="function")
|
|
217
|
+
def test_db(pg_dsn):
|
|
218
|
+
engine = create_engine(pg_dsn)
|
|
219
|
+
with engine.connect() as conn:
|
|
220
|
+
yield conn
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
@pytest.fixture
|
|
224
|
+
async def data_manager_settings(pg_dsn):
|
|
225
|
+
yield DataManagerSettings(postgresql_dsn=pg_dsn)
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
@pytest.fixture
|
|
229
|
+
async def arag_api_app(
|
|
230
|
+
arag_settings: Settings,
|
|
231
|
+
data_manager_settings: DataManagerSettings,
|
|
232
|
+
):
|
|
233
|
+
application = HTTPApplication(
|
|
234
|
+
settings=arag_settings,
|
|
235
|
+
data_manager_settings=data_manager_settings,
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
await application.startup()
|
|
239
|
+
|
|
240
|
+
yield application
|
|
241
|
+
|
|
242
|
+
await application.shutdown()
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
@pytest.fixture
|
|
246
|
+
async def arag_api(arag_api_app: HTTPApplication, load_agents):
|
|
247
|
+
yield AsyncClient(transport=ASGITransport(app=arag_api_app), base_url="http://test")
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
@pytest.fixture
|
|
251
|
+
async def arag_api_http(
|
|
252
|
+
arag_api_app: HTTPApplication,
|
|
253
|
+
):
|
|
254
|
+
"""Serve the already-started arag_api_app over real HTTP/WebSocket.
|
|
255
|
+
|
|
256
|
+
Reuses the same HTTPApplication instance as arag_api_app so that only one
|
|
257
|
+
PredictEngine (and one aiohttp session) is created per test. Uvicorn is
|
|
258
|
+
started with lifespan="off" to avoid calling startup/shutdown a second time.
|
|
259
|
+
"""
|
|
260
|
+
http_port = free_port()
|
|
261
|
+
config = uvicorn.Config(
|
|
262
|
+
arag_api_app,
|
|
263
|
+
host="127.0.0.1",
|
|
264
|
+
port=http_port,
|
|
265
|
+
log_level="warning",
|
|
266
|
+
lifespan="off",
|
|
267
|
+
)
|
|
268
|
+
server = uvicorn.Server(config)
|
|
269
|
+
server_task = asyncio.create_task(server.serve())
|
|
270
|
+
while not server.started and not server.should_exit:
|
|
271
|
+
await asyncio.sleep(0.01)
|
|
272
|
+
if not server.started:
|
|
273
|
+
await server_task
|
|
274
|
+
raise RuntimeError("arag_api_http failed to start")
|
|
275
|
+
|
|
276
|
+
yield f"127.0.0.1:{http_port}"
|
|
277
|
+
|
|
278
|
+
server.should_exit = True
|
|
279
|
+
await server_task
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
@pytest.fixture
|
|
283
|
+
async def arag_api_http_client(
|
|
284
|
+
arag_api_http: str,
|
|
285
|
+
):
|
|
286
|
+
"""
|
|
287
|
+
Fixture to provide an HTTP client for the Arag API.
|
|
288
|
+
"""
|
|
289
|
+
async with AsyncClient(base_url=f"http://{arag_api_http}") as client:
|
|
290
|
+
yield client
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
@pytest.fixture
|
|
294
|
+
async def arag_api_http_session(
|
|
295
|
+
arag_api_http_client: AsyncClient,
|
|
296
|
+
arag_kb: KnowledgeBoxObj,
|
|
297
|
+
):
|
|
298
|
+
resp = await arag_api_http_client.post(
|
|
299
|
+
f"/api/v1/agent/{arag_kb.uuid}/sessions",
|
|
300
|
+
json={
|
|
301
|
+
"slug": "slug1",
|
|
302
|
+
"name": "My Title",
|
|
303
|
+
"summary": "This is a nice user",
|
|
304
|
+
"data": '{"age": "46"}',
|
|
305
|
+
"format": "JSON",
|
|
306
|
+
},
|
|
307
|
+
headers={
|
|
308
|
+
"X-STF-USER": "user1",
|
|
309
|
+
"X-STF-ACCOUNT": "nuclia",
|
|
310
|
+
"X-STF-ACCOUNT-TYPE": "basic",
|
|
311
|
+
"X-STF-ROLES": "SOWNER",
|
|
312
|
+
},
|
|
313
|
+
)
|
|
314
|
+
assert resp.status_code == 200
|
|
315
|
+
session_id = resp.json()["uuid"]
|
|
316
|
+
yield session_id
|
|
317
|
+
await arag_api_http_client.delete(
|
|
318
|
+
f"/api/v1/agent/{arag_kb.uuid}/session/{session_id}",
|
|
319
|
+
headers={
|
|
320
|
+
"X-STF-USER": "user1",
|
|
321
|
+
"X-STF-ACCOUNT": "nuclia",
|
|
322
|
+
"X-STF-ACCOUNT-TYPE": "basic",
|
|
323
|
+
"X-STF-ROLES": "SOWNER",
|
|
324
|
+
},
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
@pytest.fixture
|
|
329
|
+
async def arag(
|
|
330
|
+
request,
|
|
331
|
+
arag_kb: KnowledgeBoxObj,
|
|
332
|
+
arag_kb_legacy: KnowledgeBoxObj,
|
|
333
|
+
arag_no_memory: str,
|
|
334
|
+
):
|
|
335
|
+
"""Indirect fixture to support parametrization of different arag KB types."""
|
|
336
|
+
fixture_map = {
|
|
337
|
+
"arag_kb": arag_kb.uuid,
|
|
338
|
+
"arag_kb_legacy": arag_kb_legacy.uuid,
|
|
339
|
+
"arag_no_memory": arag_no_memory,
|
|
340
|
+
}
|
|
341
|
+
return fixture_map[request.param]
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
@pytest.fixture
|
|
345
|
+
async def arag_kb(sdk: NucliaDB, arag_api_app: HTTPApplication):
|
|
346
|
+
kb = await create_arag_kb(
|
|
347
|
+
arag_api_app.agent_manager, sdk, "test_basic_worker", "nuclia"
|
|
348
|
+
)
|
|
349
|
+
yield kb
|
|
350
|
+
await delete_arag_kb(arag_api_app.agent_manager, sdk, kb.uuid, "nuclia")
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
@pytest.fixture
|
|
354
|
+
async def arag_no_memory(arag_api_app: HTTPApplication):
|
|
355
|
+
arag_id = await create_arag_no_memory(arag_api_app.agent_manager, "nuclia")
|
|
356
|
+
yield arag_id
|
|
357
|
+
await delete_arag_no_memory(arag_api_app.agent_manager, arag_id, "nuclia")
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
@pytest.fixture
|
|
361
|
+
async def arag_kb_legacy(sdk: NucliaDB, arag_api_app: HTTPApplication):
|
|
362
|
+
"""
|
|
363
|
+
We have some rows that have memory set to null but actually have a memory KB.
|
|
364
|
+
(Legacy from before we added the no KB option)
|
|
365
|
+
"""
|
|
366
|
+
kb = await create_arag_kb(
|
|
367
|
+
arag_api_app.agent_manager, sdk, "test_basic_worker_legacy", "nuclia"
|
|
368
|
+
)
|
|
369
|
+
# Go to the DB and set memory to null
|
|
370
|
+
db: databases.Database = arag_api_app.agent_manager.database
|
|
371
|
+
from hyperforge.db.agents import retrieval_agent_config
|
|
372
|
+
|
|
373
|
+
statement = (
|
|
374
|
+
retrieval_agent_config.update()
|
|
375
|
+
.where(retrieval_agent_config.c.agent_id == kb.uuid)
|
|
376
|
+
.values(memory=None)
|
|
377
|
+
)
|
|
378
|
+
await db.execute(statement)
|
|
379
|
+
yield kb
|
|
380
|
+
await delete_arag_kb(arag_api_app.agent_manager, sdk, kb.uuid, "nuclia")
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
@pytest.fixture
|
|
384
|
+
async def agent_db_server(data_manager_settings: DataManagerSettings):
|
|
385
|
+
agent_manager = await AgentManager.from_settings(settings=data_manager_settings)
|
|
386
|
+
await agent_manager.initialize()
|
|
387
|
+
return agent_manager
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
@pytest.fixture
|
|
391
|
+
async def arag_server(
|
|
392
|
+
sdk: NucliaDB,
|
|
393
|
+
agent_db_server: AgentManager,
|
|
394
|
+
valkey,
|
|
395
|
+
):
|
|
396
|
+
valkey_host, valkey_port = valkey
|
|
397
|
+
valkey_url = f"redis://{valkey_host}:{valkey_port}"
|
|
398
|
+
settings = ServerSettings(
|
|
399
|
+
valkey_url=valkey_url,
|
|
400
|
+
valkey_cluster_mode=False,
|
|
401
|
+
internal_nucliadb=True,
|
|
402
|
+
internal_nucliadb_url=sdk.base_url,
|
|
403
|
+
internal_nua=False,
|
|
404
|
+
local_openai=None,
|
|
405
|
+
external_nua_api_key=NUA,
|
|
406
|
+
)
|
|
407
|
+
broker = RedisBroker.from_url(
|
|
408
|
+
url=valkey_url,
|
|
409
|
+
activate_subject=settings.activate_subject,
|
|
410
|
+
keepalive_ms=int(settings.pubsub_keepalive_seconds * 1000),
|
|
411
|
+
cluster_mode=settings.valkey_cluster_mode,
|
|
412
|
+
)
|
|
413
|
+
session = SessionManager(
|
|
414
|
+
settings=settings,
|
|
415
|
+
broker=broker,
|
|
416
|
+
agent_manager=agent_db_server,
|
|
417
|
+
cache=ValkeyCache(
|
|
418
|
+
Redis(host=valkey_host, port=valkey_port, decode_responses=True)
|
|
419
|
+
),
|
|
420
|
+
)
|
|
421
|
+
await session.initialize()
|
|
422
|
+
yield session
|
|
423
|
+
await session.finalize()
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
@pytest.fixture
|
|
427
|
+
async def arag_server_kb(sdk_async: NucliaDBAsync, arag_server: SessionManager):
|
|
428
|
+
kb: KnowledgeBoxObj = await sdk_async.create_knowledge_box(slug="test_basic_worker")
|
|
429
|
+
|
|
430
|
+
assert isinstance(arag_server.agent_manager, AgentManager)
|
|
431
|
+
await arag_server.agent_manager.add_agent(
|
|
432
|
+
account="nuclia",
|
|
433
|
+
agent_id=kb.uuid,
|
|
434
|
+
rules=Rules(rules=[]),
|
|
435
|
+
memory=MemoryConfig(
|
|
436
|
+
nucliadb=NucliaDBMemoryConfig(url=sdk_async.base_url, kbid=kb.uuid)
|
|
437
|
+
),
|
|
438
|
+
)
|
|
439
|
+
|
|
440
|
+
yield kb
|
|
441
|
+
|
|
442
|
+
await sdk_async.delete_knowledge_box(kbid=kb.uuid)
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
@pytest.fixture
|
|
446
|
+
async def arag_api_session(
|
|
447
|
+
arag_api: AsyncClient,
|
|
448
|
+
arag_kb: KnowledgeBoxObj,
|
|
449
|
+
):
|
|
450
|
+
resp = await arag_api.post(
|
|
451
|
+
f"/api/v1/agent/{arag_kb.uuid}/sessions",
|
|
452
|
+
json={
|
|
453
|
+
"slug": "slug1",
|
|
454
|
+
"name": "My Title",
|
|
455
|
+
"summary": "This is a nice user",
|
|
456
|
+
"data": '{"age": "46"}',
|
|
457
|
+
"format": "JSON",
|
|
458
|
+
},
|
|
459
|
+
headers={
|
|
460
|
+
"X-STF-USER": "user1",
|
|
461
|
+
"X-STF-ACCOUNT": "nuclia",
|
|
462
|
+
"X-STF-ACCOUNT-TYPE": "basic",
|
|
463
|
+
"X-STF-ROLES": "SOWNER",
|
|
464
|
+
},
|
|
465
|
+
)
|
|
466
|
+
assert resp.status_code == 200
|
|
467
|
+
session_id = resp.json()["uuid"]
|
|
468
|
+
yield session_id
|
|
469
|
+
# Best effort cleanup - don't fail if deletion fails
|
|
470
|
+
try:
|
|
471
|
+
await arag_api.delete(
|
|
472
|
+
f"/api/v1/agent/{arag_kb.uuid}/session/{session_id}",
|
|
473
|
+
headers={
|
|
474
|
+
"X-STF-USER": "user1",
|
|
475
|
+
"X-STF-ACCOUNT": "nuclia",
|
|
476
|
+
"X-STF-ACCOUNT-TYPE": "basic",
|
|
477
|
+
"X-STF-ROLES": "SOWNER",
|
|
478
|
+
},
|
|
479
|
+
)
|
|
480
|
+
except Exception:
|
|
481
|
+
# Ignore cleanup errors
|
|
482
|
+
pass
|
|
483
|
+
|
|
484
|
+
|
|
485
|
+
@pytest.fixture(scope="session")
|
|
486
|
+
async def valkey():
|
|
487
|
+
host, port = valkey_image.run()
|
|
488
|
+
yield host, port
|
|
489
|
+
valkey_image.stop()
|
|
490
|
+
|
|
491
|
+
|
|
492
|
+
@pytest.fixture
|
|
493
|
+
async def valkey_cache(valkey):
|
|
494
|
+
yield ValkeyCache(Redis(host=valkey[0], port=valkey[1], decode_responses=True))
|
|
495
|
+
|
|
496
|
+
|
|
497
|
+
@pytest.fixture
|
|
498
|
+
async def valkey_url(valkey):
|
|
499
|
+
yield f"redis://{valkey[0]}:{valkey[1]}"
|
|
500
|
+
|
|
501
|
+
|
|
502
|
+
@pytest.fixture(autouse=True, scope="session")
|
|
503
|
+
def setup_encryption_key():
|
|
504
|
+
os.environ["ENCRYPTION_SECRET_KEY"] = Fernet.generate_key().decode()
|
|
505
|
+
yield
|
|
506
|
+
|
|
507
|
+
|
|
508
|
+
@pytest.fixture
|
|
509
|
+
async def disable_safe_transport():
|
|
510
|
+
with patch.object(SafeTransport, "is_private_address", return_value=False):
|
|
511
|
+
yield
|
|
512
|
+
|
|
513
|
+
|
|
514
|
+
class _VCRTaskExceptionFilter(logging.Filter):
|
|
515
|
+
"""Suppress 'Task exception was never retrieved' asyncio errors from vcrpy.
|
|
516
|
+
|
|
517
|
+
vcrpy's httpx stub creates a background task (_record_responses) that can
|
|
518
|
+
fail with an AssertionError due to a vcrpy/httpx version incompatibility.
|
|
519
|
+
The exception is noisy but harmless in test runs.
|
|
520
|
+
"""
|
|
521
|
+
|
|
522
|
+
def filter(self, record: logging.LogRecord) -> bool:
|
|
523
|
+
return not (
|
|
524
|
+
record.levelno == logging.ERROR
|
|
525
|
+
and "Task exception was never retrieved" in record.getMessage()
|
|
526
|
+
and "_record_responses" in record.getMessage()
|
|
527
|
+
)
|
|
528
|
+
|
|
529
|
+
|
|
530
|
+
# Import vcr_config and suppress_test_noise from minimal_fixtures (single source of truth)
|
|
531
|
+
from hyperforge.minimal_fixtures import ( # noqa: E402, F401
|
|
532
|
+
suppress_test_noise,
|
|
533
|
+
vcr_config,
|
|
534
|
+
)
|
|
535
|
+
|
|
536
|
+
|
|
537
|
+
async def delete_arag_kb(
|
|
538
|
+
agent_db: AgentManager, sdk: NucliaDB, kbid: str, account: str
|
|
539
|
+
) -> None:
|
|
540
|
+
sdk.delete_knowledge_box(kbid=kbid)
|
|
541
|
+
await agent_db.delete_agent(
|
|
542
|
+
account=account,
|
|
543
|
+
agent_id=kbid,
|
|
544
|
+
)
|
|
545
|
+
|
|
546
|
+
|
|
547
|
+
async def create_arag_kb(
|
|
548
|
+
agent_db: AgentManager, sdk: NucliaDB, slug: str, account: str
|
|
549
|
+
) -> KnowledgeBoxObj:
|
|
550
|
+
kb: KnowledgeBoxObj = sdk.create_knowledge_box(slug=slug)
|
|
551
|
+
await agent_db.add_agent(
|
|
552
|
+
account=account,
|
|
553
|
+
agent_id=kb.uuid,
|
|
554
|
+
rules=Rules(rules=[]),
|
|
555
|
+
memory=MemoryConfig(
|
|
556
|
+
nucliadb=NucliaDBMemoryConfig(url=sdk.base_url, kbid=kb.uuid, internal=True)
|
|
557
|
+
),
|
|
558
|
+
)
|
|
559
|
+
|
|
560
|
+
return kb
|
|
561
|
+
|
|
562
|
+
|
|
563
|
+
async def create_arag_no_memory(agent_db: AgentManager, account: str) -> str:
|
|
564
|
+
uuid = str(uuid4())
|
|
565
|
+
await agent_db.add_agent(
|
|
566
|
+
account=account,
|
|
567
|
+
agent_id=uuid,
|
|
568
|
+
rules=Rules(rules=[]),
|
|
569
|
+
memory=MemoryConfig(),
|
|
570
|
+
)
|
|
571
|
+
|
|
572
|
+
return uuid
|
|
573
|
+
|
|
574
|
+
|
|
575
|
+
async def delete_arag_no_memory(
|
|
576
|
+
agent_db: AgentManager, agent_id: str, account: str
|
|
577
|
+
) -> None:
|
|
578
|
+
await agent_db.delete_agent(
|
|
579
|
+
account=account,
|
|
580
|
+
agent_id=agent_id,
|
|
581
|
+
)
|
|
582
|
+
|
|
583
|
+
|
|
584
|
+
@pytest.fixture
|
|
585
|
+
def load_agents():
|
|
586
|
+
from hyperforge.configure import load_all_configurations, scan
|
|
587
|
+
|
|
588
|
+
for module in [
|
|
589
|
+
"hyperforge_external",
|
|
590
|
+
"hyperforge_conditional",
|
|
591
|
+
"hyperforge_nucliadb",
|
|
592
|
+
"hyperforge_smart",
|
|
593
|
+
"hyperforge_remi",
|
|
594
|
+
"hyperforge_restricted",
|
|
595
|
+
"hyperforge_summarize",
|
|
596
|
+
"hyperforge_static",
|
|
597
|
+
"hyperforge_rephrase",
|
|
598
|
+
"hyperforge_mcp",
|
|
599
|
+
"hyperforge_remi",
|
|
600
|
+
]:
|
|
601
|
+
scan(module)
|
|
602
|
+
load_all_configurations(module)
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import uuid
|
|
2
|
+
from enum import Enum
|
|
3
|
+
from typing import Any, Dict, Optional
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel, Field
|
|
6
|
+
|
|
7
|
+
from hyperforge.models import (
|
|
8
|
+
Answer,
|
|
9
|
+
AnswerCitations,
|
|
10
|
+
Context,
|
|
11
|
+
Step,
|
|
12
|
+
Visualization,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
# Models used between client and API for itneraction requests/responses/streams
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class Operation(int, Enum):
|
|
19
|
+
START = 0
|
|
20
|
+
STOP = 1
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class AnswerOperation(int, Enum):
|
|
24
|
+
ANSWER = 0
|
|
25
|
+
START = 2
|
|
26
|
+
DONE = 3
|
|
27
|
+
ERROR = 4
|
|
28
|
+
AGENT_REQUEST = 5
|
|
29
|
+
ANSWER_CHUNK = 6
|
|
30
|
+
REASONING = 7
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class StreamingChunk(BaseModel):
|
|
34
|
+
"""A single streaming chunk of text produced by an LLM.
|
|
35
|
+
|
|
36
|
+
When last=True this is the final chunk in the stream
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
text: str
|
|
40
|
+
last: bool = False
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class ARAGException(BaseModel):
|
|
44
|
+
detail: str
|
|
45
|
+
extra: Optional[Dict[str, Any]] = None
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class ValidationFeedbackSchema(BaseModel):
|
|
49
|
+
call_tool: bool
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class PromptFeedbackSchema(BaseModel):
|
|
53
|
+
prompt_id: str
|
|
54
|
+
data: Any
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class Provider(Enum):
|
|
58
|
+
GOOGLE_OAUTH = "google_oauth"
|
|
59
|
+
AZURE_OAUTH = "azure_oauth"
|
|
60
|
+
AZURE_CERTIFICATE_CREDENTIALS = "azure_certificate_credentials"
|
|
61
|
+
AWS_S3_ACCESS_KEYS = "aws_s3_access_keys"
|
|
62
|
+
SHAREFILE_OAUTH = "sharefile_oauth"
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class OAuthAuthenticateURL(BaseModel):
|
|
66
|
+
oauth_url: str
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class OAuthFeedbackReturnSchema(BaseModel):
|
|
70
|
+
existing_credentials: Optional[Dict[str, Dict[str, str]]] = None
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class Feedback(BaseModel):
|
|
74
|
+
request_id: str
|
|
75
|
+
feedback_id: str = Field(default_factory=lambda: uuid.uuid4().hex)
|
|
76
|
+
question: str
|
|
77
|
+
module: str
|
|
78
|
+
agent_id: str
|
|
79
|
+
data: Any
|
|
80
|
+
timeout_ms: int = 10_000
|
|
81
|
+
response_schema: Any
|
|
82
|
+
get_credentials: Optional[Dict[str, Provider]] = None
|
|
83
|
+
credentials: Optional[Dict[str, Dict[str, Any]]] = None
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class AragAnswer(BaseModel):
|
|
87
|
+
exception: Optional[ARAGException] = None
|
|
88
|
+
answer: Optional[str] = None
|
|
89
|
+
answer_citations: Optional[AnswerCitations] = None
|
|
90
|
+
answer_urls: Optional[list[str]] = None
|
|
91
|
+
agent_request: Optional[str] = None
|
|
92
|
+
generated_text: Optional[str] = None
|
|
93
|
+
step: Optional[Step] = None
|
|
94
|
+
possible_answer: Optional[Answer] = None
|
|
95
|
+
context: Optional[Context] = None
|
|
96
|
+
operation: AnswerOperation = AnswerOperation.ANSWER
|
|
97
|
+
seqid: Optional[int] = None
|
|
98
|
+
original_question_uuid: Optional[str] = None
|
|
99
|
+
actual_question_uuid: Optional[str] = None
|
|
100
|
+
feedback: Optional[Feedback] = None
|
|
101
|
+
oauth: Optional[OAuthAuthenticateURL] = None
|
|
102
|
+
data_visualizations: Optional[list[Visualization]] = None
|
|
103
|
+
streaming_response_chunk: Optional[StreamingChunk] = None
|
|
104
|
+
reasoning: Optional[StreamingChunk] = None
|
|
105
|
+
|
|
106
|
+
def __str__(self) -> str:
|
|
107
|
+
if self.step is not None:
|
|
108
|
+
return "\033[1mStep: \033[0m \n" + str(self.step)
|
|
109
|
+
elif self.exception is not None:
|
|
110
|
+
return "\033[1mException: \033[0m \n" + str(self.exception)
|
|
111
|
+
elif self.context is not None:
|
|
112
|
+
return "\033[1mContext: \033[0m \n" + str(self.context)
|
|
113
|
+
return (
|
|
114
|
+
f"AragAnswer(operation={self.operation}, answer={self.answer}, "
|
|
115
|
+
f"agent_request={self.agent_request}, exception={self.exception})"
|
|
116
|
+
)
|