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.
Files changed (29) hide show
  1. hindsight_api/__init__.py +1 -1
  2. hindsight_api/alembic/versions/5a366d414dce_initial_schema.py +16 -2
  3. hindsight_api/api/http.py +83 -1
  4. hindsight_api/banner.py +3 -0
  5. hindsight_api/config.py +44 -6
  6. hindsight_api/daemon.py +18 -112
  7. hindsight_api/engine/llm_interface.py +146 -0
  8. hindsight_api/engine/llm_wrapper.py +304 -1327
  9. hindsight_api/engine/memory_engine.py +125 -41
  10. hindsight_api/engine/providers/__init__.py +14 -0
  11. hindsight_api/engine/providers/anthropic_llm.py +434 -0
  12. hindsight_api/engine/providers/claude_code_llm.py +352 -0
  13. hindsight_api/engine/providers/codex_llm.py +527 -0
  14. hindsight_api/engine/providers/gemini_llm.py +502 -0
  15. hindsight_api/engine/providers/mock_llm.py +234 -0
  16. hindsight_api/engine/providers/openai_compatible_llm.py +745 -0
  17. hindsight_api/engine/retain/fact_extraction.py +13 -9
  18. hindsight_api/engine/retain/fact_storage.py +5 -3
  19. hindsight_api/extensions/__init__.py +10 -0
  20. hindsight_api/extensions/builtin/tenant.py +36 -0
  21. hindsight_api/extensions/operation_validator.py +129 -0
  22. hindsight_api/main.py +6 -21
  23. hindsight_api/migrations.py +75 -0
  24. hindsight_api/worker/main.py +41 -11
  25. hindsight_api/worker/poller.py +26 -14
  26. {hindsight_api-0.4.6.dist-info → hindsight_api-0.4.8.dist-info}/METADATA +2 -1
  27. {hindsight_api-0.4.6.dist-info → hindsight_api-0.4.8.dist-info}/RECORD +29 -21
  28. {hindsight_api-0.4.6.dist-info → hindsight_api-0.4.8.dist-info}/WHEEL +0 -0
  29. {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 = []