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.
- harborrag_runtime/__init__.py +22 -0
- harborrag_runtime/agent/__init__.py +37 -0
- harborrag_runtime/agent/checkpoint.py +115 -0
- harborrag_runtime/agent/tool_specs.py +207 -0
- harborrag_runtime/agent/tools.py +222 -0
- harborrag_runtime/chat/README.md +30 -0
- harborrag_runtime/chat/__init__.py +7 -0
- harborrag_runtime/chat/composition.py +17 -0
- harborrag_runtime/chat/facade.py +36 -0
- harborrag_runtime/chat/prompts/__init__.py +5 -0
- harborrag_runtime/chat/prompts/catalog.py +43 -0
- harborrag_runtime/chat/prompts/templates/__init__.py +1 -0
- harborrag_runtime/chat/prompts/templates/concise.md +1 -0
- harborrag_runtime/chat/prompts/templates/default.md +1 -0
- harborrag_runtime/chat/service.py +93 -0
- harborrag_runtime/composition/__init__.py +5 -0
- harborrag_runtime/composition/control_plane.py +277 -0
- harborrag_runtime/composition/resources.py +120 -0
- harborrag_runtime/config/__init__.py +58 -0
- harborrag_runtime/config/connectors/__init__.py +19 -0
- harborrag_runtime/config/connectors/fingerprints.py +54 -0
- harborrag_runtime/config/connectors/loader.py +95 -0
- harborrag_runtime/config/connectors/parsing.py +237 -0
- harborrag_runtime/config/connectors/providers.py +193 -0
- harborrag_runtime/config/connectors/schemas.py +216 -0
- harborrag_runtime/config/errors.py +14 -0
- harborrag_runtime/config/loading.py +245 -0
- harborrag_runtime/config/parsers/__init__.py +19 -0
- harborrag_runtime/config/parsers/loader.py +65 -0
- harborrag_runtime/config/parsers/parsing.py +230 -0
- harborrag_runtime/config/parsers/providers.py +228 -0
- harborrag_runtime/config/parsers/schemas.py +228 -0
- harborrag_runtime/config/settings.py +158 -0
- harborrag_runtime/config/temporal.py +299 -0
- harborrag_runtime/config/temporal_loading.py +285 -0
- harborrag_runtime/contracts.py +143 -0
- harborrag_runtime/document_stage_catalog.py +84 -0
- harborrag_runtime/errors.py +60 -0
- harborrag_runtime/events/__init__.py +3 -0
- harborrag_runtime/events/in_process.py +102 -0
- harborrag_runtime/execution/__init__.py +26 -0
- harborrag_runtime/execution/contracts.py +32 -0
- harborrag_runtime/execution/direct.py +49 -0
- harborrag_runtime/execution/submission.py +47 -0
- harborrag_runtime/execution/temporal.py +80 -0
- harborrag_runtime/ingestion/README.md +53 -0
- harborrag_runtime/ingestion/__init__.py +101 -0
- harborrag_runtime/ingestion/composition.py +187 -0
- harborrag_runtime/ingestion/connector_metrics.py +14 -0
- harborrag_runtime/ingestion/discovery_metrics.py +46 -0
- harborrag_runtime/ingestion/document/__init__.py +14 -0
- harborrag_runtime/ingestion/document/capture.py +339 -0
- harborrag_runtime/ingestion/document/dependencies.py +46 -0
- harborrag_runtime/ingestion/document/lifecycle.py +266 -0
- harborrag_runtime/ingestion/document/materialization.py +208 -0
- harborrag_runtime/ingestion/document/materialization_helpers.py +47 -0
- harborrag_runtime/ingestion/document/models.py +53 -0
- harborrag_runtime/ingestion/document/normalization.py +29 -0
- harborrag_runtime/ingestion/document/normalizers/__init__.py +19 -0
- harborrag_runtime/ingestion/document/normalizers/builder.py +98 -0
- harborrag_runtime/ingestion/document/normalizers/builtins.py +40 -0
- harborrag_runtime/ingestion/document/normalizers/router.py +45 -0
- harborrag_runtime/ingestion/document/normalizers/transform.py +30 -0
- harborrag_runtime/ingestion/document/pipeline.py +206 -0
- harborrag_runtime/ingestion/document/preparation.py +103 -0
- harborrag_runtime/ingestion/document/projection.py +258 -0
- harborrag_runtime/ingestion/document/projection_coordinator.py +131 -0
- harborrag_runtime/ingestion/document/projection_material.py +157 -0
- harborrag_runtime/ingestion/document/service.py +45 -0
- harborrag_runtime/ingestion/document/stage_models.py +49 -0
- harborrag_runtime/ingestion/limits.py +52 -0
- harborrag_runtime/ingestion/maintenance/__init__.py +1 -0
- harborrag_runtime/ingestion/maintenance/cleanup.py +180 -0
- harborrag_runtime/ingestion/maintenance/projection_admin.py +147 -0
- harborrag_runtime/ingestion/maintenance/reindex.py +257 -0
- harborrag_runtime/ingestion/maintenance/reindex_plan.py +51 -0
- harborrag_runtime/ingestion/maintenance/reindex_reuse.py +138 -0
- harborrag_runtime/ingestion/maintenance/relation_repair.py +293 -0
- harborrag_runtime/ingestion/maintenance/relation_supersession.py +56 -0
- harborrag_runtime/ingestion/observability.py +294 -0
- harborrag_runtime/ingestion/observability_types.py +46 -0
- harborrag_runtime/ingestion/profiles.py +37 -0
- harborrag_runtime/ingestion/runtime_builder.py +333 -0
- harborrag_runtime/ingestion/source/__init__.py +1 -0
- harborrag_runtime/ingestion/source/descriptor_mapping.py +141 -0
- harborrag_runtime/ingestion/source/discovery.py +255 -0
- harborrag_runtime/ingestion/source/documents.py +150 -0
- harborrag_runtime/ingestion/source/finalization.py +190 -0
- harborrag_runtime/ingestion/source/models.py +165 -0
- harborrag_runtime/ingestion/source/operations.py +159 -0
- harborrag_runtime/ingestion/source/plan.py +237 -0
- harborrag_runtime/ingestion/source/retry.py +132 -0
- harborrag_runtime/ingestion/source/service.py +303 -0
- harborrag_runtime/ingestion/source/tasks.py +48 -0
- harborrag_runtime/ingestion/stage_observation.py +96 -0
- harborrag_runtime/memory/__init__.py +29 -0
- harborrag_runtime/memory/composition.py +43 -0
- harborrag_runtime/memory/service.py +120 -0
- harborrag_runtime/plugins.py +56 -0
- harborrag_runtime/rate_limiting.py +39 -0
- harborrag_runtime/retrieval/__init__.py +25 -0
- harborrag_runtime/retrieval/composition.py +109 -0
- harborrag_runtime/retrieval/contracts.py +154 -0
- harborrag_runtime/retrieval/graph_observation.py +218 -0
- harborrag_runtime/retrieval/graph_service.py +75 -0
- harborrag_runtime/retrieval/service.py +313 -0
- harborrag_runtime/retrieval/validation.py +31 -0
- harborrag_runtime/sdk/__init__.py +40 -0
- harborrag_runtime/sdk/configuration.py +40 -0
- harborrag_runtime/sdk/facades.py +123 -0
- harborrag_runtime/sdk/runtime.py +184 -0
- harborrag_runtime/serialization.py +35 -0
- harborrag_runtime/temporal/__init__.py +19 -0
- harborrag_runtime/temporal/activity_observability.py +268 -0
- harborrag_runtime/temporal/client.py +266 -0
- harborrag_runtime/temporal/connection.py +42 -0
- harborrag_runtime/temporal/conversion.py +179 -0
- harborrag_runtime/temporal/dispatch.py +41 -0
- harborrag_runtime/temporal/document_workflow.py +105 -0
- harborrag_runtime/temporal/failure_handling.py +20 -0
- harborrag_runtime/temporal/heartbeats.py +67 -0
- harborrag_runtime/temporal/identity.py +14 -0
- harborrag_runtime/temporal/ingestion_activities.py +340 -0
- harborrag_runtime/temporal/maintenance_activities.py +185 -0
- harborrag_runtime/temporal/maintenance_schemas.py +72 -0
- harborrag_runtime/temporal/plan_resolver.py +39 -0
- harborrag_runtime/temporal/policies.py +18 -0
- harborrag_runtime/temporal/process_isolation.py +228 -0
- harborrag_runtime/temporal/reindex_workflow.py +71 -0
- harborrag_runtime/temporal/retry_activities.py +118 -0
- harborrag_runtime/temporal/retry_workflow.py +147 -0
- harborrag_runtime/temporal/schemas.py +334 -0
- harborrag_runtime/temporal/source_activities.py +304 -0
- harborrag_runtime/temporal/source_batch_workflow.py +76 -0
- harborrag_runtime/temporal/source_query.py +87 -0
- harborrag_runtime/temporal/source_workflow.py +344 -0
- harborrag_runtime/temporal/submission.py +197 -0
- harborrag_runtime/temporal/task_registry.py +184 -0
- harborrag_runtime/temporal/worker.py +239 -0
- harborrag_runtime/temporal/worker_registry.py +132 -0
- harborrag_runtime/temporal_models.py +106 -0
- harborrag_runtime/tokenization.py +24 -0
- harborrag_runtime-2.0.0a1.dist-info/METADATA +276 -0
- harborrag_runtime-2.0.0a1.dist-info/RECORD +146 -0
- harborrag_runtime-2.0.0a1.dist-info/WHEEL +4 -0
- 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)
|