-
Notifications
You must be signed in to change notification settings - Fork 17
Expand file tree
/
Copy pathconcierge.py
More file actions
166 lines (144 loc) · 6.71 KB
/
concierge.py
File metadata and controls
166 lines (144 loc) · 6.71 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
"""ContextConcierge — the assembly point that builds one HarnessContext.
This is the only module allowed to import the concrete semantic/safety/adapter
classes; everywhere else depends on the ``core.ports`` Protocols. Per request it
picks an LLM (OpenAI when keyed, else the FakeLLM), restores or starts a
:class:`Session`, and wires the explorer, safety pipeline, scope resolver, and
audit store into a :class:`HarnessContext` for the loop.
Dependency-injection friendly: every collaborator can be overridden in the
ctor so tests (and v1.5 swaps) need no network and no globals.
"""
from __future__ import annotations
import os
from ..adapters.db.factory import build_explorer, explorer_from_env
from ..adapters.db.postgres_explorer import PostgresExplorer
from ..adapters.llm.fake import FakeLLM
from ..adapters.llm.openai_ import OpenAILLM
from ..adapters.storage.sqlite_semantic import SqliteSemanticStore
from ..adapters.storage.sqlite_store import SqliteStore
from ..core.identity import Identity
from ..core.ports.audit import AuditPort
from ..core.ports.explorer import ExplorerPort
from ..core.ports.llm import LLMPort
from ..core.ports.safety import SafetyPipelinePort
from ..core.ports.secrets import SecretsPort
from ..core.ports.semantic_scope import ScopeResolverPort
from ..harness.context import HarnessContext
from ..harness.session import Session
from ..harness.tool_registry import ToolRegistry
from ..ingestion import FileSource, IngestionPipeline, LLMExtractor
from ..memory import InjectAllRecall, InMemoryStore, ManualExtractor, MemoryService
from ..safety.pipeline import SafetyPipeline
from ..tools import build_default_tools
from .encrypted_secrets import EncryptedSecrets
from .scope_resolver import ScopeResolver
# DSN used for the V1 explorer stub when a scope has registered none yet.
_DEFAULT_DSN = "postgresql://stub/v1"
class ContextConcierge:
"""Assembles per-request :class:`HarnessContext` from injected ports."""
def __init__(
self,
*,
path: str = ":memory:",
store: SqliteStore | None = None,
llm: LLMPort | None = None,
explorer: ExplorerPort | None = None,
safety: SafetyPipelinePort | None = None,
scope_resolver: ScopeResolverPort | None = None,
secrets: SecretsPort | None = None,
audit: AuditPort | None = None,
max_turns: int = 8,
) -> None:
# ``path`` drives the default persistence backends; ``:memory:`` keeps
# tests isolated, a file path makes sessions/definitions/secrets durable.
self._store = store if store is not None else SqliteStore(path)
# Audit + session persistence both ride the one sqlite store by default.
self._llm = llm if llm is not None else _default_llm()
# Explorer precedence: explicit injection → env-configured real DB
# (LANG2SQL_DB_URL / Cloudflare D1) → the canned stub for offline dev.
self._explorer = explorer or explorer_from_env() or PostgresExplorer(_DEFAULT_DSN)
self._safety = safety if safety is not None else SafetyPipeline()
# Persistent semantic store by default so definitions survive restart.
self._scope_resolver = (
scope_resolver
if scope_resolver is not None
else ScopeResolver(SqliteSemanticStore(path))
)
# Secrets share the session/audit store's kv table (and sqlite file).
self._secrets = (
secrets if secrets is not None else EncryptedSecrets(self._store)
)
self._audit = audit if audit is not None else self._store
self._max_turns = max_turns
# V1 memory (in-memory + inject-all + manual) and ingestion (file × LLM).
self._memory = MemoryService(InMemoryStore(), InjectAllRecall(), ManualExtractor())
self._ingestion = IngestionPipeline()
self._source = FileSource()
self._extractor = LLMExtractor(self._llm)
# Per-scope explorer cache. /setup stores a DSN under the guild scope;
# the next build_context for that scope materialises an explorer from
# it on demand and reuses it across turns (lazy + cached).
self._scope_explorers: dict[str, ExplorerPort] = {}
@property
def store(self) -> SqliteStore:
return self._store
@property
def secrets(self) -> SecretsPort:
"""Per-scope encrypted credential store (DSNs/API keys via ``/connect``)."""
return self._secrets
@property
def scope_resolver(self) -> ScopeResolverPort:
"""Federation resolver over the (by default persistent) semantic store."""
return self._scope_resolver
def forget_explorer(self, scope: str) -> None:
"""Bust the cached explorer for ``scope`` (call after /setup updates a DSN)."""
self._scope_explorers.pop(scope, None)
async def _explorer_for(self, identity: Identity) -> ExplorerPort:
"""Pick the right explorer for this identity's guild scope.
If the wizard has stored a DSN for the guild (under ``db_dsn`` in
secrets), build an explorer from it (cached). Otherwise fall back to
the concierge's default explorer (env-configured or stub).
"""
scope = identity.guild_id or f"dm:{identity.user_id}"
cached = self._scope_explorers.get(scope)
if cached is not None:
return cached
dsn = await self._secrets.get(scope, "db_dsn")
if not dsn:
return self._explorer
extras: dict[str, str] = {}
d1_token = await self._secrets.get(scope, "db_extras.d1_token")
if d1_token:
extras["d1_token"] = d1_token
explorer = build_explorer(dsn, extras=extras or None)
self._scope_explorers[scope] = explorer
return explorer
async def build_context(
self, identity: Identity, user_text: str | None = None
) -> HarnessContext:
session = await self._store.load(identity.session_key())
if session is None:
session = Session(identity=identity)
tools = ToolRegistry(
build_default_tools(
memory=self._memory,
ingestion=self._ingestion,
source=self._source,
extractor=self._extractor,
)
)
return HarnessContext(
identity=identity,
llm=self._llm,
tools=tools,
session=session,
explorer=await self._explorer_for(identity),
safety=self._safety,
audit=self._audit,
scope_resolver=self._scope_resolver,
max_turns=self._max_turns,
)
def _default_llm() -> LLMPort:
"""OpenAI when a key is present, otherwise the offline FakeLLM."""
if os.environ.get("OPENAI_API_KEY"):
return OpenAILLM()
return FakeLLM()