simile 0.5.3__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.
Potentially problematic release.
This version of simile might be problematic. Click here for more details.
- simile/__init__.py +61 -0
- simile/auth_client.py +270 -0
- simile/client.py +885 -0
- simile/exceptions.py +47 -0
- simile/models.py +435 -0
- simile/resources.py +328 -0
- simile-0.5.3.dist-info/METADATA +63 -0
- simile-0.5.3.dist-info/RECORD +11 -0
- simile-0.5.3.dist-info/WHEEL +5 -0
- simile-0.5.3.dist-info/licenses/LICENSE +21 -0
- simile-0.5.3.dist-info/top_level.txt +1 -0
simile/exceptions.py
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
class SimileAPIError(Exception):
|
|
2
|
+
"""Base exception for Simile API client errors."""
|
|
3
|
+
|
|
4
|
+
def __init__(self, message: str, status_code: int = None, detail: str = None):
|
|
5
|
+
super().__init__(message)
|
|
6
|
+
self.status_code = status_code
|
|
7
|
+
self.detail = detail
|
|
8
|
+
|
|
9
|
+
def __str__(self):
|
|
10
|
+
if self.status_code and self.detail:
|
|
11
|
+
return f"{super().__str__()} (Status Code: {self.status_code}, Detail: {self.detail})"
|
|
12
|
+
elif self.status_code:
|
|
13
|
+
return f"{super().__str__()} (Status Code: {self.status_code})"
|
|
14
|
+
return super().__str__()
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class SimileAuthenticationError(SimileAPIError):
|
|
18
|
+
"""Exception for authentication errors (e.g., invalid API key)."""
|
|
19
|
+
|
|
20
|
+
def __init__(
|
|
21
|
+
self,
|
|
22
|
+
message: str = "Authentication failed. Ensure API key is valid.",
|
|
23
|
+
status_code: int = 401,
|
|
24
|
+
detail: str = None,
|
|
25
|
+
):
|
|
26
|
+
super().__init__(message, status_code, detail)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class SimileNotFoundError(SimileAPIError):
|
|
30
|
+
"""Exception for resource not found errors (404)."""
|
|
31
|
+
|
|
32
|
+
def __init__(
|
|
33
|
+
self,
|
|
34
|
+
message: str = "Resource not found.",
|
|
35
|
+
status_code: int = 404,
|
|
36
|
+
detail: str = None,
|
|
37
|
+
):
|
|
38
|
+
super().__init__(message, status_code, detail)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class SimileBadRequestError(SimileAPIError):
|
|
42
|
+
"""Exception for bad request errors (400)."""
|
|
43
|
+
|
|
44
|
+
def __init__(
|
|
45
|
+
self, message: str = "Bad request.", status_code: int = 400, detail: str = None
|
|
46
|
+
):
|
|
47
|
+
super().__init__(message, status_code, detail)
|
simile/models.py
ADDED
|
@@ -0,0 +1,435 @@
|
|
|
1
|
+
from typing import List, Dict, Any, Optional, Union, Literal
|
|
2
|
+
from pydantic import BaseModel, Field, validator
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from enum import Enum
|
|
5
|
+
import uuid
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Population(BaseModel):
|
|
9
|
+
population_id: uuid.UUID
|
|
10
|
+
name: str
|
|
11
|
+
description: Optional[str] = None
|
|
12
|
+
created_at: datetime
|
|
13
|
+
updated_at: datetime
|
|
14
|
+
metadata: Optional[Dict[str, Any]] = None
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class PopulationInfo(BaseModel):
|
|
18
|
+
population_id: uuid.UUID
|
|
19
|
+
name: str
|
|
20
|
+
description: Optional[str] = None
|
|
21
|
+
agent_count: int
|
|
22
|
+
metadata: Optional[Dict[str, Any]] = None
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class DataItem(BaseModel):
|
|
26
|
+
id: uuid.UUID
|
|
27
|
+
agent_id: uuid.UUID
|
|
28
|
+
data_type: str
|
|
29
|
+
content: Any
|
|
30
|
+
created_at: datetime
|
|
31
|
+
updated_at: datetime
|
|
32
|
+
metadata: Optional[Dict[str, Any]] = None
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class Agent(BaseModel):
|
|
36
|
+
agent_id: uuid.UUID
|
|
37
|
+
name: str
|
|
38
|
+
population_id: Optional[uuid.UUID] = None
|
|
39
|
+
created_at: datetime
|
|
40
|
+
updated_at: datetime
|
|
41
|
+
data_items: List[DataItem] = Field(default_factory=list)
|
|
42
|
+
source: Optional[str] = None
|
|
43
|
+
source_id: Optional[str] = None
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class CreatePopulationPayload(BaseModel):
|
|
47
|
+
name: str
|
|
48
|
+
description: Optional[str] = None
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class UpdatePopulationMetadataPayload(BaseModel):
|
|
52
|
+
metadata: Dict[str, Any]
|
|
53
|
+
mode: Optional[Literal["merge", "replace"]] = "merge"
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class UpdatePopulationInfoPayload(BaseModel):
|
|
57
|
+
name: Optional[str] = None
|
|
58
|
+
description: Optional[str] = None
|
|
59
|
+
metadata: Optional[Dict[str, Any]] = None
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class UpdateAgentInfoPayload(BaseModel):
|
|
63
|
+
name: str
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class InitialDataItemPayload(BaseModel):
|
|
67
|
+
data_type: str
|
|
68
|
+
content: Any
|
|
69
|
+
metadata: Optional[Dict[str, Any]] = None
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class CreateAgentPayload(BaseModel):
|
|
73
|
+
name: str
|
|
74
|
+
source: Optional[str] = None
|
|
75
|
+
source_id: Optional[str] = None
|
|
76
|
+
population_id: Optional[uuid.UUID] = None
|
|
77
|
+
agent_data: Optional[List[InitialDataItemPayload]] = None
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class CreateDataItemPayload(BaseModel):
|
|
81
|
+
data_type: str
|
|
82
|
+
content: Any
|
|
83
|
+
metadata: Optional[Dict[str, Any]] = None
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class UpdateDataItemPayload(BaseModel):
|
|
87
|
+
content: Any
|
|
88
|
+
metadata: Optional[Dict[str, Any]] = None
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class DeletionResponse(BaseModel):
|
|
92
|
+
message: str
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
# --- Generation Operation Models ---
|
|
96
|
+
class OpenGenerationRequest(BaseModel):
|
|
97
|
+
question: str
|
|
98
|
+
data_types: Optional[List[str]] = None
|
|
99
|
+
exclude_data_types: Optional[List[str]] = None
|
|
100
|
+
images: Optional[Dict[str, str]] = (
|
|
101
|
+
None # Dict of {description: url} for multiple images
|
|
102
|
+
)
|
|
103
|
+
reasoning: bool = False
|
|
104
|
+
evidence: bool = False
|
|
105
|
+
confidence: bool = False
|
|
106
|
+
memory_stream: Optional["MemoryStream"] = None
|
|
107
|
+
include_data_room: Optional[bool] = False
|
|
108
|
+
organization_id: Optional[Union[str, uuid.UUID]] = None
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
class OpenGenerationResponse(BaseModel):
|
|
112
|
+
question: str
|
|
113
|
+
answer: str
|
|
114
|
+
reasoning: Optional[str] = ""
|
|
115
|
+
evidence: Optional[Dict[str, str]] = None # Phrase -> citation number
|
|
116
|
+
evidence_content: Optional[Dict[str, Dict[str, str]]] = None # Citation number -> data item
|
|
117
|
+
confidence: Optional[float] = None
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
class ClosedGenerationRequest(BaseModel):
|
|
121
|
+
question: str
|
|
122
|
+
options: List[str]
|
|
123
|
+
data_types: Optional[List[str]] = None
|
|
124
|
+
exclude_data_types: Optional[List[str]] = None
|
|
125
|
+
images: Optional[Dict[str, str]] = None
|
|
126
|
+
reasoning: bool = False
|
|
127
|
+
evidence: bool = False
|
|
128
|
+
confidence: bool = False
|
|
129
|
+
memory_stream: Optional["MemoryStream"] = None
|
|
130
|
+
include_data_room: Optional[bool] = False
|
|
131
|
+
organization_id: Optional[Union[str, uuid.UUID]] = None
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
class ClosedGenerationResponse(BaseModel):
|
|
135
|
+
question: str
|
|
136
|
+
options: List[str]
|
|
137
|
+
response: str
|
|
138
|
+
reasoning: Optional[str] = ""
|
|
139
|
+
evidence: Optional[Dict[str, str]] = None # Phrase -> citation number
|
|
140
|
+
evidence_content: Optional[Dict[str, Dict[str, str]]] = None # Citation number -> data item
|
|
141
|
+
confidence: Optional[float] = None
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
class AddContextRequest(BaseModel):
|
|
145
|
+
context: str
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
class AddContextResponse(BaseModel):
|
|
149
|
+
message: str
|
|
150
|
+
session_id: uuid.UUID
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
# --- Survey Session Models ---
|
|
154
|
+
class TurnType(str, Enum):
|
|
155
|
+
"""Enum for different types of conversation turns."""
|
|
156
|
+
|
|
157
|
+
CONTEXT = "context"
|
|
158
|
+
IMAGE = "image"
|
|
159
|
+
OPEN_QUESTION = "open_question"
|
|
160
|
+
CLOSED_QUESTION = "closed_question"
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
class BaseTurn(BaseModel):
|
|
164
|
+
"""Base model for all conversation turns."""
|
|
165
|
+
|
|
166
|
+
timestamp: datetime = Field(default_factory=lambda: datetime.now())
|
|
167
|
+
type: TurnType
|
|
168
|
+
|
|
169
|
+
class Config:
|
|
170
|
+
use_enum_values = True
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
class ContextTurn(BaseTurn):
|
|
174
|
+
"""A context turn that provides background information."""
|
|
175
|
+
|
|
176
|
+
type: Literal[TurnType.CONTEXT] = TurnType.CONTEXT
|
|
177
|
+
user_context: str
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
class ImageTurn(BaseTurn):
|
|
181
|
+
"""A standalone image turn (e.g., for context or reference)."""
|
|
182
|
+
|
|
183
|
+
type: Literal[TurnType.IMAGE] = TurnType.IMAGE
|
|
184
|
+
images: Dict[str, str]
|
|
185
|
+
caption: Optional[str] = None
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
class OpenQuestionTurn(BaseTurn):
|
|
189
|
+
"""An open question-answer turn."""
|
|
190
|
+
|
|
191
|
+
type: Literal[TurnType.OPEN_QUESTION] = TurnType.OPEN_QUESTION
|
|
192
|
+
user_question: str
|
|
193
|
+
user_images: Optional[Dict[str, str]] = None
|
|
194
|
+
llm_response: Optional[str] = None
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
class ClosedQuestionTurn(BaseTurn):
|
|
198
|
+
"""A closed question-answer turn."""
|
|
199
|
+
|
|
200
|
+
type: Literal[TurnType.CLOSED_QUESTION] = TurnType.CLOSED_QUESTION
|
|
201
|
+
user_question: str
|
|
202
|
+
user_options: List[str]
|
|
203
|
+
user_images: Optional[Dict[str, str]] = None
|
|
204
|
+
llm_response: Optional[str] = None
|
|
205
|
+
|
|
206
|
+
@validator("user_options")
|
|
207
|
+
def validate_options(cls, v):
|
|
208
|
+
if not v:
|
|
209
|
+
raise ValueError("Closed questions must have at least one option")
|
|
210
|
+
if len(v) < 2:
|
|
211
|
+
raise ValueError("Closed questions should have at least two options")
|
|
212
|
+
return v
|
|
213
|
+
|
|
214
|
+
@validator("llm_response")
|
|
215
|
+
def validate_response(cls, v, values):
|
|
216
|
+
if (
|
|
217
|
+
v is not None
|
|
218
|
+
and "user_options" in values
|
|
219
|
+
and v not in values["user_options"]
|
|
220
|
+
):
|
|
221
|
+
raise ValueError(f"Response '{v}' must be one of the provided options")
|
|
222
|
+
return v
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
# Union type for all possible turn types
|
|
226
|
+
SurveySessionTurn = Union[ContextTurn, ImageTurn, OpenQuestionTurn, ClosedQuestionTurn]
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
class SurveySessionCreateResponse(BaseModel):
|
|
230
|
+
id: uuid.UUID # Session ID
|
|
231
|
+
agent_id: uuid.UUID
|
|
232
|
+
created_at: datetime
|
|
233
|
+
status: str
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
class SurveySessionDetailResponse(BaseModel):
|
|
237
|
+
"""Detailed survey session response with typed conversation turns."""
|
|
238
|
+
|
|
239
|
+
id: uuid.UUID
|
|
240
|
+
agent_id: uuid.UUID
|
|
241
|
+
created_at: datetime
|
|
242
|
+
updated_at: datetime
|
|
243
|
+
status: str
|
|
244
|
+
conversation_history: List[SurveySessionTurn] = Field(default_factory=list)
|
|
245
|
+
|
|
246
|
+
class Config:
|
|
247
|
+
json_encoders = {datetime: lambda v: v.isoformat()}
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
class SurveySessionListItemResponse(BaseModel):
|
|
251
|
+
"""Summary response for listing survey sessions."""
|
|
252
|
+
|
|
253
|
+
id: uuid.UUID
|
|
254
|
+
agent_id: uuid.UUID
|
|
255
|
+
created_at: datetime
|
|
256
|
+
updated_at: datetime
|
|
257
|
+
status: str
|
|
258
|
+
turn_count: int = Field(description="Number of turns in conversation history")
|
|
259
|
+
|
|
260
|
+
class Config:
|
|
261
|
+
json_encoders = {datetime: lambda v: v.isoformat()}
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
class SurveySessionCloseResponse(BaseModel):
|
|
265
|
+
id: uuid.UUID # Session ID
|
|
266
|
+
status: str
|
|
267
|
+
updated_at: datetime
|
|
268
|
+
message: Optional[str] = None
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
# --- Memory Stream Models (to replace Survey Sessions) ---
|
|
272
|
+
class MemoryTurnType(str, Enum):
|
|
273
|
+
"""Enum for different types of memory turns."""
|
|
274
|
+
|
|
275
|
+
CONTEXT = "context"
|
|
276
|
+
IMAGE = "image"
|
|
277
|
+
OPEN_QUESTION = "open_question"
|
|
278
|
+
CLOSED_QUESTION = "closed_question"
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
class BaseMemoryTurn(BaseModel):
|
|
282
|
+
"""Base model for all memory turns."""
|
|
283
|
+
|
|
284
|
+
timestamp: datetime = Field(default_factory=lambda: datetime.now())
|
|
285
|
+
type: MemoryTurnType
|
|
286
|
+
|
|
287
|
+
class Config:
|
|
288
|
+
use_enum_values = True
|
|
289
|
+
|
|
290
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
291
|
+
"""Convert to dictionary for serialization."""
|
|
292
|
+
data = self.model_dump()
|
|
293
|
+
# Remove timestamp - let API handle it
|
|
294
|
+
data.pop("timestamp", None)
|
|
295
|
+
# Ensure enum is serialized as string
|
|
296
|
+
if "type" in data:
|
|
297
|
+
if hasattr(data["type"], "value"):
|
|
298
|
+
data["type"] = data["type"].value
|
|
299
|
+
return data
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
class ContextMemoryTurn(BaseMemoryTurn):
|
|
303
|
+
"""A context turn that provides background information."""
|
|
304
|
+
|
|
305
|
+
type: MemoryTurnType = Field(default=MemoryTurnType.CONTEXT)
|
|
306
|
+
user_context: str
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
class ImageMemoryTurn(BaseMemoryTurn):
|
|
310
|
+
"""A standalone image turn (e.g., for context or reference)."""
|
|
311
|
+
|
|
312
|
+
type: MemoryTurnType = Field(default=MemoryTurnType.IMAGE)
|
|
313
|
+
images: Dict[str, str]
|
|
314
|
+
caption: Optional[str] = None
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
class OpenQuestionMemoryTurn(BaseMemoryTurn):
|
|
318
|
+
"""An open question-answer turn."""
|
|
319
|
+
|
|
320
|
+
type: MemoryTurnType = Field(default=MemoryTurnType.OPEN_QUESTION)
|
|
321
|
+
user_question: str
|
|
322
|
+
user_images: Optional[Dict[str, str]] = None
|
|
323
|
+
llm_response: Optional[str] = None
|
|
324
|
+
llm_reasoning: Optional[str] = None
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
class ClosedQuestionMemoryTurn(BaseMemoryTurn):
|
|
328
|
+
"""A closed question-answer turn."""
|
|
329
|
+
|
|
330
|
+
type: MemoryTurnType = Field(default=MemoryTurnType.CLOSED_QUESTION)
|
|
331
|
+
user_question: str
|
|
332
|
+
user_options: List[str]
|
|
333
|
+
user_images: Optional[Dict[str, str]] = None
|
|
334
|
+
llm_response: Optional[str] = None
|
|
335
|
+
llm_reasoning: Optional[str] = None
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
# Discriminated union of all memory turn types
|
|
339
|
+
MemoryTurn = Union[
|
|
340
|
+
ContextMemoryTurn, ImageMemoryTurn, OpenQuestionMemoryTurn, ClosedQuestionMemoryTurn
|
|
341
|
+
]
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
class MemoryStream(BaseModel):
|
|
345
|
+
"""
|
|
346
|
+
A flexible memory stream that can be passed to generation functions.
|
|
347
|
+
This replaces the session-based approach with a more flexible paradigm.
|
|
348
|
+
"""
|
|
349
|
+
|
|
350
|
+
turns: List[MemoryTurn] = Field(default_factory=list)
|
|
351
|
+
|
|
352
|
+
def add_turn(self, turn: MemoryTurn) -> None:
|
|
353
|
+
"""Add a turn to the memory stream."""
|
|
354
|
+
self.turns.append(turn)
|
|
355
|
+
|
|
356
|
+
def remove_turn(self, index: int) -> Optional[MemoryTurn]:
|
|
357
|
+
"""Remove a turn at the specified index."""
|
|
358
|
+
if 0 <= index < len(self.turns):
|
|
359
|
+
return self.turns.pop(index)
|
|
360
|
+
return None
|
|
361
|
+
|
|
362
|
+
def get_turns_by_type(self, turn_type: MemoryTurnType) -> List[MemoryTurn]:
|
|
363
|
+
"""Get all turns of a specific type."""
|
|
364
|
+
return [turn for turn in self.turns if turn.type == turn_type]
|
|
365
|
+
|
|
366
|
+
def get_last_turn(self) -> Optional[MemoryTurn]:
|
|
367
|
+
"""Get the most recent turn."""
|
|
368
|
+
return self.turns[-1] if self.turns else None
|
|
369
|
+
|
|
370
|
+
def clear(self) -> None:
|
|
371
|
+
"""Clear all turns from the memory stream."""
|
|
372
|
+
self.turns = []
|
|
373
|
+
|
|
374
|
+
def __len__(self) -> int:
|
|
375
|
+
"""Return the number of turns in the memory stream."""
|
|
376
|
+
return len(self.turns)
|
|
377
|
+
|
|
378
|
+
def __bool__(self) -> bool:
|
|
379
|
+
"""Return True if the memory stream has any turns."""
|
|
380
|
+
return bool(self.turns)
|
|
381
|
+
|
|
382
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
383
|
+
"""Convert memory stream to a dictionary for serialization."""
|
|
384
|
+
return {"turns": [turn.to_dict() for turn in self.turns]}
|
|
385
|
+
|
|
386
|
+
@classmethod
|
|
387
|
+
def from_dict(cls, data: Dict[str, Any]) -> "MemoryStream":
|
|
388
|
+
"""Create a MemoryStream from a dictionary."""
|
|
389
|
+
memory = cls()
|
|
390
|
+
for turn_data in data.get("turns", []):
|
|
391
|
+
turn_type = turn_data.get("type")
|
|
392
|
+
if turn_type == MemoryTurnType.CONTEXT:
|
|
393
|
+
memory.add_turn(ContextMemoryTurn(**turn_data))
|
|
394
|
+
elif turn_type == MemoryTurnType.IMAGE:
|
|
395
|
+
memory.add_turn(ImageMemoryTurn(**turn_data))
|
|
396
|
+
elif turn_type == MemoryTurnType.OPEN_QUESTION:
|
|
397
|
+
memory.add_turn(OpenQuestionMemoryTurn(**turn_data))
|
|
398
|
+
elif turn_type == MemoryTurnType.CLOSED_QUESTION:
|
|
399
|
+
memory.add_turn(ClosedQuestionMemoryTurn(**turn_data))
|
|
400
|
+
return memory
|
|
401
|
+
|
|
402
|
+
def fork(self, up_to_index: Optional[int] = None) -> "MemoryStream":
|
|
403
|
+
"""Create a copy of this memory stream, optionally up to a specific index."""
|
|
404
|
+
new_memory = MemoryStream()
|
|
405
|
+
turns_to_copy = (
|
|
406
|
+
self.turns[:up_to_index] if up_to_index is not None else self.turns
|
|
407
|
+
)
|
|
408
|
+
for turn in turns_to_copy:
|
|
409
|
+
new_memory.add_turn(turn.model_copy())
|
|
410
|
+
return new_memory
|
|
411
|
+
|
|
412
|
+
def filter_by_type(self, turn_type: MemoryTurnType) -> "MemoryStream":
|
|
413
|
+
"""Create a new memory stream with only turns of a specific type."""
|
|
414
|
+
new_memory = MemoryStream()
|
|
415
|
+
for turn in self.get_turns_by_type(turn_type):
|
|
416
|
+
new_memory.add_turn(turn.model_copy())
|
|
417
|
+
return new_memory
|
|
418
|
+
|
|
419
|
+
def get_question_answer_pairs(self) -> List[tuple]:
|
|
420
|
+
"""Extract question-answer pairs from the memory."""
|
|
421
|
+
pairs = []
|
|
422
|
+
for turn in self.turns:
|
|
423
|
+
if isinstance(turn, (OpenQuestionMemoryTurn, ClosedQuestionMemoryTurn)):
|
|
424
|
+
if turn.llm_response:
|
|
425
|
+
pairs.append((turn.user_question, turn.llm_response))
|
|
426
|
+
return pairs
|
|
427
|
+
|
|
428
|
+
def truncate(self, max_turns: int) -> None:
|
|
429
|
+
"""Keep only the most recent N turns."""
|
|
430
|
+
if len(self.turns) > max_turns:
|
|
431
|
+
self.turns = self.turns[-max_turns:]
|
|
432
|
+
|
|
433
|
+
def insert_turn(self, index: int, turn: MemoryTurn) -> None:
|
|
434
|
+
"""Insert a turn at a specific position."""
|
|
435
|
+
self.turns.insert(index, turn)
|