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,378 @@
|
|
|
1
|
+
"""Multi-agent conversation support for agent-to-agent interactions."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from typing import TYPE_CHECKING, Any
|
|
5
|
+
|
|
6
|
+
from ..types import CompletionResponse
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from ..client import Flashlite
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class Agent:
|
|
14
|
+
"""
|
|
15
|
+
An agent with a name, persona, and optional model override.
|
|
16
|
+
|
|
17
|
+
Attributes:
|
|
18
|
+
name: Display name for the agent (used in transcript and message attribution)
|
|
19
|
+
system_prompt: The agent's personality, instructions, and behavior guidelines
|
|
20
|
+
model: Optional model override (uses MultiAgentChat default if None)
|
|
21
|
+
|
|
22
|
+
Example:
|
|
23
|
+
agent = Agent(
|
|
24
|
+
name="Scientist",
|
|
25
|
+
system_prompt="You are a curious scientist who loves experiments.",
|
|
26
|
+
model="gpt-4o", # Optional: use specific model for this agent
|
|
27
|
+
)
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
name: str
|
|
31
|
+
system_prompt: str
|
|
32
|
+
model: str | None = None
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass
|
|
36
|
+
class ChatMessage:
|
|
37
|
+
"""A message in the multi-agent conversation."""
|
|
38
|
+
|
|
39
|
+
agent_name: str
|
|
40
|
+
content: str
|
|
41
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class MultiAgentChat:
|
|
45
|
+
"""
|
|
46
|
+
Manages conversations between multiple AI agents.
|
|
47
|
+
|
|
48
|
+
This class enables agent-to-agent conversations where multiple AI agents
|
|
49
|
+
can discuss, debate, or collaborate. Each agent maintains its own persona
|
|
50
|
+
and sees the conversation from its perspective.
|
|
51
|
+
|
|
52
|
+
Key features:
|
|
53
|
+
- Multiple agents with different personas and optionally different models
|
|
54
|
+
- Automatic context building from each agent's perspective
|
|
55
|
+
- Round-robin or directed turn-taking
|
|
56
|
+
- Full conversation transcript with metadata
|
|
57
|
+
- Support for injecting external messages (moderator, user input)
|
|
58
|
+
|
|
59
|
+
How it works:
|
|
60
|
+
- Each agent has a system prompt defining their persona
|
|
61
|
+
- When an agent speaks, they see:
|
|
62
|
+
- Their own previous messages as "assistant" role
|
|
63
|
+
- Other agents' messages as "user" role with name attribution
|
|
64
|
+
- This creates natural back-and-forth conversation
|
|
65
|
+
|
|
66
|
+
Example:
|
|
67
|
+
client = Flashlite(default_model="gpt-4o-mini")
|
|
68
|
+
chat = MultiAgentChat(client)
|
|
69
|
+
|
|
70
|
+
# Add agents with different personas
|
|
71
|
+
chat.add_agent(Agent(
|
|
72
|
+
name="Optimist",
|
|
73
|
+
system_prompt="You see the positive side of everything. Be concise."
|
|
74
|
+
))
|
|
75
|
+
chat.add_agent(Agent(
|
|
76
|
+
name="Skeptic",
|
|
77
|
+
system_prompt="You question assumptions. Be concise."
|
|
78
|
+
))
|
|
79
|
+
|
|
80
|
+
# Start with a topic
|
|
81
|
+
chat.add_message("Moderator", "Discuss: Will AI help or hurt jobs?")
|
|
82
|
+
|
|
83
|
+
# Have agents take turns
|
|
84
|
+
await chat.speak("Optimist") # Optimist responds
|
|
85
|
+
await chat.speak("Skeptic") # Skeptic responds to Optimist
|
|
86
|
+
await chat.speak("Optimist") # Continue the debate
|
|
87
|
+
|
|
88
|
+
# Or use round-robin for structured turns
|
|
89
|
+
await chat.round_robin(rounds=2)
|
|
90
|
+
|
|
91
|
+
# Get formatted transcript
|
|
92
|
+
print(chat.format_transcript())
|
|
93
|
+
"""
|
|
94
|
+
|
|
95
|
+
def __init__(
|
|
96
|
+
self,
|
|
97
|
+
client: "Flashlite",
|
|
98
|
+
default_model: str | None = None,
|
|
99
|
+
):
|
|
100
|
+
"""
|
|
101
|
+
Initialize a multi-agent chat.
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
client: Flashlite client for making completions
|
|
105
|
+
default_model: Default model for agents (uses client default if None)
|
|
106
|
+
"""
|
|
107
|
+
self._client = client
|
|
108
|
+
self._default_model = default_model
|
|
109
|
+
self._agents: dict[str, Agent] = {}
|
|
110
|
+
self._transcript: list[ChatMessage] = []
|
|
111
|
+
|
|
112
|
+
def add_agent(self, agent: Agent) -> "MultiAgentChat":
|
|
113
|
+
"""
|
|
114
|
+
Add an agent to the chat.
|
|
115
|
+
|
|
116
|
+
Args:
|
|
117
|
+
agent: Agent to add
|
|
118
|
+
|
|
119
|
+
Returns:
|
|
120
|
+
Self for method chaining
|
|
121
|
+
|
|
122
|
+
Example:
|
|
123
|
+
chat.add_agent(Agent("Alice", "You are helpful."))
|
|
124
|
+
.add_agent(Agent("Bob", "You are curious."))
|
|
125
|
+
"""
|
|
126
|
+
self._agents[agent.name] = agent
|
|
127
|
+
return self
|
|
128
|
+
|
|
129
|
+
def remove_agent(self, agent_name: str) -> bool:
|
|
130
|
+
"""
|
|
131
|
+
Remove an agent from the chat.
|
|
132
|
+
|
|
133
|
+
Args:
|
|
134
|
+
agent_name: Name of agent to remove
|
|
135
|
+
|
|
136
|
+
Returns:
|
|
137
|
+
True if agent was removed, False if not found
|
|
138
|
+
"""
|
|
139
|
+
if agent_name in self._agents:
|
|
140
|
+
del self._agents[agent_name]
|
|
141
|
+
return True
|
|
142
|
+
return False
|
|
143
|
+
|
|
144
|
+
def add_message(
|
|
145
|
+
self,
|
|
146
|
+
agent_name: str,
|
|
147
|
+
content: str,
|
|
148
|
+
metadata: dict[str, Any] | None = None,
|
|
149
|
+
) -> "MultiAgentChat":
|
|
150
|
+
"""
|
|
151
|
+
Manually add a message to the transcript.
|
|
152
|
+
|
|
153
|
+
Useful for:
|
|
154
|
+
- Injecting moderator or facilitator prompts
|
|
155
|
+
- Adding user input to the conversation
|
|
156
|
+
- Simulating agent messages for testing
|
|
157
|
+
|
|
158
|
+
Args:
|
|
159
|
+
agent_name: Name to attribute the message to
|
|
160
|
+
content: Message content
|
|
161
|
+
metadata: Optional metadata to attach
|
|
162
|
+
|
|
163
|
+
Returns:
|
|
164
|
+
Self for method chaining
|
|
165
|
+
"""
|
|
166
|
+
self._transcript.append(
|
|
167
|
+
ChatMessage(
|
|
168
|
+
agent_name=agent_name,
|
|
169
|
+
content=content,
|
|
170
|
+
metadata=metadata or {},
|
|
171
|
+
)
|
|
172
|
+
)
|
|
173
|
+
return self
|
|
174
|
+
|
|
175
|
+
async def speak(
|
|
176
|
+
self,
|
|
177
|
+
agent_name: str,
|
|
178
|
+
additional_context: str | None = None,
|
|
179
|
+
**kwargs: Any,
|
|
180
|
+
) -> str:
|
|
181
|
+
"""
|
|
182
|
+
Have an agent respond to the conversation.
|
|
183
|
+
|
|
184
|
+
The agent sees the full conversation history from their perspective:
|
|
185
|
+
- Their own previous messages appear as "assistant" messages
|
|
186
|
+
- Other agents' messages appear as "user" messages with name attribution
|
|
187
|
+
|
|
188
|
+
Args:
|
|
189
|
+
agent_name: Name of the agent to speak
|
|
190
|
+
additional_context: Optional extra context/instruction for this turn
|
|
191
|
+
**kwargs: Additional kwargs passed to client.complete()
|
|
192
|
+
|
|
193
|
+
Returns:
|
|
194
|
+
The agent's response content
|
|
195
|
+
|
|
196
|
+
Raises:
|
|
197
|
+
ValueError: If agent_name is not found
|
|
198
|
+
"""
|
|
199
|
+
if agent_name not in self._agents:
|
|
200
|
+
raise ValueError(
|
|
201
|
+
f"Unknown agent: {agent_name}. Available agents: {list(self._agents.keys())}"
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
agent = self._agents[agent_name]
|
|
205
|
+
|
|
206
|
+
# Build messages from this agent's perspective
|
|
207
|
+
messages = self._build_messages_for(agent)
|
|
208
|
+
|
|
209
|
+
# Add any additional context as a user message
|
|
210
|
+
if additional_context:
|
|
211
|
+
messages.append({"role": "user", "content": additional_context})
|
|
212
|
+
|
|
213
|
+
# Make completion
|
|
214
|
+
response: CompletionResponse = await self._client.complete(
|
|
215
|
+
model=agent.model or self._default_model,
|
|
216
|
+
messages=messages,
|
|
217
|
+
**kwargs,
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
# Record in transcript with metadata
|
|
221
|
+
self._transcript.append(
|
|
222
|
+
ChatMessage(
|
|
223
|
+
agent_name=agent_name,
|
|
224
|
+
content=response.content,
|
|
225
|
+
metadata={
|
|
226
|
+
"model": response.model,
|
|
227
|
+
"tokens": response.usage.total_tokens if response.usage else None,
|
|
228
|
+
},
|
|
229
|
+
)
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
return response.content
|
|
233
|
+
|
|
234
|
+
def _build_messages_for(self, agent: Agent) -> list[dict[str, str]]:
|
|
235
|
+
"""
|
|
236
|
+
Build the message history from a specific agent's perspective.
|
|
237
|
+
|
|
238
|
+
The agent's own messages become "assistant" role (what they said).
|
|
239
|
+
Other agents' messages become "user" role with speaker attribution.
|
|
240
|
+
"""
|
|
241
|
+
messages: list[dict[str, str]] = []
|
|
242
|
+
|
|
243
|
+
# System prompt for this agent
|
|
244
|
+
messages.append({"role": "system", "content": agent.system_prompt})
|
|
245
|
+
|
|
246
|
+
# Add conversation history
|
|
247
|
+
for msg in self._transcript:
|
|
248
|
+
if msg.agent_name == agent.name:
|
|
249
|
+
# Agent's own previous messages
|
|
250
|
+
messages.append({"role": "assistant", "content": msg.content})
|
|
251
|
+
else:
|
|
252
|
+
# Other agents'/sources' messages - prefix with speaker name
|
|
253
|
+
messages.append({"role": "user", "content": f"[{msg.agent_name}]: {msg.content}"})
|
|
254
|
+
|
|
255
|
+
return messages
|
|
256
|
+
|
|
257
|
+
async def round_robin(
|
|
258
|
+
self,
|
|
259
|
+
rounds: int = 1,
|
|
260
|
+
**kwargs: Any,
|
|
261
|
+
) -> list[str]:
|
|
262
|
+
"""
|
|
263
|
+
Have all agents speak in turn for the specified number of rounds.
|
|
264
|
+
|
|
265
|
+
Each agent speaks once per round, in the order they were added.
|
|
266
|
+
|
|
267
|
+
Args:
|
|
268
|
+
rounds: Number of complete rounds (each agent speaks once per round)
|
|
269
|
+
**kwargs: Additional kwargs passed to speak()
|
|
270
|
+
|
|
271
|
+
Returns:
|
|
272
|
+
List of all responses in order
|
|
273
|
+
"""
|
|
274
|
+
responses = []
|
|
275
|
+
agent_names = list(self._agents.keys())
|
|
276
|
+
|
|
277
|
+
for _ in range(rounds):
|
|
278
|
+
for name in agent_names:
|
|
279
|
+
response = await self.speak(name, **kwargs)
|
|
280
|
+
responses.append(response)
|
|
281
|
+
|
|
282
|
+
return responses
|
|
283
|
+
|
|
284
|
+
async def speak_sequence(
|
|
285
|
+
self,
|
|
286
|
+
agent_sequence: list[str],
|
|
287
|
+
**kwargs: Any,
|
|
288
|
+
) -> list[str]:
|
|
289
|
+
"""
|
|
290
|
+
Have agents speak in a specific sequence.
|
|
291
|
+
|
|
292
|
+
Args:
|
|
293
|
+
agent_sequence: List of agent names in desired speaking order
|
|
294
|
+
**kwargs: Additional kwargs passed to speak()
|
|
295
|
+
|
|
296
|
+
Returns:
|
|
297
|
+
List of responses in order
|
|
298
|
+
"""
|
|
299
|
+
responses = []
|
|
300
|
+
for name in agent_sequence:
|
|
301
|
+
response = await self.speak(name, **kwargs)
|
|
302
|
+
responses.append(response)
|
|
303
|
+
return responses
|
|
304
|
+
|
|
305
|
+
@property
|
|
306
|
+
def transcript(self) -> list[ChatMessage]:
|
|
307
|
+
"""Get a copy of the conversation transcript."""
|
|
308
|
+
return list(self._transcript)
|
|
309
|
+
|
|
310
|
+
@property
|
|
311
|
+
def agents(self) -> dict[str, Agent]:
|
|
312
|
+
"""Get the registered agents."""
|
|
313
|
+
return dict(self._agents)
|
|
314
|
+
|
|
315
|
+
@property
|
|
316
|
+
def agent_names(self) -> list[str]:
|
|
317
|
+
"""Get list of agent names."""
|
|
318
|
+
return list(self._agents.keys())
|
|
319
|
+
|
|
320
|
+
def format_transcript(self, include_metadata: bool = False) -> str:
|
|
321
|
+
"""
|
|
322
|
+
Format the transcript as a readable string.
|
|
323
|
+
|
|
324
|
+
Args:
|
|
325
|
+
include_metadata: Whether to include metadata like tokens used
|
|
326
|
+
|
|
327
|
+
Returns:
|
|
328
|
+
Formatted transcript string
|
|
329
|
+
"""
|
|
330
|
+
lines = []
|
|
331
|
+
for msg in self._transcript:
|
|
332
|
+
lines.append(f"[{msg.agent_name}]:")
|
|
333
|
+
# Indent content for readability
|
|
334
|
+
for line in msg.content.split("\n"):
|
|
335
|
+
lines.append(f" {line}")
|
|
336
|
+
if include_metadata and msg.metadata:
|
|
337
|
+
meta_str = ", ".join(f"{k}={v}" for k, v in msg.metadata.items() if v)
|
|
338
|
+
if meta_str:
|
|
339
|
+
lines.append(f" ({meta_str})")
|
|
340
|
+
lines.append("")
|
|
341
|
+
return "\n".join(lines)
|
|
342
|
+
|
|
343
|
+
def get_messages_for(self, agent_name: str) -> list[dict[str, str]]:
|
|
344
|
+
"""
|
|
345
|
+
Get the messages list as a specific agent would see it.
|
|
346
|
+
|
|
347
|
+
Useful for debugging or custom processing.
|
|
348
|
+
|
|
349
|
+
Args:
|
|
350
|
+
agent_name: Name of agent to get perspective for
|
|
351
|
+
|
|
352
|
+
Returns:
|
|
353
|
+
Messages list from that agent's perspective
|
|
354
|
+
"""
|
|
355
|
+
if agent_name not in self._agents:
|
|
356
|
+
raise ValueError(f"Unknown agent: {agent_name}")
|
|
357
|
+
return self._build_messages_for(self._agents[agent_name])
|
|
358
|
+
|
|
359
|
+
def clear(self) -> "MultiAgentChat":
|
|
360
|
+
"""
|
|
361
|
+
Clear the conversation transcript.
|
|
362
|
+
|
|
363
|
+
Does not remove agents.
|
|
364
|
+
|
|
365
|
+
Returns:
|
|
366
|
+
Self for method chaining
|
|
367
|
+
"""
|
|
368
|
+
self._transcript = []
|
|
369
|
+
return self
|
|
370
|
+
|
|
371
|
+
def __len__(self) -> int:
|
|
372
|
+
"""Number of messages in transcript."""
|
|
373
|
+
return len(self._transcript)
|
|
374
|
+
|
|
375
|
+
def __repr__(self) -> str:
|
|
376
|
+
return (
|
|
377
|
+
f"MultiAgentChat(agents={list(self._agents.keys())}, messages={len(self._transcript)})"
|
|
378
|
+
)
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""Core completion functionality."""
|
|
2
|
+
|
|
3
|
+
from .completion import complete, complete_sync
|
|
4
|
+
from .messages import assistant_message, format_messages, system_message, user_message
|
|
5
|
+
|
|
6
|
+
__all__ = [
|
|
7
|
+
"complete",
|
|
8
|
+
"complete_sync",
|
|
9
|
+
"format_messages",
|
|
10
|
+
"user_message",
|
|
11
|
+
"system_message",
|
|
12
|
+
"assistant_message",
|
|
13
|
+
]
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"""Core completion logic using litellm."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
import litellm
|
|
7
|
+
|
|
8
|
+
from ..types import (
|
|
9
|
+
CompletionError,
|
|
10
|
+
CompletionRequest,
|
|
11
|
+
CompletionResponse,
|
|
12
|
+
Messages,
|
|
13
|
+
UsageInfo,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
async def complete(request: CompletionRequest) -> CompletionResponse:
|
|
18
|
+
"""
|
|
19
|
+
Execute a completion request using litellm.
|
|
20
|
+
|
|
21
|
+
This is the lowest-level async completion function.
|
|
22
|
+
It handles the litellm API call and response parsing.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
request: The completion request
|
|
26
|
+
|
|
27
|
+
Returns:
|
|
28
|
+
CompletionResponse with the model's response
|
|
29
|
+
|
|
30
|
+
Raises:
|
|
31
|
+
CompletionError: If the API call fails
|
|
32
|
+
"""
|
|
33
|
+
kwargs = request.to_litellm_kwargs()
|
|
34
|
+
|
|
35
|
+
try:
|
|
36
|
+
# litellm.acompletion is the async version
|
|
37
|
+
response = await litellm.acompletion(**kwargs)
|
|
38
|
+
|
|
39
|
+
# Extract content from response
|
|
40
|
+
content = ""
|
|
41
|
+
finish_reason = None
|
|
42
|
+
|
|
43
|
+
if response.choices:
|
|
44
|
+
choice = response.choices[0]
|
|
45
|
+
if choice.message and choice.message.content:
|
|
46
|
+
content = choice.message.content
|
|
47
|
+
finish_reason = getattr(choice, "finish_reason", None)
|
|
48
|
+
|
|
49
|
+
# Parse usage info
|
|
50
|
+
usage = UsageInfo.from_litellm(
|
|
51
|
+
response.usage.model_dump() if response.usage else None
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
return CompletionResponse(
|
|
55
|
+
content=content,
|
|
56
|
+
model=response.model or request.model,
|
|
57
|
+
finish_reason=finish_reason,
|
|
58
|
+
usage=usage,
|
|
59
|
+
raw_response=response,
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
except litellm.exceptions.APIConnectionError as e:
|
|
63
|
+
raise CompletionError(f"API connection error: {e}", response=e) from e
|
|
64
|
+
except litellm.exceptions.RateLimitError as e:
|
|
65
|
+
raise CompletionError(
|
|
66
|
+
f"Rate limit exceeded: {e}",
|
|
67
|
+
status_code=429,
|
|
68
|
+
response=e,
|
|
69
|
+
) from e
|
|
70
|
+
except litellm.exceptions.APIError as e:
|
|
71
|
+
raise CompletionError(
|
|
72
|
+
f"API error: {e}",
|
|
73
|
+
status_code=getattr(e, "status_code", None),
|
|
74
|
+
response=e,
|
|
75
|
+
) from e
|
|
76
|
+
except Exception as e:
|
|
77
|
+
raise CompletionError(f"Completion failed: {e}", response=e) from e
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def complete_sync(request: CompletionRequest) -> CompletionResponse:
|
|
81
|
+
"""
|
|
82
|
+
Synchronous version of complete().
|
|
83
|
+
|
|
84
|
+
Runs the async completion in an event loop.
|
|
85
|
+
"""
|
|
86
|
+
try:
|
|
87
|
+
loop = asyncio.get_running_loop()
|
|
88
|
+
except RuntimeError:
|
|
89
|
+
loop = None
|
|
90
|
+
|
|
91
|
+
if loop and loop.is_running():
|
|
92
|
+
# We're already in an async context - need to use a new thread
|
|
93
|
+
import concurrent.futures
|
|
94
|
+
|
|
95
|
+
with concurrent.futures.ThreadPoolExecutor() as executor:
|
|
96
|
+
future = executor.submit(asyncio.run, complete(request))
|
|
97
|
+
return future.result()
|
|
98
|
+
else:
|
|
99
|
+
# No running loop, we can use asyncio.run
|
|
100
|
+
return asyncio.run(complete(request))
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
async def complete_simple(
|
|
104
|
+
model: str,
|
|
105
|
+
messages: Messages,
|
|
106
|
+
**kwargs: Any,
|
|
107
|
+
) -> CompletionResponse:
|
|
108
|
+
"""
|
|
109
|
+
Simplified completion function for quick usage.
|
|
110
|
+
|
|
111
|
+
Args:
|
|
112
|
+
model: Model identifier
|
|
113
|
+
messages: List of messages
|
|
114
|
+
**kwargs: Additional parameters passed to CompletionRequest
|
|
115
|
+
|
|
116
|
+
Returns:
|
|
117
|
+
CompletionResponse
|
|
118
|
+
"""
|
|
119
|
+
# Separate known kwargs from extra kwargs
|
|
120
|
+
known_params = {
|
|
121
|
+
"temperature",
|
|
122
|
+
"max_tokens",
|
|
123
|
+
"max_completion_tokens",
|
|
124
|
+
"top_p",
|
|
125
|
+
"stop",
|
|
126
|
+
"reasoning_effort",
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
request_kwargs: dict[str, Any] = {}
|
|
130
|
+
extra_kwargs: dict[str, Any] = {}
|
|
131
|
+
|
|
132
|
+
for key, value in kwargs.items():
|
|
133
|
+
if key in known_params:
|
|
134
|
+
request_kwargs[key] = value
|
|
135
|
+
else:
|
|
136
|
+
extra_kwargs[key] = value
|
|
137
|
+
|
|
138
|
+
request = CompletionRequest(
|
|
139
|
+
model=model,
|
|
140
|
+
messages=messages,
|
|
141
|
+
extra_kwargs=extra_kwargs,
|
|
142
|
+
**request_kwargs,
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
return await complete(request)
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"""Message formatting utilities."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
from ..types import Message, Messages, Role
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def user_message(content: str, name: str | None = None) -> Message:
|
|
8
|
+
"""Create a user message."""
|
|
9
|
+
msg: Message = {"role": "user", "content": content}
|
|
10
|
+
if name:
|
|
11
|
+
msg["name"] = name
|
|
12
|
+
return msg
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def system_message(content: str) -> Message:
|
|
16
|
+
"""Create a system message."""
|
|
17
|
+
return {"role": "system", "content": content}
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def assistant_message(content: str, name: str | None = None) -> Message:
|
|
21
|
+
"""Create an assistant message."""
|
|
22
|
+
msg: Message = {"role": "assistant", "content": content}
|
|
23
|
+
if name:
|
|
24
|
+
msg["name"] = name
|
|
25
|
+
return msg
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def tool_message(content: str, tool_call_id: str) -> Message:
|
|
29
|
+
"""Create a tool result message."""
|
|
30
|
+
return {
|
|
31
|
+
"role": "tool",
|
|
32
|
+
"content": content,
|
|
33
|
+
"tool_call_id": tool_call_id,
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def format_messages(
|
|
38
|
+
messages: Messages | str | None = None,
|
|
39
|
+
system: str | None = None,
|
|
40
|
+
user: str | None = None,
|
|
41
|
+
) -> list[Message]:
|
|
42
|
+
"""
|
|
43
|
+
Flexibly format messages into a standard list format.
|
|
44
|
+
|
|
45
|
+
This allows multiple ways to specify messages:
|
|
46
|
+
- As a list of message dicts (pass through)
|
|
47
|
+
- As a single string (converted to user message)
|
|
48
|
+
- Using system/user kwargs for simple single-turn
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
messages: Existing messages list or single string
|
|
52
|
+
system: System prompt to prepend
|
|
53
|
+
user: User message to append
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
Normalized list of message dicts
|
|
57
|
+
"""
|
|
58
|
+
result: list[Message] = []
|
|
59
|
+
|
|
60
|
+
# Add system message if provided
|
|
61
|
+
if system:
|
|
62
|
+
result.append(system_message(system))
|
|
63
|
+
|
|
64
|
+
# Handle messages parameter
|
|
65
|
+
if messages is not None:
|
|
66
|
+
if isinstance(messages, str):
|
|
67
|
+
# Single string becomes user message
|
|
68
|
+
result.append(user_message(messages))
|
|
69
|
+
else:
|
|
70
|
+
# List of messages - copy to avoid mutation
|
|
71
|
+
result.extend(list(messages))
|
|
72
|
+
|
|
73
|
+
# Add user message if provided separately
|
|
74
|
+
if user:
|
|
75
|
+
result.append(user_message(user))
|
|
76
|
+
|
|
77
|
+
return result
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def extract_content(message: Message) -> str:
|
|
81
|
+
"""Extract text content from a message."""
|
|
82
|
+
content = message.get("content", "")
|
|
83
|
+
if isinstance(content, str):
|
|
84
|
+
return content
|
|
85
|
+
elif isinstance(content, list):
|
|
86
|
+
# Handle multimodal content (text parts)
|
|
87
|
+
text_parts = []
|
|
88
|
+
for part in content:
|
|
89
|
+
if isinstance(part, dict) and part.get("type") == "text":
|
|
90
|
+
text_parts.append(part.get("text", ""))
|
|
91
|
+
elif isinstance(part, str):
|
|
92
|
+
text_parts.append(part)
|
|
93
|
+
return "".join(text_parts)
|
|
94
|
+
return str(content) if content else ""
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def validate_messages(messages: Messages) -> list[str]:
|
|
98
|
+
"""
|
|
99
|
+
Validate a list of messages.
|
|
100
|
+
|
|
101
|
+
Returns list of validation errors (empty if valid).
|
|
102
|
+
"""
|
|
103
|
+
errors: list[str] = []
|
|
104
|
+
valid_roles: set[Role] = {"system", "user", "assistant", "tool"}
|
|
105
|
+
|
|
106
|
+
for i, msg in enumerate(messages):
|
|
107
|
+
if not isinstance(msg, dict):
|
|
108
|
+
errors.append(f"Message {i}: must be a dict, got {type(msg).__name__}")
|
|
109
|
+
continue
|
|
110
|
+
|
|
111
|
+
role = msg.get("role")
|
|
112
|
+
if role not in valid_roles:
|
|
113
|
+
errors.append(f"Message {i}: invalid role '{role}', must be one of {valid_roles}")
|
|
114
|
+
|
|
115
|
+
if "content" not in msg and "tool_calls" not in msg:
|
|
116
|
+
errors.append(f"Message {i}: must have 'content' or 'tool_calls'")
|
|
117
|
+
|
|
118
|
+
if role == "tool" and "tool_call_id" not in msg:
|
|
119
|
+
errors.append(f"Message {i}: tool message must have 'tool_call_id'")
|
|
120
|
+
|
|
121
|
+
return errors
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def count_messages_by_role(messages: Messages) -> dict[str, int]:
|
|
125
|
+
"""Count messages by role."""
|
|
126
|
+
counts: dict[str, int] = {}
|
|
127
|
+
for msg in messages:
|
|
128
|
+
role = msg.get("role", "unknown")
|
|
129
|
+
counts[role] = counts.get(role, 0) + 1
|
|
130
|
+
return counts
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""Middleware for request/response processing."""
|
|
2
|
+
|
|
3
|
+
from .base import Middleware, MiddlewareChain
|
|
4
|
+
from .cache import CacheConfig, CacheMiddleware
|
|
5
|
+
from .logging import CostTrackingMiddleware, LoggingMiddleware
|
|
6
|
+
from .rate_limit import RateLimitMiddleware
|
|
7
|
+
from .retry import RetryMiddleware
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
"Middleware",
|
|
11
|
+
"MiddlewareChain",
|
|
12
|
+
"RetryMiddleware",
|
|
13
|
+
"RateLimitMiddleware",
|
|
14
|
+
"CacheMiddleware",
|
|
15
|
+
"CacheConfig",
|
|
16
|
+
"LoggingMiddleware",
|
|
17
|
+
"CostTrackingMiddleware",
|
|
18
|
+
]
|