hindsight-api 0.4.6__py3-none-any.whl → 0.4.8__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.
- hindsight_api/__init__.py +1 -1
- hindsight_api/alembic/versions/5a366d414dce_initial_schema.py +16 -2
- hindsight_api/api/http.py +83 -1
- hindsight_api/banner.py +3 -0
- hindsight_api/config.py +44 -6
- hindsight_api/daemon.py +18 -112
- hindsight_api/engine/llm_interface.py +146 -0
- hindsight_api/engine/llm_wrapper.py +304 -1327
- hindsight_api/engine/memory_engine.py +125 -41
- hindsight_api/engine/providers/__init__.py +14 -0
- hindsight_api/engine/providers/anthropic_llm.py +434 -0
- hindsight_api/engine/providers/claude_code_llm.py +352 -0
- hindsight_api/engine/providers/codex_llm.py +527 -0
- hindsight_api/engine/providers/gemini_llm.py +502 -0
- hindsight_api/engine/providers/mock_llm.py +234 -0
- hindsight_api/engine/providers/openai_compatible_llm.py +745 -0
- hindsight_api/engine/retain/fact_extraction.py +13 -9
- hindsight_api/engine/retain/fact_storage.py +5 -3
- hindsight_api/extensions/__init__.py +10 -0
- hindsight_api/extensions/builtin/tenant.py +36 -0
- hindsight_api/extensions/operation_validator.py +129 -0
- hindsight_api/main.py +6 -21
- hindsight_api/migrations.py +75 -0
- hindsight_api/worker/main.py +41 -11
- hindsight_api/worker/poller.py +26 -14
- {hindsight_api-0.4.6.dist-info → hindsight_api-0.4.8.dist-info}/METADATA +2 -1
- {hindsight_api-0.4.6.dist-info → hindsight_api-0.4.8.dist-info}/RECORD +29 -21
- {hindsight_api-0.4.6.dist-info → hindsight_api-0.4.8.dist-info}/WHEEL +0 -0
- {hindsight_api-0.4.6.dist-info → hindsight_api-0.4.8.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Mock LLM provider for testing.
|
|
3
|
+
|
|
4
|
+
This provider allows tests to record LLM calls and return configurable mock responses
|
|
5
|
+
without making actual API calls to external LLM services.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from ..llm_interface import LLMInterface
|
|
12
|
+
from ..response_models import LLMToolCall, LLMToolCallResult, TokenUsage
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class MockLLM(LLMInterface):
|
|
18
|
+
"""
|
|
19
|
+
Mock LLM provider for testing.
|
|
20
|
+
|
|
21
|
+
This provider records all calls and returns configurable mock responses,
|
|
22
|
+
enabling tests to verify LLM interactions without making real API calls.
|
|
23
|
+
|
|
24
|
+
Example:
|
|
25
|
+
# Create mock provider
|
|
26
|
+
mock_llm = MockLLM(provider="mock", api_key="", base_url="", model="mock-model")
|
|
27
|
+
|
|
28
|
+
# Set mock response
|
|
29
|
+
mock_llm.set_mock_response({"answer": "test"})
|
|
30
|
+
|
|
31
|
+
# Make calls
|
|
32
|
+
result = await mock_llm.call(
|
|
33
|
+
messages=[{"role": "user", "content": "test"}],
|
|
34
|
+
response_format=MyResponseModel
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
# Verify calls
|
|
38
|
+
calls = mock_llm.get_mock_calls()
|
|
39
|
+
assert len(calls) == 1
|
|
40
|
+
assert calls[0]["scope"] == "memory"
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
def __init__(
|
|
44
|
+
self,
|
|
45
|
+
provider: str,
|
|
46
|
+
api_key: str,
|
|
47
|
+
base_url: str,
|
|
48
|
+
model: str,
|
|
49
|
+
reasoning_effort: str = "low",
|
|
50
|
+
**kwargs: Any,
|
|
51
|
+
):
|
|
52
|
+
"""
|
|
53
|
+
Initialize mock LLM provider.
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
provider: Provider name (should be "mock").
|
|
57
|
+
api_key: Not used for mock provider.
|
|
58
|
+
base_url: Not used for mock provider.
|
|
59
|
+
model: Model name for tracking.
|
|
60
|
+
reasoning_effort: Not used for mock provider.
|
|
61
|
+
**kwargs: Additional parameters (not used).
|
|
62
|
+
"""
|
|
63
|
+
super().__init__(provider, api_key, base_url, model, reasoning_effort, **kwargs)
|
|
64
|
+
|
|
65
|
+
# Storage for test verification
|
|
66
|
+
self._mock_calls: list[dict] = []
|
|
67
|
+
self._mock_response: Any = None
|
|
68
|
+
|
|
69
|
+
async def verify_connection(self) -> None:
|
|
70
|
+
"""
|
|
71
|
+
Verify mock provider (always succeeds).
|
|
72
|
+
|
|
73
|
+
Mock provider doesn't need connection verification since it doesn't
|
|
74
|
+
make real API calls.
|
|
75
|
+
"""
|
|
76
|
+
logger.debug("Mock LLM: connection verification (always succeeds)")
|
|
77
|
+
|
|
78
|
+
async def call(
|
|
79
|
+
self,
|
|
80
|
+
messages: list[dict[str, str]],
|
|
81
|
+
response_format: Any | None = None,
|
|
82
|
+
max_completion_tokens: int | None = None,
|
|
83
|
+
temperature: float | None = None,
|
|
84
|
+
scope: str = "memory",
|
|
85
|
+
max_retries: int = 10,
|
|
86
|
+
initial_backoff: float = 1.0,
|
|
87
|
+
max_backoff: float = 60.0,
|
|
88
|
+
skip_validation: bool = False,
|
|
89
|
+
strict_schema: bool = False,
|
|
90
|
+
return_usage: bool = False,
|
|
91
|
+
) -> Any:
|
|
92
|
+
"""
|
|
93
|
+
Make a mock LLM API call.
|
|
94
|
+
|
|
95
|
+
Records the call for test verification and returns the configured mock response.
|
|
96
|
+
|
|
97
|
+
Args:
|
|
98
|
+
messages: List of message dicts with 'role' and 'content'.
|
|
99
|
+
response_format: Optional Pydantic model for structured output.
|
|
100
|
+
max_completion_tokens: Not used in mock.
|
|
101
|
+
temperature: Not used in mock.
|
|
102
|
+
scope: Scope identifier for tracking.
|
|
103
|
+
max_retries: Not used in mock.
|
|
104
|
+
initial_backoff: Not used in mock.
|
|
105
|
+
max_backoff: Not used in mock.
|
|
106
|
+
skip_validation: Return raw JSON without Pydantic validation.
|
|
107
|
+
strict_schema: Not used in mock.
|
|
108
|
+
return_usage: If True, return tuple (result, TokenUsage) instead of just result.
|
|
109
|
+
|
|
110
|
+
Returns:
|
|
111
|
+
If return_usage=False: Parsed response if response_format is provided, otherwise text content.
|
|
112
|
+
If return_usage=True: Tuple of (result, TokenUsage) with mock token counts.
|
|
113
|
+
"""
|
|
114
|
+
# Record the call for test verification
|
|
115
|
+
call_record = {
|
|
116
|
+
"provider": self.provider,
|
|
117
|
+
"model": self.model,
|
|
118
|
+
"messages": messages,
|
|
119
|
+
"response_format": response_format.__name__
|
|
120
|
+
if response_format and hasattr(response_format, "__name__")
|
|
121
|
+
else str(response_format),
|
|
122
|
+
"scope": scope,
|
|
123
|
+
}
|
|
124
|
+
self._mock_calls.append(call_record)
|
|
125
|
+
logger.debug(f"Mock LLM call recorded: scope={scope}, model={self.model}")
|
|
126
|
+
|
|
127
|
+
# Return mock response
|
|
128
|
+
if self._mock_response is not None:
|
|
129
|
+
result = self._mock_response
|
|
130
|
+
elif response_format is not None:
|
|
131
|
+
# Try to create a minimal valid instance of the response format
|
|
132
|
+
try:
|
|
133
|
+
# For Pydantic models, try to create with minimal valid data
|
|
134
|
+
result = {"mock": True}
|
|
135
|
+
except Exception:
|
|
136
|
+
result = {"mock": True}
|
|
137
|
+
else:
|
|
138
|
+
result = "mock response"
|
|
139
|
+
|
|
140
|
+
if return_usage:
|
|
141
|
+
token_usage = TokenUsage(input_tokens=10, output_tokens=5, total_tokens=15)
|
|
142
|
+
return result, token_usage
|
|
143
|
+
return result
|
|
144
|
+
|
|
145
|
+
async def call_with_tools(
|
|
146
|
+
self,
|
|
147
|
+
messages: list[dict[str, Any]],
|
|
148
|
+
tools: list[dict[str, Any]],
|
|
149
|
+
max_completion_tokens: int | None = None,
|
|
150
|
+
temperature: float | None = None,
|
|
151
|
+
scope: str = "tools",
|
|
152
|
+
max_retries: int = 5,
|
|
153
|
+
initial_backoff: float = 1.0,
|
|
154
|
+
max_backoff: float = 30.0,
|
|
155
|
+
tool_choice: str | dict[str, Any] = "auto",
|
|
156
|
+
) -> LLMToolCallResult:
|
|
157
|
+
"""
|
|
158
|
+
Make a mock LLM API call with tool/function calling support.
|
|
159
|
+
|
|
160
|
+
Records the call for test verification and returns the configured mock response.
|
|
161
|
+
|
|
162
|
+
Args:
|
|
163
|
+
messages: List of message dicts. Can include tool results with role='tool'.
|
|
164
|
+
tools: List of tool definitions in OpenAI format.
|
|
165
|
+
max_completion_tokens: Not used in mock.
|
|
166
|
+
temperature: Not used in mock.
|
|
167
|
+
scope: Scope identifier for tracking.
|
|
168
|
+
max_retries: Not used in mock.
|
|
169
|
+
initial_backoff: Not used in mock.
|
|
170
|
+
max_backoff: Not used in mock.
|
|
171
|
+
tool_choice: Not used in mock.
|
|
172
|
+
|
|
173
|
+
Returns:
|
|
174
|
+
LLMToolCallResult with content and/or tool_calls.
|
|
175
|
+
"""
|
|
176
|
+
# Record the call for test verification
|
|
177
|
+
call_record = {
|
|
178
|
+
"provider": self.provider,
|
|
179
|
+
"model": self.model,
|
|
180
|
+
"messages": messages,
|
|
181
|
+
"tools": [t.get("function", {}).get("name") for t in tools],
|
|
182
|
+
"scope": scope,
|
|
183
|
+
}
|
|
184
|
+
self._mock_calls.append(call_record)
|
|
185
|
+
|
|
186
|
+
if self._mock_response is not None:
|
|
187
|
+
if isinstance(self._mock_response, LLMToolCallResult):
|
|
188
|
+
return self._mock_response
|
|
189
|
+
# Allow setting just tool calls as a list
|
|
190
|
+
if isinstance(self._mock_response, list):
|
|
191
|
+
return LLMToolCallResult(
|
|
192
|
+
tool_calls=[
|
|
193
|
+
LLMToolCall(id=f"mock_{i}", name=tc["name"], arguments=tc.get("arguments", {}))
|
|
194
|
+
for i, tc in enumerate(self._mock_response)
|
|
195
|
+
],
|
|
196
|
+
finish_reason="tool_calls",
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
return LLMToolCallResult(content="mock response", finish_reason="stop")
|
|
200
|
+
|
|
201
|
+
async def cleanup(self) -> None:
|
|
202
|
+
"""Clean up resources (no-op for mock provider)."""
|
|
203
|
+
pass
|
|
204
|
+
|
|
205
|
+
def set_mock_response(self, response: Any) -> None:
|
|
206
|
+
"""
|
|
207
|
+
Set the response to return from mock calls.
|
|
208
|
+
|
|
209
|
+
Args:
|
|
210
|
+
response: The response to return. Can be:
|
|
211
|
+
- A dict/Pydantic model for regular calls
|
|
212
|
+
- An LLMToolCallResult for tool calls
|
|
213
|
+
- A list of tool call dicts for tool calls
|
|
214
|
+
- Any other value to return as-is
|
|
215
|
+
"""
|
|
216
|
+
self._mock_response = response
|
|
217
|
+
|
|
218
|
+
def get_mock_calls(self) -> list[dict]:
|
|
219
|
+
"""
|
|
220
|
+
Get the list of recorded mock calls.
|
|
221
|
+
|
|
222
|
+
Returns:
|
|
223
|
+
List of call records, each containing:
|
|
224
|
+
- provider: Provider name
|
|
225
|
+
- model: Model name
|
|
226
|
+
- messages: Messages sent
|
|
227
|
+
- response_format/tools: Format or tools used
|
|
228
|
+
- scope: Call scope
|
|
229
|
+
"""
|
|
230
|
+
return self._mock_calls
|
|
231
|
+
|
|
232
|
+
def clear_mock_calls(self) -> None:
|
|
233
|
+
"""Clear the recorded mock calls."""
|
|
234
|
+
self._mock_calls = []
|