friday-framework-core 0.1.0a0__tar.gz

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 (37) hide show
  1. friday_framework_core-0.1.0a0/.gitignore +24 -0
  2. friday_framework_core-0.1.0a0/PKG-INFO +48 -0
  3. friday_framework_core-0.1.0a0/README.md +23 -0
  4. friday_framework_core-0.1.0a0/examples/01_introductory.py +86 -0
  5. friday_framework_core-0.1.0a0/examples/02_intermediate.py +156 -0
  6. friday_framework_core-0.1.0a0/examples/03_advanced.py +327 -0
  7. friday_framework_core-0.1.0a0/pyproject.toml +47 -0
  8. friday_framework_core-0.1.0a0/src/friday_core/__init__.py +129 -0
  9. friday_framework_core-0.1.0a0/src/friday_core/config.py +39 -0
  10. friday_framework_core-0.1.0a0/src/friday_core/exceptions.py +50 -0
  11. friday_framework_core-0.1.0a0/src/friday_core/interfaces/__init__.py +98 -0
  12. friday_framework_core-0.1.0a0/src/friday_core/interfaces/agent.py +71 -0
  13. friday_framework_core-0.1.0a0/src/friday_core/interfaces/context.py +187 -0
  14. friday_framework_core-0.1.0a0/src/friday_core/interfaces/lab.py +65 -0
  15. friday_framework_core-0.1.0a0/src/friday_core/interfaces/llm.py +135 -0
  16. friday_framework_core-0.1.0a0/src/friday_core/interfaces/memory.py +490 -0
  17. friday_framework_core-0.1.0a0/src/friday_core/interfaces/registry.py +81 -0
  18. friday_framework_core-0.1.0a0/src/friday_core/interfaces/runtime.py +94 -0
  19. friday_framework_core-0.1.0a0/src/friday_core/interfaces/tools.py +286 -0
  20. friday_framework_core-0.1.0a0/src/friday_core/interfaces/transcript.py +436 -0
  21. friday_framework_core-0.1.0a0/src/friday_core/logging.py +122 -0
  22. friday_framework_core-0.1.0a0/src/friday_core/profiles.py +115 -0
  23. friday_framework_core-0.1.0a0/src/friday_core/security/__init__.py +51 -0
  24. friday_framework_core-0.1.0a0/src/friday_core/security/config.py +150 -0
  25. friday_framework_core-0.1.0a0/src/friday_core/security/markers.py +128 -0
  26. friday_framework_core-0.1.0a0/src/friday_core/security/patterns.py +275 -0
  27. friday_framework_core-0.1.0a0/src/friday_core/security/quarantine.py +91 -0
  28. friday_framework_core-0.1.0a0/src/friday_core/security/sanitizer.py +266 -0
  29. friday_framework_core-0.1.0a0/src/friday_core/telemetry.py +24 -0
  30. friday_framework_core-0.1.0a0/tests_core/__init__.py +2 -0
  31. friday_framework_core-0.1.0a0/tests_core/conftest.py +48 -0
  32. friday_framework_core-0.1.0a0/tests_core/test_config.py +130 -0
  33. friday_framework_core-0.1.0a0/tests_core/test_exceptions.py +137 -0
  34. friday_framework_core-0.1.0a0/tests_core/test_interfaces.py +311 -0
  35. friday_framework_core-0.1.0a0/tests_core/test_logging.py +158 -0
  36. friday_framework_core-0.1.0a0/tests_core/test_telemetry.py +391 -0
  37. friday_framework_core-0.1.0a0/tests_core/test_transcript_bridge.py +95 -0
@@ -0,0 +1,24 @@
1
+ __pycache__/
2
+ *.pyc
3
+ .env
4
+ .venv/
5
+ .idea/
6
+ .vscode/
7
+ dist/
8
+ build/
9
+ *.egg-info/
10
+ chroma_db/
11
+ .friday/
12
+ /vector/
13
+ /exports/
14
+ tests/integration/memory/vector/*/
15
+ tests/integration/memory/vector/backups/*/
16
+ tests/integration/memory/vector/reorganization_logs/*
17
+ *.gpickle
18
+ keys.txt
19
+ experiments/*/config/runtime.local.yaml
20
+ experiments/*/artifacts/*
21
+ !experiments/*/artifacts/.gitkeep
22
+ scripts/source-friday-chat-example-env.sh
23
+ scripts/pypi.sh
24
+ /docs/reference/PyPI-Recovery-Codes-cichuck-2026-07-20T15_36_49.709334.txt
@@ -0,0 +1,48 @@
1
+ Metadata-Version: 2.4
2
+ Name: friday-framework-core
3
+ Version: 0.1.0a0
4
+ Summary: Foundation interfaces and utilities for Friday
5
+ Project-URL: Homepage, https://github.com/CIChuck/agent-framework
6
+ Project-URL: Repository, https://github.com/CIChuck/agent-framework
7
+ Project-URL: Issues, https://github.com/CIChuck/agent-framework/issues
8
+ Project-URL: Documentation, https://github.com/CIChuck/agent-framework/tree/main/docs
9
+ Project-URL: Source, https://github.com/CIChuck/agent-framework/tree/main/packages/friday-core
10
+ Author: Friday Team
11
+ License-Expression: MIT
12
+ Requires-Python: >=3.10
13
+ Requires-Dist: friday-framework-telemetry==0.1.0a0
14
+ Requires-Dist: pydantic-settings>=2.0
15
+ Requires-Dist: pydantic>=2.0
16
+ Requires-Dist: structlog>=24.0.0
17
+ Provides-Extra: dev
18
+ Requires-Dist: pytest-asyncio>=0.23.0; extra == 'dev'
19
+ Requires-Dist: pytest>=8.0.0; extra == 'dev'
20
+ Provides-Extra: telemetry
21
+ Requires-Dist: opentelemetry-api>=1.20.0; extra == 'telemetry'
22
+ Requires-Dist: opentelemetry-exporter-otlp>=1.20.0; extra == 'telemetry'
23
+ Requires-Dist: opentelemetry-sdk>=1.20.0; extra == 'telemetry'
24
+ Description-Content-Type: text/markdown
25
+
26
+ # Friday Framework Core
27
+
28
+ Foundation interfaces and utilities for the Friday Framework.
29
+
30
+ This package provides shared protocols, configuration primitives, structured
31
+ logging helpers, exceptions, and security-oriented types used across Friday
32
+ packages.
33
+
34
+ ## Install
35
+
36
+ ```bash
37
+ pip install friday-framework-core
38
+ ```
39
+
40
+ ## Import Package
41
+
42
+ ```python
43
+ import friday_core
44
+ ```
45
+
46
+ ## License
47
+
48
+ MIT
@@ -0,0 +1,23 @@
1
+ # Friday Framework Core
2
+
3
+ Foundation interfaces and utilities for the Friday Framework.
4
+
5
+ This package provides shared protocols, configuration primitives, structured
6
+ logging helpers, exceptions, and security-oriented types used across Friday
7
+ packages.
8
+
9
+ ## Install
10
+
11
+ ```bash
12
+ pip install friday-framework-core
13
+ ```
14
+
15
+ ## Import Package
16
+
17
+ ```python
18
+ import friday_core
19
+ ```
20
+
21
+ ## License
22
+
23
+ MIT
@@ -0,0 +1,86 @@
1
+ """
2
+ Introductory Example: Basic Structured Logging
3
+
4
+ This example demonstrates:
5
+ - Creating loggers with get_logger()
6
+ - Logging at different levels (debug, info, warning, error)
7
+ - Adding structured context to log messages
8
+ - Switching between development and production output formats
9
+
10
+ Run with: uv run python packages/friday-core/examples/01_introductory.py
11
+ """
12
+
13
+ from friday_core import get_logger
14
+
15
+
16
+ def main():
17
+ # Create a logger for this module
18
+ # Development mode provides colored, human-readable output
19
+ logger = get_logger("my_app", environment="development")
20
+
21
+ print("=== Development Mode (Colored Console Output) ===\n")
22
+
23
+ # Basic logging at different levels
24
+ logger.debug("This is a debug message")
25
+ logger.info("Application started")
26
+ logger.warning("This is a warning")
27
+ logger.error("Something went wrong")
28
+
29
+ print("\n=== Structured Context Logging ===\n")
30
+
31
+ # Add structured context to log messages
32
+ # These key-value pairs are attached to the log entry
33
+ logger.info("User logged in", user_id="user_123", ip_address="192.168.1.1")
34
+
35
+ logger.info(
36
+ "Processing request",
37
+ request_id="req_abc",
38
+ method="POST",
39
+ path="/api/recall",
40
+ )
41
+
42
+ logger.warning(
43
+ "Slow response detected", latency_ms=1500, threshold_ms=1000, endpoint="/search"
44
+ )
45
+
46
+ logger.error(
47
+ "Database connection failed",
48
+ error_code="DB_001",
49
+ retry_count=3,
50
+ database="postgres",
51
+ )
52
+
53
+ print("\n=== Production Mode (JSON Output) ===\n")
54
+
55
+ # Production mode outputs JSON for machine parsing
56
+ prod_logger = get_logger("my_app_prod", environment="production")
57
+
58
+ prod_logger.info(
59
+ "Request completed",
60
+ request_id="req_xyz",
61
+ status_code=200,
62
+ duration_ms=45,
63
+ )
64
+
65
+ prod_logger.error(
66
+ "API rate limit exceeded",
67
+ provider="openai",
68
+ requests_remaining=0,
69
+ reset_at="2024-01-15T10:30:00Z",
70
+ )
71
+
72
+ print("\n=== Complex Nested Context ===\n")
73
+
74
+ # Loggers handle complex nested data structures
75
+ dev_logger = get_logger("complex_app", environment="development")
76
+
77
+ dev_logger.info(
78
+ "Memory operation completed",
79
+ user={"id": "user_456", "role": "admin"},
80
+ operation={"type": "recall", "budget_tokens": 4000},
81
+ metrics={"latency_ms": 120, "tokens_used": 350, "cache_hit": True},
82
+ )
83
+
84
+
85
+ if __name__ == "__main__":
86
+ main()
@@ -0,0 +1,156 @@
1
+ """
2
+ Intermediate Example: Telemetry with Events and Spans
3
+
4
+ This example demonstrates:
5
+ - TelemetryInterface protocol for instrumentation
6
+ - DebugTelemetryRecorder for white-box testing
7
+ - Capturing discrete events with data
8
+ - Tracking operation timing with spans
9
+ - Parent-child span relationships
10
+ - Using helper methods for test assertions
11
+
12
+ Run with: uv run python packages/friday-core/examples/02_intermediate.py
13
+ """
14
+
15
+ import time
16
+
17
+ from friday_core import (
18
+ DebugTelemetryRecorder,
19
+ TelemetryInterface,
20
+ )
21
+
22
+
23
+ def main():
24
+ # DebugTelemetryRecorder is an in-memory implementation
25
+ # Perfect for testing and development
26
+ recorder = DebugTelemetryRecorder()
27
+
28
+ print("=== Capturing Events ===\n")
29
+
30
+ # Events are discrete occurrences with associated data
31
+ recorder.capture_event("user_login", {"user_id": "user_123", "method": "oauth"})
32
+
33
+ recorder.capture_event(
34
+ "memory_recall",
35
+ {
36
+ "query": "What did we discuss about the API?",
37
+ "results_count": 5,
38
+ "latency_ms": 45,
39
+ },
40
+ )
41
+
42
+ recorder.capture_event(
43
+ "tool_execution",
44
+ {"tool_name": "web_search", "status": "success", "tokens_used": 150},
45
+ )
46
+
47
+ # View all captured events
48
+ print(f"Captured {len(recorder.events)} events:")
49
+ for event in recorder.events:
50
+ print(f" - {event.name}: {event.data}")
51
+
52
+ print("\n=== Working with Spans ===\n")
53
+
54
+ # Spans track the duration of operations
55
+ # start_span returns a span_id for later reference
56
+ span_id = recorder.start_span("database_query")
57
+
58
+ # Simulate some work
59
+ time.sleep(0.05) # 50ms
60
+
61
+ # End the span with optional result data
62
+ recorder.end_span(span_id, {"rows_returned": 42, "cache_hit": False})
63
+
64
+ # Retrieve and inspect the span
65
+ span = recorder.get_span(span_id)
66
+ print(f"Span '{span.name}':")
67
+ print(f" Duration: {span.duration_ms:.2f}ms")
68
+ print(f" Data: {span.data}")
69
+
70
+ print("\n=== Parent-Child Span Relationships ===\n")
71
+
72
+ # Spans can have parent-child relationships for tracing
73
+ parent_id = recorder.start_span("handle_request")
74
+
75
+ # Child spans reference their parent
76
+ child1_id = recorder.start_span("validate_input", parent_id=parent_id)
77
+ time.sleep(0.01)
78
+ recorder.end_span(child1_id, {"valid": True})
79
+
80
+ child2_id = recorder.start_span("process_query", parent_id=parent_id)
81
+ time.sleep(0.02)
82
+ recorder.end_span(child2_id, {"tokens": 500})
83
+
84
+ child3_id = recorder.start_span("format_response", parent_id=parent_id)
85
+ time.sleep(0.01)
86
+ recorder.end_span(child3_id, {"format": "json"})
87
+
88
+ recorder.end_span(parent_id, {"status": 200})
89
+
90
+ # Display the span hierarchy
91
+ parent_span = recorder.get_span(parent_id)
92
+ print(f"Parent: {parent_span.name} ({parent_span.duration_ms:.2f}ms)")
93
+
94
+ for span_id, span in recorder.spans.items(): # noqa: B007
95
+ if span.parent_id == parent_id:
96
+ print(f" Child: {span.name} ({span.duration_ms:.2f}ms)")
97
+
98
+ print("\n=== Helper Methods for Testing ===\n")
99
+
100
+ # get_event retrieves the first event with a given name
101
+ login_data = recorder.get_event("user_login")
102
+ print(f"User login data: {login_data}")
103
+
104
+ # get_all_events retrieves all events with a given name
105
+ recorder.capture_event("api_call", {"endpoint": "/v1/chat"})
106
+ recorder.capture_event("api_call", {"endpoint": "/v1/embeddings"})
107
+ all_api_calls = recorder.get_all_events("api_call")
108
+ print(f"All API calls ({len(all_api_calls)}): {all_api_calls}")
109
+
110
+ # get_span_by_name finds a span by its name
111
+ query_span = recorder.get_span_by_name("database_query")
112
+ print(f"Database query span duration: {query_span.duration_ms:.2f}ms")
113
+
114
+ # event_names lists all event names in order
115
+ print(f"Event names: {recorder.event_names}")
116
+
117
+ print("\n=== Using Telemetry for Test Assertions ===\n")
118
+
119
+ # Clear previous data for a fresh test
120
+ recorder.clear()
121
+
122
+ # Simulate a component that uses telemetry
123
+ def memory_search(query: str, telemetry: TelemetryInterface):
124
+ span_id = telemetry.start_span("memory_search")
125
+
126
+ telemetry.capture_event("search_started", {"query": query})
127
+
128
+ # Simulate search
129
+ time.sleep(0.02)
130
+ results = ["result1", "result2", "result3"]
131
+
132
+ telemetry.capture_event(
133
+ "search_completed", {"query": query, "count": len(results)}
134
+ )
135
+
136
+ telemetry.end_span(span_id, {"results": len(results)})
137
+ return results
138
+
139
+ # Execute the function
140
+ memory_search("API design patterns", recorder)
141
+
142
+ # Now assert on the telemetry data
143
+ assert recorder.get_event("search_started")["query"] == "API design patterns"
144
+ assert recorder.get_event("search_completed")["count"] == 3
145
+
146
+ search_span = recorder.get_span_by_name("memory_search")
147
+ assert search_span is not None
148
+ assert search_span.duration_ms >= 20
149
+ assert search_span.data["results"] == 3
150
+
151
+ print("All assertions passed!")
152
+ print(f"Search completed in {search_span.duration_ms:.2f}ms")
153
+
154
+
155
+ if __name__ == "__main__":
156
+ main()
@@ -0,0 +1,327 @@
1
+ """
2
+ Advanced Example: Production Patterns with Logging and Telemetry
3
+
4
+ This example demonstrates:
5
+ - Dependency injection pattern for observability
6
+ - Combined logging and telemetry in components
7
+ - Configuration-driven environment switching
8
+ - Optional telemetry (graceful degradation)
9
+ - Simulating a realistic service with full instrumentation
10
+ - Test harness pattern for white-box testing
11
+
12
+ Run with: uv run python packages/friday-core/examples/03_advanced.py
13
+ """
14
+
15
+ import asyncio
16
+ import time
17
+ from dataclasses import dataclass
18
+ from typing import Any
19
+
20
+ from friday_core import (
21
+ DebugTelemetryRecorder,
22
+ FridayBaseSettings,
23
+ LoggerProtocol,
24
+ TelemetryInterface,
25
+ get_logger,
26
+ )
27
+
28
+
29
+ @dataclass
30
+ class RetrievalResult:
31
+ """Result from memory retrieval."""
32
+
33
+ query: str
34
+ documents: list[dict[str, Any]]
35
+ latency_ms: float
36
+ source: str
37
+
38
+
39
+ class MemoryService:
40
+ """
41
+ A service demonstrating production-ready observability patterns.
42
+
43
+ Key patterns:
44
+ - Telemetry is optional (works without it)
45
+ - Logger is injected for testability
46
+ - Both are used together for comprehensive observability
47
+ """
48
+
49
+ def __init__(
50
+ self,
51
+ logger: LoggerProtocol,
52
+ telemetry: TelemetryInterface | None = None,
53
+ ):
54
+ self._logger = logger
55
+ self._telemetry = telemetry
56
+
57
+ async def retrieve(self, query: str, limit: int = 5) -> RetrievalResult:
58
+ """
59
+ Retrieve relevant documents from memory.
60
+
61
+ Demonstrates:
62
+ - Span wrapping for timing
63
+ - Structured logging with context
64
+ - Event capture for discrete operations
65
+ """
66
+ span_id = None
67
+ if self._telemetry:
68
+ span_id = self._telemetry.start_span("memory_retrieve")
69
+ self._telemetry.capture_event(
70
+ "retrieve_started", {"query": query, "limit": limit}
71
+ )
72
+
73
+ self._logger.info("Starting memory retrieval", query=query, limit=limit)
74
+
75
+ start_time = time.perf_counter()
76
+
77
+ try:
78
+ # Simulate vector search
79
+ await self._vector_search(query)
80
+
81
+ # Simulate graph traversal
82
+ await self._graph_lookup(query)
83
+
84
+ # Simulate result ranking
85
+ documents = await self._rank_results(query, limit)
86
+
87
+ latency_ms = (time.perf_counter() - start_time) * 1000
88
+
89
+ self._logger.info(
90
+ "Memory retrieval completed",
91
+ query=query,
92
+ results_count=len(documents),
93
+ latency_ms=round(latency_ms, 2),
94
+ )
95
+
96
+ if self._telemetry:
97
+ self._telemetry.capture_event(
98
+ "retrieve_completed",
99
+ {
100
+ "query": query,
101
+ "results_count": len(documents),
102
+ "latency_ms": latency_ms,
103
+ },
104
+ )
105
+ if span_id:
106
+ self._telemetry.end_span(
107
+ span_id,
108
+ {"results": len(documents), "status": "success"},
109
+ )
110
+
111
+ return RetrievalResult(
112
+ query=query,
113
+ documents=documents,
114
+ latency_ms=latency_ms,
115
+ source="combined",
116
+ )
117
+
118
+ except Exception as e:
119
+ self._logger.error(
120
+ "Memory retrieval failed",
121
+ query=query,
122
+ error=str(e),
123
+ )
124
+
125
+ if self._telemetry:
126
+ self._telemetry.capture_event(
127
+ "retrieve_failed", {"query": query, "error": str(e)}
128
+ )
129
+ if span_id:
130
+ self._telemetry.end_span(
131
+ span_id, {"status": "error", "error": str(e)}
132
+ )
133
+
134
+ raise
135
+
136
+ async def _vector_search(self, query: str) -> list[dict]:
137
+ """Simulate vector similarity search."""
138
+ span_id = None
139
+ if self._telemetry:
140
+ span_id = self._telemetry.start_span("vector_search")
141
+
142
+ self._logger.debug("Executing vector search", query=query[:50])
143
+
144
+ # Simulate latency
145
+ await asyncio.sleep(0.02)
146
+
147
+ if self._telemetry and span_id:
148
+ self._telemetry.end_span(span_id, {"matches": 10})
149
+
150
+ return [{"id": f"vec_{i}", "score": 0.9 - i * 0.1} for i in range(10)]
151
+
152
+ async def _graph_lookup(self, query: str) -> list[dict]:
153
+ """Simulate knowledge graph traversal."""
154
+ span_id = None
155
+ if self._telemetry:
156
+ span_id = self._telemetry.start_span("graph_lookup")
157
+
158
+ self._logger.debug("Executing graph lookup", query=query[:50])
159
+
160
+ # Simulate latency
161
+ await asyncio.sleep(0.015)
162
+
163
+ if self._telemetry and span_id:
164
+ self._telemetry.end_span(span_id, {"nodes_visited": 25})
165
+
166
+ return [{"id": f"node_{i}", "relevance": 0.85 - i * 0.05} for i in range(5)]
167
+
168
+ async def _rank_results(self, query: str, limit: int) -> list[dict]:
169
+ """Simulate result ranking and deduplication."""
170
+ span_id = None
171
+ if self._telemetry:
172
+ span_id = self._telemetry.start_span("rank_results")
173
+
174
+ self._logger.debug("Ranking results", limit=limit)
175
+
176
+ # Simulate ranking computation
177
+ await asyncio.sleep(0.01)
178
+
179
+ results = [
180
+ {
181
+ "id": f"doc_{i}",
182
+ "content": f"Document {i} about {query}",
183
+ "score": 0.95 - i * 0.05,
184
+ }
185
+ for i in range(limit)
186
+ ]
187
+
188
+ if self._telemetry and span_id:
189
+ self._telemetry.end_span(span_id, {"final_count": len(results)})
190
+
191
+ return results
192
+
193
+
194
+ class ApplicationContext:
195
+ """
196
+ Application context that wires up dependencies.
197
+
198
+ Demonstrates configuration-driven setup based on environment.
199
+ """
200
+
201
+ def __init__(self, settings: FridayBaseSettings):
202
+ self.settings = settings
203
+
204
+ # Configure logger based on environment
205
+ self.logger = get_logger("friday_app", environment=settings.APP_ENV)
206
+
207
+ # Telemetry is only enabled in development/testing
208
+ self.telemetry: TelemetryInterface | None = None
209
+ if settings.APP_ENV in ("development", "testing"):
210
+ self.telemetry = DebugTelemetryRecorder()
211
+ self.logger.info("Telemetry enabled", mode="debug_recorder")
212
+
213
+ def create_memory_service(self) -> MemoryService:
214
+ """Factory method for creating the memory service."""
215
+ return MemoryService(
216
+ logger=self.logger,
217
+ telemetry=self.telemetry,
218
+ )
219
+
220
+
221
+ async def run_production_simulation():
222
+ """Simulate production usage with logging only."""
223
+ print("=== Production Mode (Logging Only) ===\n")
224
+
225
+ # Production settings - telemetry disabled
226
+ settings = FridayBaseSettings(APP_ENV="production")
227
+ ctx = ApplicationContext(settings)
228
+
229
+ service = ctx.create_memory_service()
230
+
231
+ result = await service.retrieve("What are the best practices for API design?")
232
+ print(
233
+ f"\nRetrieved {len(result.documents)} documents in {result.latency_ms:.2f}ms\n"
234
+ )
235
+
236
+
237
+ async def run_development_simulation():
238
+ """Simulate development usage with full observability."""
239
+ print("=== Development Mode (Logging + Telemetry) ===\n")
240
+
241
+ settings = FridayBaseSettings(APP_ENV="development")
242
+ ctx = ApplicationContext(settings)
243
+
244
+ service = ctx.create_memory_service()
245
+
246
+ result = await service.retrieve("How do I implement authentication?", limit=3)
247
+ print(f"\nRetrieved {len(result.documents)} documents in {result.latency_ms:.2f}ms")
248
+
249
+ # Access telemetry data for debugging
250
+ if ctx.telemetry:
251
+ recorder = ctx.telemetry
252
+ print("\n--- Telemetry Summary ---")
253
+ print(f"Events captured: {len(recorder.events)}")
254
+ print(f"Event types: {set(recorder.event_names)}")
255
+ print(f"Spans recorded: {len(recorder.spans)}")
256
+
257
+ # Show span hierarchy
258
+ print("\nSpan timings:")
259
+ for _span_id, span in recorder.spans.items():
260
+ if span.duration_ms is not None:
261
+ indent = " " if span.parent_id else ""
262
+ print(f"{indent}{span.name}: {span.duration_ms:.2f}ms")
263
+
264
+
265
+ async def run_test_harness():
266
+ """Demonstrate white-box testing with telemetry assertions."""
267
+ print("\n=== Test Harness Pattern ===\n")
268
+
269
+ # Create a test-specific recorder
270
+ test_recorder = DebugTelemetryRecorder()
271
+ test_logger = get_logger("test", environment="development")
272
+
273
+ # Inject test dependencies
274
+ service = MemoryService(logger=test_logger, telemetry=test_recorder)
275
+
276
+ # Execute the operation
277
+ await service.retrieve("test query", limit=5)
278
+
279
+ # White-box assertions on internal behavior
280
+ print("Running assertions on telemetry data...")
281
+
282
+ # Assert events were captured
283
+ started_event = test_recorder.get_event("retrieve_started")
284
+ assert started_event is not None, "Should capture retrieve_started event"
285
+ assert started_event["query"] == "test query"
286
+ assert started_event["limit"] == 5
287
+
288
+ completed_event = test_recorder.get_event("retrieve_completed")
289
+ assert completed_event is not None, "Should capture retrieve_completed event"
290
+ assert completed_event["results_count"] == 5
291
+
292
+ # Assert spans were recorded
293
+ main_span = test_recorder.get_span_by_name("memory_retrieve")
294
+ assert main_span is not None, "Should record main span"
295
+ assert main_span.data["status"] == "success"
296
+
297
+ vector_span = test_recorder.get_span_by_name("vector_search")
298
+ assert vector_span is not None, "Should record vector_search span"
299
+
300
+ graph_span = test_recorder.get_span_by_name("graph_lookup")
301
+ assert graph_span is not None, "Should record graph_lookup span"
302
+
303
+ rank_span = test_recorder.get_span_by_name("rank_results")
304
+ assert rank_span is not None, "Should record rank_results span"
305
+
306
+ # Assert timing relationships
307
+ assert main_span.duration_ms >= (
308
+ vector_span.duration_ms + graph_span.duration_ms + rank_span.duration_ms
309
+ ), "Parent span should be >= sum of child spans"
310
+
311
+ print("All assertions passed!")
312
+ print(f"\nTotal operation time: {main_span.duration_ms:.2f}ms")
313
+ print(f" - Vector search: {vector_span.duration_ms:.2f}ms")
314
+ print(f" - Graph lookup: {graph_span.duration_ms:.2f}ms")
315
+ print(f" - Ranking: {rank_span.duration_ms:.2f}ms")
316
+
317
+
318
+ async def main():
319
+ await run_production_simulation()
320
+ print("\n" + "=" * 60 + "\n")
321
+ await run_development_simulation()
322
+ print("\n" + "=" * 60 + "\n")
323
+ await run_test_harness()
324
+
325
+
326
+ if __name__ == "__main__":
327
+ asyncio.run(main())