flashlite 0.1.0__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.
- flashlite/__init__.py +169 -0
- flashlite/cache/__init__.py +14 -0
- flashlite/cache/base.py +194 -0
- flashlite/cache/disk.py +285 -0
- flashlite/cache/memory.py +157 -0
- flashlite/client.py +671 -0
- flashlite/config.py +154 -0
- flashlite/conversation/__init__.py +30 -0
- flashlite/conversation/context.py +319 -0
- flashlite/conversation/manager.py +385 -0
- flashlite/conversation/multi_agent.py +378 -0
- flashlite/core/__init__.py +13 -0
- flashlite/core/completion.py +145 -0
- flashlite/core/messages.py +130 -0
- flashlite/middleware/__init__.py +18 -0
- flashlite/middleware/base.py +90 -0
- flashlite/middleware/cache.py +121 -0
- flashlite/middleware/logging.py +159 -0
- flashlite/middleware/rate_limit.py +211 -0
- flashlite/middleware/retry.py +149 -0
- flashlite/observability/__init__.py +34 -0
- flashlite/observability/callbacks.py +155 -0
- flashlite/observability/inspect_compat.py +266 -0
- flashlite/observability/logging.py +293 -0
- flashlite/observability/metrics.py +221 -0
- flashlite/py.typed +0 -0
- flashlite/structured/__init__.py +31 -0
- flashlite/structured/outputs.py +189 -0
- flashlite/structured/schema.py +165 -0
- flashlite/templating/__init__.py +11 -0
- flashlite/templating/engine.py +217 -0
- flashlite/templating/filters.py +143 -0
- flashlite/templating/registry.py +165 -0
- flashlite/tools/__init__.py +74 -0
- flashlite/tools/definitions.py +382 -0
- flashlite/tools/execution.py +353 -0
- flashlite/types.py +233 -0
- flashlite-0.1.0.dist-info/METADATA +173 -0
- flashlite-0.1.0.dist-info/RECORD +41 -0
- flashlite-0.1.0.dist-info/WHEEL +4 -0
- flashlite-0.1.0.dist-info/licenses/LICENSE.md +21 -0
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
"""Conversation management for multi-turn interactions."""
|
|
2
|
+
|
|
3
|
+
import copy
|
|
4
|
+
import json
|
|
5
|
+
import uuid
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
from datetime import UTC, datetime
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import TYPE_CHECKING, Any, TypeVar
|
|
10
|
+
|
|
11
|
+
from pydantic import BaseModel
|
|
12
|
+
|
|
13
|
+
from ..types import CompletionResponse, Message, Messages
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
from ..client import Flashlite
|
|
17
|
+
|
|
18
|
+
T = TypeVar("T", bound=BaseModel)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass
|
|
22
|
+
class Turn:
|
|
23
|
+
"""A single turn in a conversation."""
|
|
24
|
+
|
|
25
|
+
role: str
|
|
26
|
+
content: str
|
|
27
|
+
model: str | None = None
|
|
28
|
+
timestamp: str = field(default_factory=lambda: datetime.now(UTC).isoformat())
|
|
29
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
30
|
+
|
|
31
|
+
def to_message(self) -> Message:
|
|
32
|
+
"""Convert to a message dict."""
|
|
33
|
+
return {"role": self.role, "content": self.content}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass
|
|
37
|
+
class ConversationState:
|
|
38
|
+
"""Serializable state of a conversation."""
|
|
39
|
+
|
|
40
|
+
id: str
|
|
41
|
+
system_prompt: str | None
|
|
42
|
+
turns: list[Turn]
|
|
43
|
+
default_model: str | None
|
|
44
|
+
metadata: dict[str, Any]
|
|
45
|
+
created_at: str
|
|
46
|
+
updated_at: str
|
|
47
|
+
|
|
48
|
+
def to_dict(self) -> dict[str, Any]:
|
|
49
|
+
"""Convert to dictionary for serialization."""
|
|
50
|
+
return {
|
|
51
|
+
"id": self.id,
|
|
52
|
+
"system_prompt": self.system_prompt,
|
|
53
|
+
"turns": [
|
|
54
|
+
{
|
|
55
|
+
"role": t.role,
|
|
56
|
+
"content": t.content,
|
|
57
|
+
"model": t.model,
|
|
58
|
+
"timestamp": t.timestamp,
|
|
59
|
+
"metadata": t.metadata,
|
|
60
|
+
}
|
|
61
|
+
for t in self.turns
|
|
62
|
+
],
|
|
63
|
+
"default_model": self.default_model,
|
|
64
|
+
"metadata": self.metadata,
|
|
65
|
+
"created_at": self.created_at,
|
|
66
|
+
"updated_at": self.updated_at,
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
@classmethod
|
|
70
|
+
def from_dict(cls, data: dict[str, Any]) -> "ConversationState":
|
|
71
|
+
"""Create from dictionary."""
|
|
72
|
+
return cls(
|
|
73
|
+
id=data["id"],
|
|
74
|
+
system_prompt=data.get("system_prompt"),
|
|
75
|
+
turns=[
|
|
76
|
+
Turn(
|
|
77
|
+
role=t["role"],
|
|
78
|
+
content=t["content"],
|
|
79
|
+
model=t.get("model"),
|
|
80
|
+
timestamp=t.get("timestamp", ""),
|
|
81
|
+
metadata=t.get("metadata", {}),
|
|
82
|
+
)
|
|
83
|
+
for t in data.get("turns", [])
|
|
84
|
+
],
|
|
85
|
+
default_model=data.get("default_model"),
|
|
86
|
+
metadata=data.get("metadata", {}),
|
|
87
|
+
created_at=data.get("created_at", ""),
|
|
88
|
+
updated_at=data.get("updated_at", ""),
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class Conversation:
|
|
93
|
+
"""
|
|
94
|
+
Manages multi-turn conversations with LLMs.
|
|
95
|
+
|
|
96
|
+
Features:
|
|
97
|
+
- Automatic message history management
|
|
98
|
+
- System prompt support
|
|
99
|
+
- Model switching mid-conversation
|
|
100
|
+
- Branching/forking for tree-of-thought patterns
|
|
101
|
+
- Serialization for persistence
|
|
102
|
+
|
|
103
|
+
Example:
|
|
104
|
+
client = Flashlite(default_model="gpt-4o")
|
|
105
|
+
conv = Conversation(client, system="You are a helpful assistant.")
|
|
106
|
+
|
|
107
|
+
# Multi-turn conversation
|
|
108
|
+
response1 = await conv.say("What is Python?")
|
|
109
|
+
response2 = await conv.say("How do I install it?")
|
|
110
|
+
|
|
111
|
+
# Fork for exploration
|
|
112
|
+
branch = conv.fork()
|
|
113
|
+
alt_response = await branch.say("What about JavaScript instead?")
|
|
114
|
+
|
|
115
|
+
# Save and restore
|
|
116
|
+
conv.save("conversation.json")
|
|
117
|
+
restored = Conversation.load(client, "conversation.json")
|
|
118
|
+
"""
|
|
119
|
+
|
|
120
|
+
def __init__(
|
|
121
|
+
self,
|
|
122
|
+
client: "Flashlite",
|
|
123
|
+
system: str | None = None,
|
|
124
|
+
model: str | None = None,
|
|
125
|
+
max_turns: int | None = None,
|
|
126
|
+
conversation_id: str | None = None,
|
|
127
|
+
metadata: dict[str, Any] | None = None,
|
|
128
|
+
):
|
|
129
|
+
"""
|
|
130
|
+
Initialize a new conversation.
|
|
131
|
+
|
|
132
|
+
Args:
|
|
133
|
+
client: Flashlite client to use for completions
|
|
134
|
+
system: System prompt for the conversation
|
|
135
|
+
model: Default model to use (overrides client default)
|
|
136
|
+
max_turns: Maximum number of turns to keep (None = unlimited)
|
|
137
|
+
conversation_id: Custom conversation ID (auto-generated if not provided)
|
|
138
|
+
metadata: Custom metadata to attach to the conversation
|
|
139
|
+
"""
|
|
140
|
+
self._client = client
|
|
141
|
+
self._system = system
|
|
142
|
+
self._model = model
|
|
143
|
+
self._max_turns = max_turns
|
|
144
|
+
self._turns: list[Turn] = []
|
|
145
|
+
self._id = conversation_id or str(uuid.uuid4())
|
|
146
|
+
self._metadata = metadata or {}
|
|
147
|
+
self._created_at = datetime.now(UTC).isoformat()
|
|
148
|
+
self._updated_at = self._created_at
|
|
149
|
+
|
|
150
|
+
async def say(
|
|
151
|
+
self,
|
|
152
|
+
message: str,
|
|
153
|
+
*,
|
|
154
|
+
model: str | None = None,
|
|
155
|
+
response_model: type[T] | None = None,
|
|
156
|
+
**kwargs: Any,
|
|
157
|
+
) -> CompletionResponse | T:
|
|
158
|
+
"""
|
|
159
|
+
Send a message and get a response.
|
|
160
|
+
|
|
161
|
+
The message is added to history, and the assistant's response
|
|
162
|
+
is also added automatically.
|
|
163
|
+
|
|
164
|
+
Args:
|
|
165
|
+
message: The user message to send
|
|
166
|
+
model: Model to use for this turn (overrides conversation default)
|
|
167
|
+
response_model: Pydantic model for structured output
|
|
168
|
+
**kwargs: Additional arguments for complete()
|
|
169
|
+
|
|
170
|
+
Returns:
|
|
171
|
+
CompletionResponse or validated model instance if response_model provided
|
|
172
|
+
"""
|
|
173
|
+
# Add user message to history
|
|
174
|
+
self._add_turn("user", message, model=model)
|
|
175
|
+
|
|
176
|
+
# Build messages
|
|
177
|
+
messages = self._build_messages()
|
|
178
|
+
|
|
179
|
+
# Determine model
|
|
180
|
+
effective_model = model or self._model
|
|
181
|
+
|
|
182
|
+
# Make completion
|
|
183
|
+
response = await self._client.complete(
|
|
184
|
+
model=effective_model,
|
|
185
|
+
messages=messages,
|
|
186
|
+
response_model=response_model,
|
|
187
|
+
**kwargs,
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
# Extract content and add to history
|
|
191
|
+
if isinstance(response, BaseModel):
|
|
192
|
+
# For structured outputs, store the JSON representation
|
|
193
|
+
content = response.model_dump_json()
|
|
194
|
+
self._add_turn("assistant", content, model=effective_model)
|
|
195
|
+
return response
|
|
196
|
+
else:
|
|
197
|
+
self._add_turn("assistant", response.content, model=response.model)
|
|
198
|
+
return response
|
|
199
|
+
|
|
200
|
+
def add_user_message(self, content: str) -> None:
|
|
201
|
+
"""Add a user message to history without making a completion."""
|
|
202
|
+
self._add_turn("user", content)
|
|
203
|
+
|
|
204
|
+
def add_assistant_message(self, content: str, model: str | None = None) -> None:
|
|
205
|
+
"""Add an assistant message to history."""
|
|
206
|
+
self._add_turn("assistant", content, model=model)
|
|
207
|
+
|
|
208
|
+
def _add_turn(
|
|
209
|
+
self,
|
|
210
|
+
role: str,
|
|
211
|
+
content: str,
|
|
212
|
+
model: str | None = None,
|
|
213
|
+
) -> None:
|
|
214
|
+
"""Add a turn to the conversation history."""
|
|
215
|
+
turn = Turn(role=role, content=content, model=model)
|
|
216
|
+
self._turns.append(turn)
|
|
217
|
+
self._updated_at = datetime.now(UTC).isoformat()
|
|
218
|
+
|
|
219
|
+
# Enforce max_turns limit
|
|
220
|
+
if self._max_turns is not None:
|
|
221
|
+
# Keep the most recent turns, but always keep assistant responses
|
|
222
|
+
# paired with their user messages
|
|
223
|
+
while len(self._turns) > self._max_turns * 2:
|
|
224
|
+
self._turns.pop(0)
|
|
225
|
+
|
|
226
|
+
def _build_messages(self) -> Messages:
|
|
227
|
+
"""Build the messages list for completion."""
|
|
228
|
+
messages: list[Message] = []
|
|
229
|
+
|
|
230
|
+
# Add system prompt if present
|
|
231
|
+
if self._system:
|
|
232
|
+
messages.append({"role": "system", "content": self._system})
|
|
233
|
+
|
|
234
|
+
# Add conversation history
|
|
235
|
+
for turn in self._turns:
|
|
236
|
+
messages.append(turn.to_message())
|
|
237
|
+
|
|
238
|
+
return messages
|
|
239
|
+
|
|
240
|
+
def fork(self) -> "Conversation":
|
|
241
|
+
"""
|
|
242
|
+
Create a branch of this conversation.
|
|
243
|
+
|
|
244
|
+
The branch shares history up to this point but can diverge.
|
|
245
|
+
Useful for exploring alternative conversation paths.
|
|
246
|
+
|
|
247
|
+
Returns:
|
|
248
|
+
A new Conversation with copied history
|
|
249
|
+
"""
|
|
250
|
+
branch = Conversation(
|
|
251
|
+
client=self._client,
|
|
252
|
+
system=self._system,
|
|
253
|
+
model=self._model,
|
|
254
|
+
max_turns=self._max_turns,
|
|
255
|
+
metadata=copy.deepcopy(self._metadata),
|
|
256
|
+
)
|
|
257
|
+
branch._turns = copy.deepcopy(self._turns)
|
|
258
|
+
branch._created_at = self._created_at
|
|
259
|
+
return branch
|
|
260
|
+
|
|
261
|
+
def clear(self) -> None:
|
|
262
|
+
"""Clear conversation history (keeps system prompt)."""
|
|
263
|
+
self._turns = []
|
|
264
|
+
self._updated_at = datetime.now(UTC).isoformat()
|
|
265
|
+
|
|
266
|
+
def rollback(self, n: int = 1) -> list[Turn]:
|
|
267
|
+
"""
|
|
268
|
+
Remove the last n turns from history.
|
|
269
|
+
|
|
270
|
+
Args:
|
|
271
|
+
n: Number of turns to remove
|
|
272
|
+
|
|
273
|
+
Returns:
|
|
274
|
+
The removed turns
|
|
275
|
+
"""
|
|
276
|
+
removed = []
|
|
277
|
+
for _ in range(min(n, len(self._turns))):
|
|
278
|
+
removed.append(self._turns.pop())
|
|
279
|
+
self._updated_at = datetime.now(UTC).isoformat()
|
|
280
|
+
return list(reversed(removed))
|
|
281
|
+
|
|
282
|
+
def get_state(self) -> ConversationState:
|
|
283
|
+
"""Get the current conversation state."""
|
|
284
|
+
return ConversationState(
|
|
285
|
+
id=self._id,
|
|
286
|
+
system_prompt=self._system,
|
|
287
|
+
turns=copy.deepcopy(self._turns),
|
|
288
|
+
default_model=self._model,
|
|
289
|
+
metadata=copy.deepcopy(self._metadata),
|
|
290
|
+
created_at=self._created_at,
|
|
291
|
+
updated_at=self._updated_at,
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
def save(self, path: str | Path) -> None:
|
|
295
|
+
"""
|
|
296
|
+
Save conversation to a JSON file.
|
|
297
|
+
|
|
298
|
+
Args:
|
|
299
|
+
path: Path to save to
|
|
300
|
+
"""
|
|
301
|
+
state = self.get_state()
|
|
302
|
+
with open(path, "w") as f:
|
|
303
|
+
json.dump(state.to_dict(), f, indent=2)
|
|
304
|
+
|
|
305
|
+
@classmethod
|
|
306
|
+
def load(
|
|
307
|
+
cls,
|
|
308
|
+
client: "Flashlite",
|
|
309
|
+
path: str | Path,
|
|
310
|
+
) -> "Conversation":
|
|
311
|
+
"""
|
|
312
|
+
Load a conversation from a JSON file.
|
|
313
|
+
|
|
314
|
+
Args:
|
|
315
|
+
client: Flashlite client to use
|
|
316
|
+
path: Path to load from
|
|
317
|
+
|
|
318
|
+
Returns:
|
|
319
|
+
Restored Conversation instance
|
|
320
|
+
"""
|
|
321
|
+
with open(path) as f:
|
|
322
|
+
data = json.load(f)
|
|
323
|
+
|
|
324
|
+
state = ConversationState.from_dict(data)
|
|
325
|
+
|
|
326
|
+
conv = cls(
|
|
327
|
+
client=client,
|
|
328
|
+
system=state.system_prompt,
|
|
329
|
+
model=state.default_model,
|
|
330
|
+
conversation_id=state.id,
|
|
331
|
+
metadata=state.metadata,
|
|
332
|
+
)
|
|
333
|
+
conv._turns = state.turns
|
|
334
|
+
conv._created_at = state.created_at
|
|
335
|
+
conv._updated_at = state.updated_at
|
|
336
|
+
|
|
337
|
+
return conv
|
|
338
|
+
|
|
339
|
+
@property
|
|
340
|
+
def id(self) -> str:
|
|
341
|
+
"""Conversation ID."""
|
|
342
|
+
return self._id
|
|
343
|
+
|
|
344
|
+
@property
|
|
345
|
+
def system(self) -> str | None:
|
|
346
|
+
"""System prompt."""
|
|
347
|
+
return self._system
|
|
348
|
+
|
|
349
|
+
@system.setter
|
|
350
|
+
def system(self, value: str | None) -> None:
|
|
351
|
+
"""Set system prompt."""
|
|
352
|
+
self._system = value
|
|
353
|
+
self._updated_at = datetime.now(UTC).isoformat()
|
|
354
|
+
|
|
355
|
+
@property
|
|
356
|
+
def model(self) -> str | None:
|
|
357
|
+
"""Default model for this conversation."""
|
|
358
|
+
return self._model
|
|
359
|
+
|
|
360
|
+
@model.setter
|
|
361
|
+
def model(self, value: str | None) -> None:
|
|
362
|
+
"""Set default model."""
|
|
363
|
+
self._model = value
|
|
364
|
+
|
|
365
|
+
@property
|
|
366
|
+
def turns(self) -> list[Turn]:
|
|
367
|
+
"""List of conversation turns (read-only copy)."""
|
|
368
|
+
return copy.deepcopy(self._turns)
|
|
369
|
+
|
|
370
|
+
@property
|
|
371
|
+
def messages(self) -> Messages:
|
|
372
|
+
"""Current messages list for the conversation."""
|
|
373
|
+
return self._build_messages()
|
|
374
|
+
|
|
375
|
+
@property
|
|
376
|
+
def turn_count(self) -> int:
|
|
377
|
+
"""Number of turns in the conversation."""
|
|
378
|
+
return len(self._turns)
|
|
379
|
+
|
|
380
|
+
def __len__(self) -> int:
|
|
381
|
+
"""Number of turns."""
|
|
382
|
+
return len(self._turns)
|
|
383
|
+
|
|
384
|
+
def __repr__(self) -> str:
|
|
385
|
+
return f"Conversation(id={self._id!r}, turns={len(self._turns)}, model={self._model!r})"
|