friday-framework-runtime 0.1.0a0__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.
friday_runtime/di.py ADDED
@@ -0,0 +1,342 @@
1
+ """
2
+ Dependency Injection container for the Runtime Kernel.
3
+
4
+ Extracts the boot sequence from FridayContainer into reusable
5
+ factory methods. Each method creates a single service or group
6
+ of related services.
7
+
8
+ Ref: Phase 1 Build Plan, Step 4.2
9
+ """
10
+
11
+ import logging
12
+ from typing import Any, cast
13
+
14
+ from friday_agent import DefaultToolRegistry
15
+ from friday_core import TelemetryInterface
16
+ from friday_core.interfaces import (
17
+ EmbeddingModel,
18
+ EpisodicStoreAdapter,
19
+ LLMClient,
20
+ TranscriptBackedEpisodicStore,
21
+ )
22
+ from friday_core.profiles import (
23
+ MemoryProfile,
24
+ create_graph_store,
25
+ create_vector_store,
26
+ )
27
+ from friday_core.telemetry import DebugTelemetryRecorder, OpenTelemetryProvider
28
+ from friday_llm import LiteLLMEmbeddingService, LiteLLMService
29
+ from friday_llm.config import ResilienceConfig
30
+ from friday_memory import (
31
+ CollectionConfig,
32
+ CollectionManager,
33
+ ContextAssembler,
34
+ InMemoryEpisodicStore,
35
+ MemoryController,
36
+ MemoryExtractor,
37
+ MultiAgentMemoryController,
38
+ NamespacePolicyEngine,
39
+ OntologyConfig,
40
+ PromotionCoordinator,
41
+ PromotionJournal,
42
+ )
43
+ from friday_transcript import SQLiteTranscriptStore
44
+
45
+ from friday_runtime.config import RuntimeConfig
46
+ from friday_runtime.tool_loading import register_runtime_tools
47
+
48
+ logger = logging.getLogger(__name__)
49
+
50
+
51
+ class DIContainer:
52
+ """
53
+ Dependency Injection container that holds all created services.
54
+
55
+ The boot sequence mirrors the legacy FridayContainer.initialize()
56
+ order: telemetry -> LLM -> storage -> memory -> context -> tools.
57
+ """
58
+
59
+ def __init__(
60
+ self,
61
+ config: RuntimeConfig,
62
+ memory_profile: MemoryProfile,
63
+ ) -> None:
64
+ """
65
+ Initialize the DI container.
66
+
67
+ Args:
68
+ config: Runtime configuration.
69
+ memory_profile: Memory storage profile.
70
+ """
71
+ self._config = config
72
+ self._memory_profile = memory_profile
73
+
74
+ # Service slots (populated during boot)
75
+ self.telemetry: TelemetryInterface | None = None
76
+ self.llm: LiteLLMService | None = None
77
+ self.llm_pool: dict[str, LiteLLMService] = {}
78
+ self.embedding_pool: dict[str, LiteLLMEmbeddingService] = {}
79
+ self.embeddings: LiteLLMEmbeddingService | None = None
80
+ self.vector_store: Any = None
81
+ self.graph_store: Any = None
82
+ self.episodic_store: EpisodicStoreAdapter | None = None
83
+ self.transcript_store: SQLiteTranscriptStore | None = None
84
+ self.memory: MemoryController | None = None
85
+ self.multi_agent_memory: MultiAgentMemoryController | None = None
86
+ self.multi_agent_journal: PromotionJournal | None = None
87
+ self.multi_agent_coordinator: PromotionCoordinator | None = None
88
+ self.context_engine: ContextAssembler | None = None
89
+ self.tool_registry: DefaultToolRegistry | None = None
90
+
91
+ async def boot(self) -> None:
92
+ """
93
+ Execute the full boot sequence.
94
+
95
+ Creates all eager services in dependency order.
96
+ Raises on failure — partial initialization is not supported.
97
+ """
98
+ self._create_telemetry()
99
+ self._create_llm_services()
100
+ await self._create_storage_adapters()
101
+ self._create_memory_controller()
102
+ self._create_multi_agent_stack()
103
+ self._create_context_engine()
104
+ self._create_tool_registry()
105
+ logger.info("DI container boot complete")
106
+
107
+ async def shutdown(self) -> None:
108
+ """
109
+ Gracefully shut down all services.
110
+
111
+ Safe to call multiple times.
112
+ """
113
+ logger.info("Shutting down DI container...")
114
+ # Currently ChromaDB handles its own cleanup.
115
+ # Future: flush telemetry, persist memory, etc.
116
+ logger.info("DI container shutdown complete")
117
+
118
+ # --- Private Boot Methods ---
119
+
120
+ def _create_telemetry(self) -> None:
121
+ """Create telemetry interface based on config."""
122
+ if self._config.debug_mode:
123
+ self.telemetry = DebugTelemetryRecorder()
124
+ logger.info("Using DebugTelemetryRecorder for telemetry")
125
+ elif self._config.telemetry_enabled:
126
+ self.telemetry = OpenTelemetryProvider(
127
+ service_name="friday",
128
+ endpoint=self._config.telemetry_endpoint,
129
+ )
130
+ logger.info("Using OpenTelemetryProvider for telemetry")
131
+ else:
132
+ self.telemetry = None
133
+ logger.info("Telemetry disabled")
134
+
135
+ def _create_llm_services(self) -> None:
136
+ """Create LLM and embedding services."""
137
+ self.llm_pool = {}
138
+ for service_name, service_config in self._config.llm_pool.services.items():
139
+ self.llm_pool[service_name] = LiteLLMService(
140
+ model_name=service_config.model,
141
+ api_key=service_config.api_key,
142
+ api_base=service_config.api_base,
143
+ telemetry=self.telemetry,
144
+ resilience=ResilienceConfig(
145
+ max_retries=service_config.resilience.max_retries,
146
+ timeout_seconds=float(service_config.resilience.timeout_seconds),
147
+ ),
148
+ )
149
+
150
+ self.llm = self.llm_pool[self._config.llm_pool.default_service]
151
+ default_service = self._config.get_llm_service_config()
152
+
153
+ self.embedding_pool = {}
154
+ for (
155
+ service_name,
156
+ embedding_service_config,
157
+ ) in self._config.embedding_pool.services.items():
158
+ self.embedding_pool[service_name] = LiteLLMEmbeddingService(
159
+ model_name=embedding_service_config.model,
160
+ api_key=embedding_service_config.api_key,
161
+ api_base=embedding_service_config.api_base,
162
+ telemetry=self.telemetry,
163
+ resilience=ResilienceConfig(
164
+ max_retries=embedding_service_config.resilience.max_retries,
165
+ timeout_seconds=float(
166
+ embedding_service_config.resilience.timeout_seconds
167
+ ),
168
+ ),
169
+ )
170
+
171
+ self.embeddings = self.embedding_pool[self._config.embedding_pool.default_service]
172
+ default_embedding_service = self._config.get_embedding_service_config()
173
+
174
+ logger.info(
175
+ "LLM pool configured with default service '%s' (%s) across %d services; "
176
+ "embedding pool default '%s' (%s) across %d services",
177
+ self._config.llm_pool.default_service,
178
+ default_service.model,
179
+ len(self.llm_pool),
180
+ self._config.embedding_pool.default_service,
181
+ default_embedding_service.model,
182
+ len(self.embedding_pool),
183
+ )
184
+
185
+ async def _create_storage_adapters(self) -> None:
186
+ """Create storage adapter instances based on memory profile."""
187
+ assert self.embeddings is not None
188
+ graph_storage_path = self._config.resolve_runtime_state_path("memory/graph.json")
189
+ graph_storage_path.parent.mkdir(parents=True, exist_ok=True)
190
+ self.vector_store = create_vector_store(
191
+ profile=self._memory_profile,
192
+ embedding_function=cast(EmbeddingModel, self.embeddings),
193
+ path=self._config.chroma_path,
194
+ collection_name=self._config.chroma_collection,
195
+ )
196
+
197
+ self.graph_store = create_graph_store(
198
+ profile=self._memory_profile,
199
+ storage_path=str(graph_storage_path),
200
+ )
201
+
202
+ if self._config.transcript.enabled:
203
+ transcript_path = self._config.resolve_transcript_path(
204
+ self._config.transcript.path
205
+ )
206
+ transcript_path.parent.mkdir(parents=True, exist_ok=True)
207
+ self.transcript_store = SQLiteTranscriptStore(transcript_path)
208
+ self.episodic_store = TranscriptBackedEpisodicStore(
209
+ transcript_store=self.transcript_store,
210
+ model_name=self._config.get_llm_service_config().model,
211
+ system_prompt=self._config.prompting.base_system_prompt,
212
+ capture_context_log=self._config.transcript.capture_context_log,
213
+ )
214
+ else:
215
+ self.transcript_store = None
216
+ self.episodic_store = InMemoryEpisodicStore()
217
+
218
+ logger.info(
219
+ f"Storage adapters initialized (profile: {self._memory_profile.value})"
220
+ )
221
+
222
+ def _create_memory_controller(self) -> None:
223
+ """Create memory controller with lifecycle-aware collections."""
224
+ assert self.embeddings is not None
225
+ assert self.episodic_store is not None
226
+ base_name = self._config.chroma_collection
227
+ collection_config = CollectionConfig(
228
+ short_term_collection=f"{base_name}_short",
229
+ long_term_collection=f"{base_name}_long",
230
+ decayed_collection=f"{base_name}_decayed",
231
+ )
232
+ collection_manager = CollectionManager(
233
+ embedding_function=cast(EmbeddingModel, self.embeddings),
234
+ config=collection_config,
235
+ path=self._config.chroma_path,
236
+ telemetry=self.telemetry,
237
+ )
238
+
239
+ self.memory = MemoryController(
240
+ vector_store=self.vector_store,
241
+ graph_store=self.graph_store,
242
+ episodic_store=cast(EpisodicStoreAdapter, self.episodic_store),
243
+ telemetry=self.telemetry,
244
+ collection_manager=collection_manager,
245
+ transcript_store=self.transcript_store,
246
+ )
247
+
248
+ logger.info("Memory controller initialized with lifecycle collections")
249
+
250
+ def _create_multi_agent_stack(self) -> None:
251
+ """Create additive multi-agent memory stack when mode is enabled."""
252
+ if self._config.memory_mode != "multi_agent":
253
+ self.multi_agent_memory = None
254
+ self.multi_agent_journal = None
255
+ self.multi_agent_coordinator = None
256
+ return
257
+
258
+ self.multi_agent_journal = PromotionJournal()
259
+ self.multi_agent_coordinator = PromotionCoordinator(self.multi_agent_journal)
260
+ assert self.memory is not None
261
+ self.multi_agent_memory = MultiAgentMemoryController(
262
+ memory=self.memory,
263
+ session_id="runtime-default",
264
+ agent_id="runtime",
265
+ namespace="default",
266
+ policy=NamespacePolicyEngine(
267
+ session_id="runtime-default",
268
+ actor_id="runtime",
269
+ namespace="default",
270
+ ),
271
+ coordinator=self.multi_agent_coordinator,
272
+ )
273
+ logger.warning(
274
+ "Multi-agent mode enabled without arbitrator service binding; "
275
+ "shared promotion remains unavailable until arbitrator is attached."
276
+ )
277
+
278
+ def _create_context_engine(self) -> None:
279
+ """Create context assembler and wire extractor."""
280
+ assert self.memory is not None
281
+ self.context_engine = ContextAssembler(
282
+ memory_controller=self.memory,
283
+ telemetry=self.telemetry,
284
+ )
285
+
286
+ if self._config.memory_extraction.enabled:
287
+ assert self.llm is not None
288
+ extraction_llm = self.llm_pool[self._config.memory_extraction.llm_service or self._config.llm_pool.default_service]
289
+ extraction_embeddings: EmbeddingModel | None = None
290
+ extraction_embedding_service = (
291
+ self._config.memory_extraction.embedding_service
292
+ )
293
+ if extraction_embedding_service is not None:
294
+ extraction_embeddings = cast(
295
+ EmbeddingModel,
296
+ self.embedding_pool[extraction_embedding_service],
297
+ )
298
+
299
+ ontology = self._config.memory_extraction.ontology
300
+ extractor = MemoryExtractor(
301
+ memory_controller=self.memory,
302
+ llm=cast(LLMClient, extraction_llm),
303
+ telemetry=self.telemetry,
304
+ prompt_template=self._config.memory_extraction.prompt,
305
+ user_name=self._config.memory_extraction.user_name,
306
+ assistant_name=self._config.memory_extraction.assistant_name,
307
+ embedding_model=extraction_embeddings,
308
+ max_tokens=self._config.memory_extraction.max_tokens,
309
+ ontology_config=(
310
+ OntologyConfig.from_dict(ontology) if ontology is not None else None
311
+ ),
312
+ rejection_log_path=self._config.memory_extraction.rejection_log,
313
+ )
314
+ self.context_engine.set_extractor(extractor)
315
+ logger.info(
316
+ "Context engine initialized with extractor service '%s'",
317
+ self._config.memory_extraction.llm_service,
318
+ )
319
+ else:
320
+ logger.info("Context engine initialized without archive extractor")
321
+
322
+ def _create_tool_registry(self) -> None:
323
+ """Create and configure tool registry."""
324
+ self.tool_registry = DefaultToolRegistry()
325
+ register_runtime_tools(self.tool_registry, self._config.tools)
326
+
327
+ # Bind dependencies to built-in tools
328
+ if self.tool_registry.has_tool("recall_memory"):
329
+ self.tool_registry.bind_dependency(
330
+ "recall_memory",
331
+ memory=self.memory,
332
+ )
333
+ if self.tool_registry.has_tool("store_fact"):
334
+ self.tool_registry.bind_dependency(
335
+ "store_fact",
336
+ memory=self.memory,
337
+ )
338
+
339
+ logger.info(
340
+ f"Tool registry initialized with "
341
+ f"{len(self.tool_registry.list_tools())} tools"
342
+ )