harborrag-runtime 2.0.0a1__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 (146) hide show
  1. harborrag_runtime/__init__.py +22 -0
  2. harborrag_runtime/agent/__init__.py +37 -0
  3. harborrag_runtime/agent/checkpoint.py +115 -0
  4. harborrag_runtime/agent/tool_specs.py +207 -0
  5. harborrag_runtime/agent/tools.py +222 -0
  6. harborrag_runtime/chat/README.md +30 -0
  7. harborrag_runtime/chat/__init__.py +7 -0
  8. harborrag_runtime/chat/composition.py +17 -0
  9. harborrag_runtime/chat/facade.py +36 -0
  10. harborrag_runtime/chat/prompts/__init__.py +5 -0
  11. harborrag_runtime/chat/prompts/catalog.py +43 -0
  12. harborrag_runtime/chat/prompts/templates/__init__.py +1 -0
  13. harborrag_runtime/chat/prompts/templates/concise.md +1 -0
  14. harborrag_runtime/chat/prompts/templates/default.md +1 -0
  15. harborrag_runtime/chat/service.py +93 -0
  16. harborrag_runtime/composition/__init__.py +5 -0
  17. harborrag_runtime/composition/control_plane.py +277 -0
  18. harborrag_runtime/composition/resources.py +120 -0
  19. harborrag_runtime/config/__init__.py +58 -0
  20. harborrag_runtime/config/connectors/__init__.py +19 -0
  21. harborrag_runtime/config/connectors/fingerprints.py +54 -0
  22. harborrag_runtime/config/connectors/loader.py +95 -0
  23. harborrag_runtime/config/connectors/parsing.py +237 -0
  24. harborrag_runtime/config/connectors/providers.py +193 -0
  25. harborrag_runtime/config/connectors/schemas.py +216 -0
  26. harborrag_runtime/config/errors.py +14 -0
  27. harborrag_runtime/config/loading.py +245 -0
  28. harborrag_runtime/config/parsers/__init__.py +19 -0
  29. harborrag_runtime/config/parsers/loader.py +65 -0
  30. harborrag_runtime/config/parsers/parsing.py +230 -0
  31. harborrag_runtime/config/parsers/providers.py +228 -0
  32. harborrag_runtime/config/parsers/schemas.py +228 -0
  33. harborrag_runtime/config/settings.py +158 -0
  34. harborrag_runtime/config/temporal.py +299 -0
  35. harborrag_runtime/config/temporal_loading.py +285 -0
  36. harborrag_runtime/contracts.py +143 -0
  37. harborrag_runtime/document_stage_catalog.py +84 -0
  38. harborrag_runtime/errors.py +60 -0
  39. harborrag_runtime/events/__init__.py +3 -0
  40. harborrag_runtime/events/in_process.py +102 -0
  41. harborrag_runtime/execution/__init__.py +26 -0
  42. harborrag_runtime/execution/contracts.py +32 -0
  43. harborrag_runtime/execution/direct.py +49 -0
  44. harborrag_runtime/execution/submission.py +47 -0
  45. harborrag_runtime/execution/temporal.py +80 -0
  46. harborrag_runtime/ingestion/README.md +53 -0
  47. harborrag_runtime/ingestion/__init__.py +101 -0
  48. harborrag_runtime/ingestion/composition.py +187 -0
  49. harborrag_runtime/ingestion/connector_metrics.py +14 -0
  50. harborrag_runtime/ingestion/discovery_metrics.py +46 -0
  51. harborrag_runtime/ingestion/document/__init__.py +14 -0
  52. harborrag_runtime/ingestion/document/capture.py +339 -0
  53. harborrag_runtime/ingestion/document/dependencies.py +46 -0
  54. harborrag_runtime/ingestion/document/lifecycle.py +266 -0
  55. harborrag_runtime/ingestion/document/materialization.py +208 -0
  56. harborrag_runtime/ingestion/document/materialization_helpers.py +47 -0
  57. harborrag_runtime/ingestion/document/models.py +53 -0
  58. harborrag_runtime/ingestion/document/normalization.py +29 -0
  59. harborrag_runtime/ingestion/document/normalizers/__init__.py +19 -0
  60. harborrag_runtime/ingestion/document/normalizers/builder.py +98 -0
  61. harborrag_runtime/ingestion/document/normalizers/builtins.py +40 -0
  62. harborrag_runtime/ingestion/document/normalizers/router.py +45 -0
  63. harborrag_runtime/ingestion/document/normalizers/transform.py +30 -0
  64. harborrag_runtime/ingestion/document/pipeline.py +206 -0
  65. harborrag_runtime/ingestion/document/preparation.py +103 -0
  66. harborrag_runtime/ingestion/document/projection.py +258 -0
  67. harborrag_runtime/ingestion/document/projection_coordinator.py +131 -0
  68. harborrag_runtime/ingestion/document/projection_material.py +157 -0
  69. harborrag_runtime/ingestion/document/service.py +45 -0
  70. harborrag_runtime/ingestion/document/stage_models.py +49 -0
  71. harborrag_runtime/ingestion/limits.py +52 -0
  72. harborrag_runtime/ingestion/maintenance/__init__.py +1 -0
  73. harborrag_runtime/ingestion/maintenance/cleanup.py +180 -0
  74. harborrag_runtime/ingestion/maintenance/projection_admin.py +147 -0
  75. harborrag_runtime/ingestion/maintenance/reindex.py +257 -0
  76. harborrag_runtime/ingestion/maintenance/reindex_plan.py +51 -0
  77. harborrag_runtime/ingestion/maintenance/reindex_reuse.py +138 -0
  78. harborrag_runtime/ingestion/maintenance/relation_repair.py +293 -0
  79. harborrag_runtime/ingestion/maintenance/relation_supersession.py +56 -0
  80. harborrag_runtime/ingestion/observability.py +294 -0
  81. harborrag_runtime/ingestion/observability_types.py +46 -0
  82. harborrag_runtime/ingestion/profiles.py +37 -0
  83. harborrag_runtime/ingestion/runtime_builder.py +333 -0
  84. harborrag_runtime/ingestion/source/__init__.py +1 -0
  85. harborrag_runtime/ingestion/source/descriptor_mapping.py +141 -0
  86. harborrag_runtime/ingestion/source/discovery.py +255 -0
  87. harborrag_runtime/ingestion/source/documents.py +150 -0
  88. harborrag_runtime/ingestion/source/finalization.py +190 -0
  89. harborrag_runtime/ingestion/source/models.py +165 -0
  90. harborrag_runtime/ingestion/source/operations.py +159 -0
  91. harborrag_runtime/ingestion/source/plan.py +237 -0
  92. harborrag_runtime/ingestion/source/retry.py +132 -0
  93. harborrag_runtime/ingestion/source/service.py +303 -0
  94. harborrag_runtime/ingestion/source/tasks.py +48 -0
  95. harborrag_runtime/ingestion/stage_observation.py +96 -0
  96. harborrag_runtime/memory/__init__.py +29 -0
  97. harborrag_runtime/memory/composition.py +43 -0
  98. harborrag_runtime/memory/service.py +120 -0
  99. harborrag_runtime/plugins.py +56 -0
  100. harborrag_runtime/rate_limiting.py +39 -0
  101. harborrag_runtime/retrieval/__init__.py +25 -0
  102. harborrag_runtime/retrieval/composition.py +109 -0
  103. harborrag_runtime/retrieval/contracts.py +154 -0
  104. harborrag_runtime/retrieval/graph_observation.py +218 -0
  105. harborrag_runtime/retrieval/graph_service.py +75 -0
  106. harborrag_runtime/retrieval/service.py +313 -0
  107. harborrag_runtime/retrieval/validation.py +31 -0
  108. harborrag_runtime/sdk/__init__.py +40 -0
  109. harborrag_runtime/sdk/configuration.py +40 -0
  110. harborrag_runtime/sdk/facades.py +123 -0
  111. harborrag_runtime/sdk/runtime.py +184 -0
  112. harborrag_runtime/serialization.py +35 -0
  113. harborrag_runtime/temporal/__init__.py +19 -0
  114. harborrag_runtime/temporal/activity_observability.py +268 -0
  115. harborrag_runtime/temporal/client.py +266 -0
  116. harborrag_runtime/temporal/connection.py +42 -0
  117. harborrag_runtime/temporal/conversion.py +179 -0
  118. harborrag_runtime/temporal/dispatch.py +41 -0
  119. harborrag_runtime/temporal/document_workflow.py +105 -0
  120. harborrag_runtime/temporal/failure_handling.py +20 -0
  121. harborrag_runtime/temporal/heartbeats.py +67 -0
  122. harborrag_runtime/temporal/identity.py +14 -0
  123. harborrag_runtime/temporal/ingestion_activities.py +340 -0
  124. harborrag_runtime/temporal/maintenance_activities.py +185 -0
  125. harborrag_runtime/temporal/maintenance_schemas.py +72 -0
  126. harborrag_runtime/temporal/plan_resolver.py +39 -0
  127. harborrag_runtime/temporal/policies.py +18 -0
  128. harborrag_runtime/temporal/process_isolation.py +228 -0
  129. harborrag_runtime/temporal/reindex_workflow.py +71 -0
  130. harborrag_runtime/temporal/retry_activities.py +118 -0
  131. harborrag_runtime/temporal/retry_workflow.py +147 -0
  132. harborrag_runtime/temporal/schemas.py +334 -0
  133. harborrag_runtime/temporal/source_activities.py +304 -0
  134. harborrag_runtime/temporal/source_batch_workflow.py +76 -0
  135. harborrag_runtime/temporal/source_query.py +87 -0
  136. harborrag_runtime/temporal/source_workflow.py +344 -0
  137. harborrag_runtime/temporal/submission.py +197 -0
  138. harborrag_runtime/temporal/task_registry.py +184 -0
  139. harborrag_runtime/temporal/worker.py +239 -0
  140. harborrag_runtime/temporal/worker_registry.py +132 -0
  141. harborrag_runtime/temporal_models.py +106 -0
  142. harborrag_runtime/tokenization.py +24 -0
  143. harborrag_runtime-2.0.0a1.dist-info/METADATA +276 -0
  144. harborrag_runtime-2.0.0a1.dist-info/RECORD +146 -0
  145. harborrag_runtime-2.0.0a1.dist-info/WHEEL +4 -0
  146. harborrag_runtime-2.0.0a1.dist-info/licenses/LICENSE +201 -0
@@ -0,0 +1,22 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING, Any
4
+
5
+ if TYPE_CHECKING:
6
+ from harborrag_runtime.composition import CompositionRoot
7
+ from harborrag_runtime.sdk import HarborRAG, HarborRAGConfig
8
+
9
+ __all__ = ["CompositionRoot", "HarborRAG", "HarborRAGConfig"]
10
+
11
+
12
+ def __getattr__(name: str) -> Any:
13
+ """Load the composition root lazily so configuration utilities stay standalone."""
14
+ if name == "CompositionRoot":
15
+ from harborrag_runtime.composition import CompositionRoot
16
+
17
+ return CompositionRoot
18
+ if name in {"HarborRAG", "HarborRAGConfig"}:
19
+ from harborrag_runtime.sdk import HarborRAG, HarborRAGConfig
20
+
21
+ return {"HarborRAG": HarborRAG, "HarborRAGConfig": HarborRAGConfig}[name]
22
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
@@ -0,0 +1,37 @@
1
+ """Runtime agent orchestration and retrieval-tool adapters."""
2
+
3
+ from harborrag_engine.agent import (
4
+ AgentCheckpoint,
5
+ AgentEvent,
6
+ AgentEventSink,
7
+ AgentRunIdentity,
8
+ AgentRunOptions,
9
+ AgentRunRepository,
10
+ AgentRunResult,
11
+ AgentRunStatus,
12
+ AgentService,
13
+ AgentStopReason,
14
+ AgentToolExecution,
15
+ AgentToolProvider,
16
+ AgentToolSpec,
17
+ )
18
+
19
+ from .checkpoint import DatabaseAgentRunRepository, InMemoryAgentRunRepository
20
+
21
+ __all__ = [
22
+ "AgentCheckpoint",
23
+ "AgentEvent",
24
+ "AgentEventSink",
25
+ "AgentRunIdentity",
26
+ "AgentRunOptions",
27
+ "AgentRunRepository",
28
+ "AgentRunResult",
29
+ "AgentRunStatus",
30
+ "AgentService",
31
+ "AgentStopReason",
32
+ "AgentToolExecution",
33
+ "AgentToolProvider",
34
+ "AgentToolSpec",
35
+ "DatabaseAgentRunRepository",
36
+ "InMemoryAgentRunRepository",
37
+ ]
@@ -0,0 +1,115 @@
1
+ """Agent-run checkpoint contracts and runtime implementations."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ from collections import OrderedDict
7
+ from dataclasses import dataclass
8
+ from typing import TYPE_CHECKING
9
+
10
+ from harborrag_core.contracts.errors import HarborConflictError
11
+ from harborrag_core.ports.agent_runs import AgentCheckpoint, AgentRunIdentity, AgentRunRepository
12
+
13
+ if TYPE_CHECKING:
14
+ from sqlalchemy.ext.asyncio import AsyncEngine
15
+
16
+ from harborrag_runtime.config.settings import RuntimeSettings
17
+
18
+
19
+ class InMemoryAgentRunRepository:
20
+ """Bounded process-local implementation for unit tests and local checks."""
21
+
22
+ def __init__(self, *, max_runs: int = 10_000) -> None:
23
+ if max_runs < 1:
24
+ raise ValueError("agent run repository bounds must be positive")
25
+ self._max_runs = max_runs
26
+ self._checkpoints: OrderedDict[str, AgentCheckpoint] = OrderedDict()
27
+ self._lock = asyncio.Lock()
28
+
29
+ async def create(self, checkpoint: AgentCheckpoint) -> None:
30
+ async with self._lock:
31
+ run_id = checkpoint.identity.run_id
32
+ self._checkpoints[run_id] = checkpoint
33
+ self._checkpoints.move_to_end(run_id)
34
+ while len(self._checkpoints) > self._max_runs:
35
+ self._checkpoints.popitem(last=False)
36
+
37
+ async def save_step(self, checkpoint: AgentCheckpoint) -> None:
38
+ async with self._lock:
39
+ run_id = checkpoint.identity.run_id
40
+ current = self._checkpoints.get(run_id)
41
+ if current is None or current.version != checkpoint.version - 1:
42
+ raise HarborConflictError(f"agent run {run_id!r} checkpoint version conflict")
43
+ self._checkpoints[run_id] = checkpoint
44
+ self._checkpoints.move_to_end(run_id)
45
+
46
+ async def get(self, identity: AgentRunIdentity) -> AgentCheckpoint | None:
47
+ async with self._lock:
48
+ checkpoint = self._checkpoints.get(identity.run_id)
49
+ if checkpoint is None:
50
+ return None
51
+ owner = checkpoint.identity
52
+ if (owner.tenant_id, owner.principal_id, owner.session_id) != (
53
+ identity.tenant_id,
54
+ identity.principal_id,
55
+ identity.session_id,
56
+ ):
57
+ return None
58
+ return checkpoint
59
+
60
+
61
+ @dataclass(slots=True)
62
+ class DatabaseAgentRunRepository:
63
+ """Runtime-owned SQL checkpoint plugin; production DSNs use PostgreSQL/asyncpg."""
64
+
65
+ repository: AgentRunRepository
66
+ engine: AsyncEngine
67
+
68
+ @classmethod
69
+ def configured(
70
+ cls,
71
+ settings: RuntimeSettings | None = None,
72
+ ) -> DatabaseAgentRunRepository:
73
+ """Migrate and open the configured control DB for a standalone process."""
74
+
75
+ from harborrag_adapters.repositories.database.control_plane.agent_runs import (
76
+ SqlAgentRunRepository,
77
+ )
78
+ from harborrag_adapters.repositories.database.control_plane.engine import (
79
+ create_control_plane_engine,
80
+ create_session_factory,
81
+ )
82
+ from harborrag_adapters.repositories.database.control_plane.migrations import (
83
+ run_migrations,
84
+ )
85
+ from harborrag_core.contracts.errors import HarborConfigurationError
86
+ from harborrag_runtime.config.settings import DEFAULT_CONTROL_DB_URL, RuntimeSettings
87
+
88
+ selected = settings or RuntimeSettings()
89
+ dsn = selected.control_db_url.get_secret_value()
90
+ if selected.env == "prod" and dsn == DEFAULT_CONTROL_DB_URL:
91
+ raise HarborConfigurationError(
92
+ "agent run checkpoints require HARBORRAG_CONTROL_DB_URL in production"
93
+ )
94
+ run_migrations(dsn)
95
+ engine = create_control_plane_engine(dsn)
96
+ repository = SqlAgentRunRepository(create_session_factory(engine))
97
+ return cls(repository=repository, engine=engine)
98
+
99
+ async def create(self, checkpoint: AgentCheckpoint) -> None:
100
+ await self.repository.create(checkpoint)
101
+
102
+ async def save_step(self, checkpoint: AgentCheckpoint) -> None:
103
+ await self.repository.save_step(checkpoint)
104
+
105
+ async def get(self, identity: AgentRunIdentity) -> AgentCheckpoint | None:
106
+ return await self.repository.get(identity)
107
+
108
+ async def aclose(self) -> None:
109
+ await self.engine.dispose()
110
+
111
+
112
+ __all__ = [
113
+ "DatabaseAgentRunRepository",
114
+ "InMemoryAgentRunRepository",
115
+ ]
@@ -0,0 +1,207 @@
1
+ """Canonical read-tool schemas shared by the agent loop and the MCP server.
2
+
3
+ These builders are the single definition of the retrieval tool surface. The MCP server
4
+ renders the same schemas under its own policy bounds rather than restating them, because
5
+ the two copies previously drifted -- ``max_nodes`` defaulted to the policy maximum in one
6
+ and a hardcoded 20 in the other, equal only by coincidence.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from dataclasses import dataclass
12
+
13
+ from harborrag_core.chunking import PROJECTED_RELATION_TYPES
14
+ from harborrag_core.retrieval import GraphDirection
15
+ from harborrag_engine.retrieval import RetrievalLane
16
+
17
+ _TENANT = {"type": "string", "minLength": 1, "maxLength": 128}
18
+ # Only the predicates the projection actually emits. Offering the reserved members too
19
+ # would let a caller filter on an edge type that can never match.
20
+ _RELATIONS = [item.value for item in PROJECTED_RELATION_TYPES]
21
+ _DIRECTIONS = [item.value for item in GraphDirection]
22
+ _DEFAULT_MAX_RESULTS = 20
23
+
24
+ # Every graph tool below takes a node selector. This sentence is the only thing that tells
25
+ # a caller how to obtain one, so it is repeated verbatim in each description rather than
26
+ # stated once in a place the model may not read.
27
+ _SELECTOR_HINT = (
28
+ "Node selectors accept a chunk_id returned by vector_search (chunk IDs and Chunk "
29
+ "node keys are the same value), a node_key from an earlier graph result, or an exact "
30
+ "full node title. Titles are unset on chunk nodes and are never matched partially."
31
+ )
32
+
33
+
34
+ @dataclass(frozen=True, slots=True)
35
+ class RuntimeAgentToolSpec:
36
+ name: str
37
+ description: str
38
+ input_schema: dict[str, object]
39
+ capability: str = "read"
40
+
41
+
42
+ def _relationship_types() -> dict[str, object]:
43
+ return {
44
+ "type": "array",
45
+ "items": {"type": "string", "enum": list(_RELATIONS)},
46
+ "uniqueItems": True,
47
+ "default": [],
48
+ }
49
+
50
+
51
+ def vector_search_schema(
52
+ *,
53
+ max_results: int = _DEFAULT_MAX_RESULTS,
54
+ tenant: dict[str, object] | None = None,
55
+ ) -> dict[str, object]:
56
+ return {
57
+ "type": "object",
58
+ "required": ["query", "tenant_id"],
59
+ "properties": {
60
+ "query": {"type": "string", "minLength": 1},
61
+ "tenant_id": dict(tenant or _TENANT),
62
+ "top_k": {"type": "integer", "minimum": 1, "maximum": max_results, "default": 5},
63
+ "lane": {
64
+ "type": "string",
65
+ "enum": [item.value for item in RetrievalLane],
66
+ "default": RetrievalLane.HYBRID.value,
67
+ },
68
+ "filters": {"type": "object"},
69
+ },
70
+ "additionalProperties": False,
71
+ }
72
+
73
+
74
+ def graph_triplet_schema(
75
+ *,
76
+ max_results: int = _DEFAULT_MAX_RESULTS,
77
+ tenant: dict[str, object] | None = None,
78
+ ) -> dict[str, object]:
79
+ return {
80
+ "type": "object",
81
+ "required": ["tenant_id"],
82
+ "properties": {
83
+ "tenant_id": dict(tenant or _TENANT),
84
+ "subject": {"type": "string", "minLength": 1},
85
+ "predicate": {"type": "string", "enum": list(_RELATIONS)},
86
+ "object": {"type": "string", "minLength": 1},
87
+ "limit": {"type": "integer", "minimum": 1, "maximum": max_results, "default": 10},
88
+ },
89
+ "anyOf": [
90
+ {"required": ["subject"]},
91
+ {"required": ["predicate"]},
92
+ {"required": ["object"]},
93
+ ],
94
+ "additionalProperties": False,
95
+ }
96
+
97
+
98
+ def graph_path_schema(
99
+ *,
100
+ max_results: int = _DEFAULT_MAX_RESULTS,
101
+ tenant: dict[str, object] | None = None,
102
+ ) -> dict[str, object]:
103
+ return {
104
+ "type": "object",
105
+ "required": ["tenant_id", "start_node", "end_node"],
106
+ "properties": {
107
+ "tenant_id": dict(tenant or _TENANT),
108
+ "start_node": {"type": "string", "minLength": 1},
109
+ "end_node": {"type": "string", "minLength": 1},
110
+ "relationship_types": _relationship_types(),
111
+ "max_depth": {"type": "integer", "minimum": 1, "maximum": 8, "default": 4},
112
+ "max_paths": {
113
+ "type": "integer",
114
+ "minimum": 1,
115
+ "maximum": max_results,
116
+ "default": 10,
117
+ },
118
+ "direction": {
119
+ "type": "string",
120
+ "enum": list(_DIRECTIONS),
121
+ "default": GraphDirection.BOTH.value,
122
+ },
123
+ },
124
+ "additionalProperties": False,
125
+ }
126
+
127
+
128
+ def graph_subgraph_schema(
129
+ *,
130
+ max_results: int = _DEFAULT_MAX_RESULTS,
131
+ tenant: dict[str, object] | None = None,
132
+ ) -> dict[str, object]:
133
+ return {
134
+ "type": "object",
135
+ "required": ["tenant_id", "start_node"],
136
+ "properties": {
137
+ "tenant_id": dict(tenant or _TENANT),
138
+ "start_node": {"type": "string", "minLength": 1},
139
+ "relationship_types": _relationship_types(),
140
+ "max_depth": {"type": "integer", "minimum": 1, "maximum": 8, "default": 2},
141
+ "max_nodes": {
142
+ "type": "integer",
143
+ "minimum": 1,
144
+ "maximum": max_results,
145
+ "default": max_results,
146
+ },
147
+ "direction": {
148
+ "type": "string",
149
+ "enum": list(_DIRECTIONS),
150
+ "default": GraphDirection.BOTH.value,
151
+ },
152
+ },
153
+ "additionalProperties": False,
154
+ }
155
+
156
+
157
+ VECTOR_SEARCH_DESCRIPTION = (
158
+ "Search tenant-scoped indexed evidence for a natural-language query. Each result "
159
+ "carries a chunk_id that doubles as a graph node key for the graph tools."
160
+ )
161
+ GRAPH_TRIPLET_DESCRIPTION = (
162
+ f"Find active subject-predicate-object records in the tenant knowledge graph. {_SELECTOR_HINT}"
163
+ )
164
+ GRAPH_PATH_DESCRIPTION = (
165
+ f"Find active graph paths between two tenant-scoped nodes. Defaults to an "
166
+ f"undirected walk, because the spine is not uniformly directed. {_SELECTOR_HINT}"
167
+ )
168
+ GRAPH_SUBGRAPH_DESCRIPTION = (
169
+ f"Expand an active tenant-scoped graph neighborhood from one known node. {_SELECTOR_HINT}"
170
+ )
171
+
172
+
173
+ RUNTIME_AGENT_TOOL_SPECS = (
174
+ RuntimeAgentToolSpec(
175
+ "vector_search",
176
+ VECTOR_SEARCH_DESCRIPTION,
177
+ vector_search_schema(),
178
+ ),
179
+ RuntimeAgentToolSpec(
180
+ "graph_triplet_search",
181
+ GRAPH_TRIPLET_DESCRIPTION,
182
+ graph_triplet_schema(),
183
+ ),
184
+ RuntimeAgentToolSpec(
185
+ "graph_path_search",
186
+ GRAPH_PATH_DESCRIPTION,
187
+ graph_path_schema(),
188
+ ),
189
+ RuntimeAgentToolSpec(
190
+ "graph_subgraph_search",
191
+ GRAPH_SUBGRAPH_DESCRIPTION,
192
+ graph_subgraph_schema(),
193
+ ),
194
+ )
195
+
196
+ __all__ = [
197
+ "GRAPH_PATH_DESCRIPTION",
198
+ "GRAPH_SUBGRAPH_DESCRIPTION",
199
+ "GRAPH_TRIPLET_DESCRIPTION",
200
+ "RUNTIME_AGENT_TOOL_SPECS",
201
+ "RuntimeAgentToolSpec",
202
+ "VECTOR_SEARCH_DESCRIPTION",
203
+ "graph_path_schema",
204
+ "graph_subgraph_schema",
205
+ "graph_triplet_schema",
206
+ "vector_search_schema",
207
+ ]
@@ -0,0 +1,222 @@
1
+ """Runtime adapter exposing retrieval façades as bounded agent read tools."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ from dataclasses import asdict, dataclass
7
+ from typing import TYPE_CHECKING, cast
8
+
9
+ from harborrag_core.chunking import RelationType
10
+ from harborrag_core.retrieval import (
11
+ GraphDirection,
12
+ GraphPathQuery,
13
+ GraphSubgraphQuery,
14
+ GraphTripletQuery,
15
+ compact_node,
16
+ compact_path,
17
+ compact_relation,
18
+ compact_triplet,
19
+ )
20
+ from harborrag_core.schemas.ids import TenantId
21
+ from harborrag_core.security import AccessContext
22
+ from harborrag_engine.agent import AgentToolSpec
23
+ from harborrag_engine.retrieval import RetrievalLane
24
+ from harborrag_runtime.agent.tool_specs import RUNTIME_AGENT_TOOL_SPECS
25
+ from harborrag_runtime.contracts import (
26
+ GraphPathRequest,
27
+ GraphSubgraphRequest,
28
+ GraphTripletRequest,
29
+ RetrievalRequest,
30
+ )
31
+
32
+ if TYPE_CHECKING:
33
+ from harborrag_runtime.sdk import HarborRAG
34
+
35
+ logger = logging.getLogger("harborrag.runtime.agent.tools")
36
+
37
+
38
+ @dataclass(slots=True)
39
+ class RuntimeAgentToolProvider:
40
+ """Translate engine tool calls into the shared runtime SDK façades."""
41
+
42
+ runtime: HarborRAG
43
+
44
+ def list_tools(self, tenant_id: str | None = None) -> list[AgentToolSpec]:
45
+ del tenant_id
46
+ return [cast("AgentToolSpec", spec) for spec in RUNTIME_AGENT_TOOL_SPECS]
47
+
48
+ async def call_tool(
49
+ self,
50
+ name: str,
51
+ arguments: dict[str, object] | None = None,
52
+ *,
53
+ principal_id: str = "in-process",
54
+ ) -> dict[str, object]:
55
+ values = dict(arguments or {})
56
+ try:
57
+ access = _access(values, principal_id)
58
+ operations = {
59
+ "vector_search": self._vector,
60
+ "graph_triplet_search": self._triplets,
61
+ "graph_path_search": self._paths,
62
+ "graph_subgraph_search": self._subgraph,
63
+ }
64
+ operation = operations.get(name)
65
+ if operation is None:
66
+ return {"ok": False, "error": "agent tool is not available"}
67
+ return await operation(values, access)
68
+ except (TypeError, ValueError) as exc:
69
+ return {"ok": False, "error": str(exc)}
70
+ except Exception:
71
+ logger.exception("agent retrieval tool %r raised during call_tool", name)
72
+ return {"ok": False, "error": "agent retrieval tool failed"}
73
+
74
+ async def _vector(
75
+ self,
76
+ values: dict[str, object],
77
+ access: AccessContext,
78
+ ) -> dict[str, object]:
79
+ lane = RetrievalLane(_optional_text(values, "lane") or RetrievalLane.HYBRID.value)
80
+ response = await self.runtime.retrieval.search(
81
+ RetrievalRequest(
82
+ access=access,
83
+ query=_text(values, "query"),
84
+ top_k=_integer(values, "top_k", default=5, maximum=20),
85
+ filters=_mapping(values, "filters"),
86
+ lane=lane,
87
+ observe_graph=False,
88
+ )
89
+ )
90
+ return {
91
+ "ok": True,
92
+ "results": [asdict(item) for item in response.results],
93
+ "diagnostics": response.diagnostics,
94
+ }
95
+
96
+ async def _triplets(
97
+ self,
98
+ values: dict[str, object],
99
+ access: AccessContext,
100
+ ) -> dict[str, object]:
101
+ predicate = _optional_text(values, "predicate")
102
+ response = await self.runtime.graph.search_triplets(
103
+ GraphTripletRequest(
104
+ access=access,
105
+ query=GraphTripletQuery(
106
+ subject=_optional_text(values, "subject"),
107
+ predicate=RelationType(predicate) if predicate is not None else None,
108
+ object=_optional_text(values, "object"),
109
+ limit=_integer(values, "limit", default=10, maximum=20),
110
+ ),
111
+ )
112
+ )
113
+ return {
114
+ "ok": True,
115
+ "triplets": [compact_triplet(item) for item in response.triplets],
116
+ "diagnostics": response.diagnostics,
117
+ }
118
+
119
+ async def _paths(
120
+ self,
121
+ values: dict[str, object],
122
+ access: AccessContext,
123
+ ) -> dict[str, object]:
124
+ response = await self.runtime.graph.find_paths(
125
+ GraphPathRequest(
126
+ access=access,
127
+ query=GraphPathQuery(
128
+ start_node=_text(values, "start_node"),
129
+ end_node=_text(values, "end_node"),
130
+ relationship_types=_relations(values),
131
+ max_depth=_integer(values, "max_depth", default=4, maximum=8),
132
+ max_paths=_integer(values, "max_paths", default=10, maximum=20),
133
+ direction=_direction(values, GraphDirection.BOTH),
134
+ ),
135
+ )
136
+ )
137
+ return {
138
+ "ok": True,
139
+ "paths": [compact_path(item) for item in response.paths],
140
+ "diagnostics": response.diagnostics,
141
+ }
142
+
143
+ async def _subgraph(
144
+ self,
145
+ values: dict[str, object],
146
+ access: AccessContext,
147
+ ) -> dict[str, object]:
148
+ response = await self.runtime.graph.expand_subgraph(
149
+ GraphSubgraphRequest(
150
+ access=access,
151
+ query=GraphSubgraphQuery(
152
+ start_node=_text(values, "start_node"),
153
+ relationship_types=_relations(values),
154
+ max_depth=_integer(values, "max_depth", default=2, maximum=8),
155
+ max_nodes=_integer(values, "max_nodes", default=20, maximum=20),
156
+ direction=_direction(values, GraphDirection.BOTH),
157
+ ),
158
+ )
159
+ )
160
+ return {
161
+ "ok": True,
162
+ "nodes": [compact_node(item) for item in response.nodes],
163
+ "relations": [compact_relation(item) for item in response.relations],
164
+ "diagnostics": response.diagnostics,
165
+ }
166
+
167
+
168
+ def _access(values: dict[str, object], principal_id: str) -> AccessContext:
169
+ return AccessContext(
170
+ principal_id=principal_id,
171
+ tenant_id=TenantId(_text(values, "tenant_id")),
172
+ )
173
+
174
+
175
+ def _text(values: dict[str, object], name: str) -> str:
176
+ value = values.get(name)
177
+ if not isinstance(value, str) or not value.strip():
178
+ raise ValueError(f"{name} must be a non-empty string")
179
+ return value.strip()
180
+
181
+
182
+ def _optional_text(values: dict[str, object], name: str) -> str | None:
183
+ return _text(values, name) if values.get(name) is not None else None
184
+
185
+
186
+ def _integer(
187
+ values: dict[str, object],
188
+ name: str,
189
+ *,
190
+ default: int,
191
+ maximum: int,
192
+ ) -> int:
193
+ value = values.get(name, default)
194
+ if isinstance(value, bool) or not isinstance(value, int) or not 1 <= value <= maximum:
195
+ raise ValueError(f"{name} must be between 1 and {maximum}")
196
+ return value
197
+
198
+
199
+ def _mapping(values: dict[str, object], name: str) -> dict[str, object]:
200
+ value = values.get(name, {})
201
+ if not isinstance(value, dict):
202
+ raise TypeError(f"{name} must be an object")
203
+ if "tenant_id" in value:
204
+ raise ValueError("tenant_id is not allowed inside filters")
205
+ return dict(value)
206
+
207
+
208
+ def _relations(values: dict[str, object]) -> tuple[RelationType, ...]:
209
+ value = values.get("relationship_types", [])
210
+ if not isinstance(value, list) or not all(isinstance(item, str) for item in value):
211
+ raise TypeError("relationship_types must be an array of strings")
212
+ return tuple(RelationType(item) for item in value)
213
+
214
+
215
+ def _direction(
216
+ values: dict[str, object],
217
+ default: GraphDirection,
218
+ ) -> GraphDirection:
219
+ return GraphDirection(_optional_text(values, "direction") or default.value)
220
+
221
+
222
+ __all__ = ["RuntimeAgentToolProvider"]
@@ -0,0 +1,30 @@
1
+ # Runtime chat
2
+
3
+ This package owns provider-neutral chat orchestration. Transport packages call
4
+ the `ChatFacade`; they do not construct model adapters or read prompt files.
5
+
6
+ ```text
7
+ chat/
8
+ ├── facade.py # stable HarborRAG SDK surface
9
+ ├── service.py # prompt application and async client lifecycle
10
+ ├── composition.py # model-catalog to AsyncHarborChatClient wiring
11
+ └── prompts/
12
+ ├── catalog.py # typed prompt names and safe resource lookup
13
+ └── templates/
14
+ ├── default.md
15
+ └── concise.md
16
+ ```
17
+
18
+ Prompt templates are server-owned system messages. A selected template is
19
+ prepended before request messages; selecting no prompt leaves messages
20
+ unchanged. Templates never contain provider credentials or tenant data.
21
+
22
+ To add a prompt:
23
+
24
+ 1. add a UTF-8 Markdown file under `prompts/templates/`;
25
+ 2. add its stable public name and filename to `ChatPrompt` and
26
+ `PromptCatalog._FILENAMES`;
27
+ 3. add catalog and transport-contract tests.
28
+
29
+ Model/provider selection remains in `config/models.yaml`; prompt files should
30
+ contain behavior instructions only.
@@ -0,0 +1,7 @@
1
+ """Chat completion, prompt, and provider composition services."""
2
+
3
+ from .facade import ChatFacade
4
+ from .prompts import ChatPrompt, PromptCatalog
5
+ from .service import RuntimeChatService
6
+
7
+ __all__ = ["ChatFacade", "ChatPrompt", "PromptCatalog", "RuntimeChatService"]
@@ -0,0 +1,17 @@
1
+ """Provider-client composition for runtime chat services."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from harborrag_adapters.models.chat import (
6
+ AsyncHarborChatClient,
7
+ ChatClientFactory,
8
+ HarborChatClientConfig,
9
+ )
10
+ from harborrag_runtime.config.settings import RuntimeSettings
11
+
12
+
13
+ def build_chat_client(settings: RuntimeSettings) -> AsyncHarborChatClient:
14
+ """Build the async client from the shared model catalog."""
15
+
16
+ config = HarborChatClientConfig.from_file(settings.model_config_path)
17
+ return ChatClientFactory.create_async(config)