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.
Files changed (90) hide show
  1. hyperforge/__init__.py +16 -0
  2. hyperforge/agent.py +81 -0
  3. hyperforge/api/__init__.py +20 -0
  4. hyperforge/api/app.py +155 -0
  5. hyperforge/api/authentication.py +271 -0
  6. hyperforge/api/commands.py +33 -0
  7. hyperforge/api/internal/__init__.py +4 -0
  8. hyperforge/api/internal/inspect.py +30 -0
  9. hyperforge/api/internal/router.py +3 -0
  10. hyperforge/api/logging.py +18 -0
  11. hyperforge/api/models.py +129 -0
  12. hyperforge/api/session.py +197 -0
  13. hyperforge/api/settings.py +38 -0
  14. hyperforge/api/utils.py +354 -0
  15. hyperforge/api/v1/__init__.py +23 -0
  16. hyperforge/api/v1/agents.py +531 -0
  17. hyperforge/api/v1/interaction.py +430 -0
  18. hyperforge/api/v1/mcp_content.py +311 -0
  19. hyperforge/api/v1/mcp_interaction.py +322 -0
  20. hyperforge/api/v1/oauth.py +60 -0
  21. hyperforge/api/v1/prompt.py +129 -0
  22. hyperforge/api/v1/router.py +3 -0
  23. hyperforge/api/v1/schema.py +56 -0
  24. hyperforge/api/v1/session.py +182 -0
  25. hyperforge/api/v1/utils.py +12 -0
  26. hyperforge/api/v1/workflows.py +643 -0
  27. hyperforge/arag.py +28 -0
  28. hyperforge/broker/__init__.py +52 -0
  29. hyperforge/broker/local.py +116 -0
  30. hyperforge/broker/redis.py +161 -0
  31. hyperforge/configure.py +571 -0
  32. hyperforge/context/__init__.py +0 -0
  33. hyperforge/context/agent.py +377 -0
  34. hyperforge/context/config.py +103 -0
  35. hyperforge/database.py +3 -0
  36. hyperforge/db/__init__.py +6 -0
  37. hyperforge/db/agents.py +1521 -0
  38. hyperforge/db/encryption.py +91 -0
  39. hyperforge/db/exceptions.py +26 -0
  40. hyperforge/db/settings.py +16 -0
  41. hyperforge/db/workflow_cleanup.py +69 -0
  42. hyperforge/definition.py +13 -0
  43. hyperforge/driver.py +31 -0
  44. hyperforge/dummy.py +28 -0
  45. hyperforge/engine.py +189 -0
  46. hyperforge/exceptions.py +14 -0
  47. hyperforge/feature_flag.py +105 -0
  48. hyperforge/fixtures.py +602 -0
  49. hyperforge/interaction.py +116 -0
  50. hyperforge/llm.py +75 -0
  51. hyperforge/manager.py +432 -0
  52. hyperforge/memory/__init__.py +5 -0
  53. hyperforge/memory/memory.py +974 -0
  54. hyperforge/minimal_fixtures.py +75 -0
  55. hyperforge/models.py +336 -0
  56. hyperforge/nua.py +336 -0
  57. hyperforge/openapi.py +63 -0
  58. hyperforge/prompts.py +188 -0
  59. hyperforge/pubsub.py +90 -0
  60. hyperforge/py.typed +0 -0
  61. hyperforge/redis_utils.py +82 -0
  62. hyperforge/retrieval/__init__.py +0 -0
  63. hyperforge/retrieval/agent.py +169 -0
  64. hyperforge/retrieval/config.py +94 -0
  65. hyperforge/server/__init__.py +5 -0
  66. hyperforge/server/cache.py +131 -0
  67. hyperforge/server/run.py +109 -0
  68. hyperforge/server/sandbox.py +60 -0
  69. hyperforge/server/session.py +421 -0
  70. hyperforge/server/settings.py +47 -0
  71. hyperforge/server/utils.py +57 -0
  72. hyperforge/server/web.py +31 -0
  73. hyperforge/settings.py +18 -0
  74. hyperforge/standalone/__init__.py +5 -0
  75. hyperforge/standalone/agent.py +189 -0
  76. hyperforge/standalone/app.py +264 -0
  77. hyperforge/standalone/config.py +137 -0
  78. hyperforge/standalone/const.py +1 -0
  79. hyperforge/standalone/run.py +60 -0
  80. hyperforge/standalone/settings.py +133 -0
  81. hyperforge/standalone/ui_router.py +241 -0
  82. hyperforge/trace.py +42 -0
  83. hyperforge/utils/__init__.py +112 -0
  84. hyperforge/utils/http.py +48 -0
  85. hyperforge/workflows.py +44 -0
  86. hyperforge-1.0.0.post19.dist-info/METADATA +95 -0
  87. hyperforge-1.0.0.post19.dist-info/RECORD +90 -0
  88. hyperforge-1.0.0.post19.dist-info/WHEEL +5 -0
  89. hyperforge-1.0.0.post19.dist-info/entry_points.txt +8 -0
  90. 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
+ )