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.
Files changed (41) hide show
  1. flashlite/__init__.py +169 -0
  2. flashlite/cache/__init__.py +14 -0
  3. flashlite/cache/base.py +194 -0
  4. flashlite/cache/disk.py +285 -0
  5. flashlite/cache/memory.py +157 -0
  6. flashlite/client.py +671 -0
  7. flashlite/config.py +154 -0
  8. flashlite/conversation/__init__.py +30 -0
  9. flashlite/conversation/context.py +319 -0
  10. flashlite/conversation/manager.py +385 -0
  11. flashlite/conversation/multi_agent.py +378 -0
  12. flashlite/core/__init__.py +13 -0
  13. flashlite/core/completion.py +145 -0
  14. flashlite/core/messages.py +130 -0
  15. flashlite/middleware/__init__.py +18 -0
  16. flashlite/middleware/base.py +90 -0
  17. flashlite/middleware/cache.py +121 -0
  18. flashlite/middleware/logging.py +159 -0
  19. flashlite/middleware/rate_limit.py +211 -0
  20. flashlite/middleware/retry.py +149 -0
  21. flashlite/observability/__init__.py +34 -0
  22. flashlite/observability/callbacks.py +155 -0
  23. flashlite/observability/inspect_compat.py +266 -0
  24. flashlite/observability/logging.py +293 -0
  25. flashlite/observability/metrics.py +221 -0
  26. flashlite/py.typed +0 -0
  27. flashlite/structured/__init__.py +31 -0
  28. flashlite/structured/outputs.py +189 -0
  29. flashlite/structured/schema.py +165 -0
  30. flashlite/templating/__init__.py +11 -0
  31. flashlite/templating/engine.py +217 -0
  32. flashlite/templating/filters.py +143 -0
  33. flashlite/templating/registry.py +165 -0
  34. flashlite/tools/__init__.py +74 -0
  35. flashlite/tools/definitions.py +382 -0
  36. flashlite/tools/execution.py +353 -0
  37. flashlite/types.py +233 -0
  38. flashlite-0.1.0.dist-info/METADATA +173 -0
  39. flashlite-0.1.0.dist-info/RECORD +41 -0
  40. flashlite-0.1.0.dist-info/WHEEL +4 -0
  41. 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})"