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.
- friday_framework_core-0.1.0a0/.gitignore +24 -0
- friday_framework_core-0.1.0a0/PKG-INFO +48 -0
- friday_framework_core-0.1.0a0/README.md +23 -0
- friday_framework_core-0.1.0a0/examples/01_introductory.py +86 -0
- friday_framework_core-0.1.0a0/examples/02_intermediate.py +156 -0
- friday_framework_core-0.1.0a0/examples/03_advanced.py +327 -0
- friday_framework_core-0.1.0a0/pyproject.toml +47 -0
- friday_framework_core-0.1.0a0/src/friday_core/__init__.py +129 -0
- friday_framework_core-0.1.0a0/src/friday_core/config.py +39 -0
- friday_framework_core-0.1.0a0/src/friday_core/exceptions.py +50 -0
- friday_framework_core-0.1.0a0/src/friday_core/interfaces/__init__.py +98 -0
- friday_framework_core-0.1.0a0/src/friday_core/interfaces/agent.py +71 -0
- friday_framework_core-0.1.0a0/src/friday_core/interfaces/context.py +187 -0
- friday_framework_core-0.1.0a0/src/friday_core/interfaces/lab.py +65 -0
- friday_framework_core-0.1.0a0/src/friday_core/interfaces/llm.py +135 -0
- friday_framework_core-0.1.0a0/src/friday_core/interfaces/memory.py +490 -0
- friday_framework_core-0.1.0a0/src/friday_core/interfaces/registry.py +81 -0
- friday_framework_core-0.1.0a0/src/friday_core/interfaces/runtime.py +94 -0
- friday_framework_core-0.1.0a0/src/friday_core/interfaces/tools.py +286 -0
- friday_framework_core-0.1.0a0/src/friday_core/interfaces/transcript.py +436 -0
- friday_framework_core-0.1.0a0/src/friday_core/logging.py +122 -0
- friday_framework_core-0.1.0a0/src/friday_core/profiles.py +115 -0
- friday_framework_core-0.1.0a0/src/friday_core/security/__init__.py +51 -0
- friday_framework_core-0.1.0a0/src/friday_core/security/config.py +150 -0
- friday_framework_core-0.1.0a0/src/friday_core/security/markers.py +128 -0
- friday_framework_core-0.1.0a0/src/friday_core/security/patterns.py +275 -0
- friday_framework_core-0.1.0a0/src/friday_core/security/quarantine.py +91 -0
- friday_framework_core-0.1.0a0/src/friday_core/security/sanitizer.py +266 -0
- friday_framework_core-0.1.0a0/src/friday_core/telemetry.py +24 -0
- friday_framework_core-0.1.0a0/tests_core/__init__.py +2 -0
- friday_framework_core-0.1.0a0/tests_core/conftest.py +48 -0
- friday_framework_core-0.1.0a0/tests_core/test_config.py +130 -0
- friday_framework_core-0.1.0a0/tests_core/test_exceptions.py +137 -0
- friday_framework_core-0.1.0a0/tests_core/test_interfaces.py +311 -0
- friday_framework_core-0.1.0a0/tests_core/test_logging.py +158 -0
- friday_framework_core-0.1.0a0/tests_core/test_telemetry.py +391 -0
- 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())
|