voicerun_completions 0.1.2__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.
- voicerun_completions/__init__.py +0 -0
- voicerun_completions/client.py +165 -0
- voicerun_completions/providers/anthropic/anthropic_client.py +192 -0
- voicerun_completions/providers/anthropic/streaming.py +197 -0
- voicerun_completions/providers/anthropic/utils.py +193 -0
- voicerun_completions/providers/base.py +145 -0
- voicerun_completions/providers/google/google_client.py +165 -0
- voicerun_completions/providers/google/streaming.py +177 -0
- voicerun_completions/providers/google/utils.py +142 -0
- voicerun_completions/providers/openai/openai_client.py +159 -0
- voicerun_completions/providers/openai/streaming.py +182 -0
- voicerun_completions/providers/openai/utils.py +135 -0
- voicerun_completions-0.1.2.dist-info/METADATA +46 -0
- voicerun_completions-0.1.2.dist-info/RECORD +16 -0
- voicerun_completions-0.1.2.dist-info/WHEEL +5 -0
- voicerun_completions-0.1.2.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
from typing import Optional
|
|
2
|
+
from google.genai.types import (
|
|
3
|
+
Content as GoogleMessageContent,
|
|
4
|
+
Part as GoogleMessagePart,
|
|
5
|
+
FunctionDeclaration as GoogleFunctionDefinition,
|
|
6
|
+
Tool as GoogleToolDefinition,
|
|
7
|
+
ToolConfig as GoogleToolChoice,
|
|
8
|
+
FunctionCallingConfig as GoogleFunctionChoice,
|
|
9
|
+
)
|
|
10
|
+
from primfunctions.completions.messages import (
|
|
11
|
+
ConversationHistory,
|
|
12
|
+
UserMessage,
|
|
13
|
+
AssistantMessage,
|
|
14
|
+
SystemMessage,
|
|
15
|
+
ToolResultMessage,
|
|
16
|
+
)
|
|
17
|
+
from primfunctions.completions.request import ToolChoice, ToolDefinition
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def denormalize_conversation_history(normalized_messages: ConversationHistory) -> tuple[list[GoogleMessageContent], Optional[str]]:
|
|
21
|
+
"""Convert normalized Message objects to Google Content format.
|
|
22
|
+
|
|
23
|
+
Returns:
|
|
24
|
+
Tuple of (contents, system_instruction) since Google handles system prompts separately
|
|
25
|
+
"""
|
|
26
|
+
contents: list[GoogleMessageContent] = []
|
|
27
|
+
system_messages: list[str] = []
|
|
28
|
+
|
|
29
|
+
# TODO: thought signatures required https://ai.google.dev/gemini-api/docs/thought-signatures skip_thought_signature_validator?
|
|
30
|
+
|
|
31
|
+
for msg in normalized_messages:
|
|
32
|
+
match msg:
|
|
33
|
+
case SystemMessage():
|
|
34
|
+
system_messages.append(msg.content)
|
|
35
|
+
case UserMessage():
|
|
36
|
+
parts = []
|
|
37
|
+
if msg.content:
|
|
38
|
+
parts.append(GoogleMessagePart.from_text(text=msg.content))
|
|
39
|
+
|
|
40
|
+
content = GoogleMessageContent(
|
|
41
|
+
role="user",
|
|
42
|
+
parts=parts
|
|
43
|
+
)
|
|
44
|
+
contents.append(content)
|
|
45
|
+
case AssistantMessage():
|
|
46
|
+
# Assistant messages (model role in Google)
|
|
47
|
+
parts = []
|
|
48
|
+
|
|
49
|
+
# Add text content if present
|
|
50
|
+
if msg.content:
|
|
51
|
+
parts.append(GoogleMessagePart.from_text(text=msg.content))
|
|
52
|
+
|
|
53
|
+
# Add function calls if present
|
|
54
|
+
if msg.tool_calls:
|
|
55
|
+
for tc in msg.tool_calls:
|
|
56
|
+
# function_call = GoogleFunctionCall(
|
|
57
|
+
# id=tc.id,
|
|
58
|
+
# name=tc.function.name,
|
|
59
|
+
# args=tc.function.arguments
|
|
60
|
+
# )
|
|
61
|
+
parts.append(GoogleMessagePart.from_function_call(
|
|
62
|
+
name=tc.function.name,
|
|
63
|
+
args=tc.function.arguments
|
|
64
|
+
))
|
|
65
|
+
|
|
66
|
+
content = GoogleMessageContent(
|
|
67
|
+
role="model",
|
|
68
|
+
parts=parts
|
|
69
|
+
)
|
|
70
|
+
contents.append(content)
|
|
71
|
+
case ToolResultMessage():
|
|
72
|
+
# Tool results become function_response parts in user messages
|
|
73
|
+
# Need to find the corresponding function name from tool_call_id
|
|
74
|
+
# For now, we'll need to track function names separately or get from context
|
|
75
|
+
if msg.tool_call_id and msg.content:
|
|
76
|
+
# TODO: We need the function name from the original tool call
|
|
77
|
+
# This requires tracking the function name when processing assistant messages
|
|
78
|
+
# function_response = GoogleFunctionResponse(
|
|
79
|
+
# id=msg.tool_call_id,
|
|
80
|
+
# name=msg.name or "unknown_function", # Name is required
|
|
81
|
+
# response={"output": msg.content}
|
|
82
|
+
# )
|
|
83
|
+
|
|
84
|
+
content = GoogleMessageContent(
|
|
85
|
+
role="user",
|
|
86
|
+
parts=[GoogleMessagePart.from_function_response(
|
|
87
|
+
name=msg.name or "unknown_function",
|
|
88
|
+
response={"output": msg.content}
|
|
89
|
+
)]
|
|
90
|
+
)
|
|
91
|
+
contents.append(content)
|
|
92
|
+
|
|
93
|
+
# Combine system messages with newlines
|
|
94
|
+
system_instruction: Optional[str] = "\n".join(system_messages) if system_messages else None
|
|
95
|
+
|
|
96
|
+
return contents, system_instruction
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def denormalize_tools(normalized_tools: Optional[list[ToolDefinition]]) -> Optional[list[GoogleToolDefinition]]:
|
|
100
|
+
"""Convert normalized Tool objects to Google Tool format."""
|
|
101
|
+
|
|
102
|
+
function_defs: Optional[list[GoogleFunctionDefinition]] = None
|
|
103
|
+
if normalized_tools:
|
|
104
|
+
function_defs = []
|
|
105
|
+
for tool in normalized_tools:
|
|
106
|
+
function_declaration = GoogleFunctionDefinition(
|
|
107
|
+
name=tool.function.name,
|
|
108
|
+
description=tool.function.description,
|
|
109
|
+
parameters=tool.function.parameters
|
|
110
|
+
)
|
|
111
|
+
function_defs.append(function_declaration)
|
|
112
|
+
|
|
113
|
+
# All google function calling tools are included in a single Google Tool
|
|
114
|
+
return [GoogleToolDefinition(function_declarations=function_defs)] if function_defs else None
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def denormalize_tool_choice(normalized_tool_choice: Optional[ToolChoice]) -> Optional[GoogleToolChoice]:
|
|
118
|
+
"""Convert normalized ToolChoice to Google GoogleToolChoice format."""
|
|
119
|
+
if not normalized_tool_choice:
|
|
120
|
+
return None
|
|
121
|
+
|
|
122
|
+
# Google uses ToolConfig with FunctionCallingConfig
|
|
123
|
+
if normalized_tool_choice == "auto":
|
|
124
|
+
return GoogleToolChoice(
|
|
125
|
+
function_calling_config=GoogleFunctionChoice(mode="AUTO")
|
|
126
|
+
)
|
|
127
|
+
elif normalized_tool_choice == "none":
|
|
128
|
+
return GoogleToolChoice(
|
|
129
|
+
function_calling_config=GoogleFunctionChoice(mode="NONE")
|
|
130
|
+
)
|
|
131
|
+
elif normalized_tool_choice == "required":
|
|
132
|
+
return GoogleToolChoice(
|
|
133
|
+
function_calling_config=GoogleFunctionChoice(mode="ANY")
|
|
134
|
+
)
|
|
135
|
+
else:
|
|
136
|
+
# Specific tool name - use allowed_function_names
|
|
137
|
+
return GoogleToolChoice(
|
|
138
|
+
function_calling_config=GoogleFunctionChoice(
|
|
139
|
+
mode="ANY",
|
|
140
|
+
allowed_function_names=[normalized_tool_choice]
|
|
141
|
+
)
|
|
142
|
+
)
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
from typing import Any, Optional, AsyncIterable
|
|
2
|
+
from openai import AsyncOpenAI
|
|
3
|
+
from openai.types.chat import (
|
|
4
|
+
ChatCompletion,
|
|
5
|
+
ChatCompletionMessageParam,
|
|
6
|
+
ChatCompletionFunctionToolParam,
|
|
7
|
+
ChatCompletionToolChoiceOptionParam,
|
|
8
|
+
)
|
|
9
|
+
from openai.types.chat.chat_completion import (
|
|
10
|
+
Choice as OpenAiChoice,
|
|
11
|
+
ChatCompletionMessage as OpenAiChatCompletionMessage,
|
|
12
|
+
)
|
|
13
|
+
from openai.types.chat.chat_completion_chunk import ChatCompletionChunk as OpenAiCompletionChunk
|
|
14
|
+
from primfunctions.completions.messages import AssistantMessage
|
|
15
|
+
from primfunctions.completions.response import ChatCompletionResponse
|
|
16
|
+
from primfunctions.completions.request import ChatCompletionRequest, StreamOptions
|
|
17
|
+
|
|
18
|
+
from .streaming import OpenAiStreamProcessor
|
|
19
|
+
from ..base import CompletionClient
|
|
20
|
+
from .utils import (
|
|
21
|
+
denormalize_conversation_history,
|
|
22
|
+
denormalize_tools,
|
|
23
|
+
denormalize_tool_choice,
|
|
24
|
+
normalize_tool_calls,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class OpenAiCompletionClient(CompletionClient):
|
|
29
|
+
|
|
30
|
+
def _denormalize_request(
|
|
31
|
+
self,
|
|
32
|
+
request: ChatCompletionRequest,
|
|
33
|
+
) -> dict[str, Any]:
|
|
34
|
+
"""Convert ChatCompletionRequest to kwargs for _get_completion."""
|
|
35
|
+
|
|
36
|
+
kwargs = {
|
|
37
|
+
"api_key": request.api_key,
|
|
38
|
+
"model": request.model,
|
|
39
|
+
"messages": denormalize_conversation_history(request.messages),
|
|
40
|
+
"tools": denormalize_tools(request.tools),
|
|
41
|
+
"tool_choice": denormalize_tool_choice(request.tool_choice),
|
|
42
|
+
"temperature": request.temperature if request.temperature else None,
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if request.max_tokens is not None:
|
|
46
|
+
kwargs["max_tokens"] = request.max_tokens
|
|
47
|
+
|
|
48
|
+
if request.timeout is not None:
|
|
49
|
+
kwargs["timeout"] = request.timeout
|
|
50
|
+
|
|
51
|
+
return kwargs
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _normalize_response(
|
|
55
|
+
self,
|
|
56
|
+
response: ChatCompletion,
|
|
57
|
+
) -> ChatCompletionResponse:
|
|
58
|
+
"""Convert OpenAI ChatCompletion to normalized ChatCompletionResponse."""
|
|
59
|
+
# Take only first choice
|
|
60
|
+
choice: OpenAiChoice = response.choices[0]
|
|
61
|
+
openai_message: OpenAiChatCompletionMessage = choice.message
|
|
62
|
+
|
|
63
|
+
normalized_message = AssistantMessage(
|
|
64
|
+
content=openai_message.content,
|
|
65
|
+
tool_calls=normalize_tool_calls(openai_message.tool_calls)
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
return ChatCompletionResponse(
|
|
69
|
+
message=normalized_message,
|
|
70
|
+
finish_reason=choice.finish_reason,
|
|
71
|
+
usage=response.usage.model_dump() if response.usage else None
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
async def _get_completion(
|
|
76
|
+
self,
|
|
77
|
+
api_key: str,
|
|
78
|
+
model: str,
|
|
79
|
+
messages: list[ChatCompletionMessageParam],
|
|
80
|
+
tools: Optional[list[ChatCompletionFunctionToolParam]] = None,
|
|
81
|
+
tool_choice: Optional[ChatCompletionToolChoiceOptionParam] = None,
|
|
82
|
+
temperature: Optional[float] = None,
|
|
83
|
+
max_tokens: Optional[int] = None,
|
|
84
|
+
timeout: Optional[float] = None,
|
|
85
|
+
) -> ChatCompletion:
|
|
86
|
+
"""TODO.
|
|
87
|
+
|
|
88
|
+
[Client](https://github.com/openai/openai-python)
|
|
89
|
+
"""
|
|
90
|
+
async with AsyncOpenAI(api_key=api_key) as client:
|
|
91
|
+
# Build kwargs dict with only non-None values
|
|
92
|
+
kwargs = {
|
|
93
|
+
"model": model,
|
|
94
|
+
"messages": messages,
|
|
95
|
+
"stream": False,
|
|
96
|
+
# TODO: support reasoning by model
|
|
97
|
+
# "reasoning_effort": "none", # Disable reasoning
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
# Only add optional parameters if they're provided
|
|
101
|
+
if tools is not None:
|
|
102
|
+
kwargs["tools"] = tools
|
|
103
|
+
if tool_choice is not None:
|
|
104
|
+
kwargs["tool_choice"] = tool_choice
|
|
105
|
+
if temperature is not None:
|
|
106
|
+
kwargs["temperature"] = temperature
|
|
107
|
+
if max_tokens is not None:
|
|
108
|
+
kwargs["max_tokens"] = max_tokens
|
|
109
|
+
if timeout is not None:
|
|
110
|
+
kwargs["timeout"] = timeout
|
|
111
|
+
|
|
112
|
+
return await client.chat.completions.create(**kwargs)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _get_stream_processor(
|
|
116
|
+
self,
|
|
117
|
+
stream_options: Optional[StreamOptions] = None,
|
|
118
|
+
) -> OpenAiStreamProcessor:
|
|
119
|
+
"""Get openai-specific StreamProcessor."""
|
|
120
|
+
return OpenAiStreamProcessor(stream_options=stream_options)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
async def _get_completion_stream(
|
|
124
|
+
self,
|
|
125
|
+
api_key: str,
|
|
126
|
+
model: str,
|
|
127
|
+
messages: list[ChatCompletionMessageParam],
|
|
128
|
+
tools: Optional[list[ChatCompletionFunctionToolParam]] = None,
|
|
129
|
+
tool_choice: Optional[ChatCompletionToolChoiceOptionParam] = None,
|
|
130
|
+
temperature: Optional[float] = None,
|
|
131
|
+
max_tokens: Optional[int] = None,
|
|
132
|
+
timeout: Optional[float] = None,
|
|
133
|
+
) -> AsyncIterable[OpenAiCompletionChunk]:
|
|
134
|
+
"""Stream chat completion chunks from OpenAI."""
|
|
135
|
+
client = AsyncOpenAI(api_key=api_key)
|
|
136
|
+
|
|
137
|
+
# Build kwargs dict with only non-None values
|
|
138
|
+
kwargs = {
|
|
139
|
+
"model": model,
|
|
140
|
+
"messages": messages,
|
|
141
|
+
"stream": True, # Enable streaming
|
|
142
|
+
"stream_options": {"include_usage": True}, # Include usage information in stream
|
|
143
|
+
# TODO: support reasoning by model
|
|
144
|
+
# "reasoning_effort": "none", # Disable reasoning
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
# Only add optional parameters if they're provided
|
|
148
|
+
if tools is not None:
|
|
149
|
+
kwargs["tools"] = tools
|
|
150
|
+
if tool_choice is not None:
|
|
151
|
+
kwargs["tool_choice"] = tool_choice
|
|
152
|
+
if temperature is not None:
|
|
153
|
+
kwargs["temperature"] = temperature
|
|
154
|
+
if max_tokens is not None:
|
|
155
|
+
kwargs["max_tokens"] = max_tokens
|
|
156
|
+
if timeout is not None:
|
|
157
|
+
kwargs["timeout"] = timeout
|
|
158
|
+
|
|
159
|
+
return await client.chat.completions.create(**kwargs)
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
from typing import Any, AsyncIterable, Dict, List, Optional
|
|
2
|
+
from openai.types.chat.chat_completion_chunk import (
|
|
3
|
+
Choice as OpenAiChunkChoice,
|
|
4
|
+
ChoiceDelta as OpenAiChoiceDelta,
|
|
5
|
+
ChatCompletionChunk as OpenAiCompletionChunk,
|
|
6
|
+
ChoiceDeltaToolCall as OpenAiToolDelta,
|
|
7
|
+
)
|
|
8
|
+
from primfunctions.completions.messages import ToolCall, AssistantMessage
|
|
9
|
+
from primfunctions.completions.response import ChatCompletionResponse
|
|
10
|
+
from primfunctions.completions.streaming import (
|
|
11
|
+
ChatCompletionChunk,
|
|
12
|
+
AssistantMessageDeltaChunk,
|
|
13
|
+
AssistantMessageSentenceChunk,
|
|
14
|
+
FinishReasonChunk,
|
|
15
|
+
ToolCallChunk,
|
|
16
|
+
UsageChunk,
|
|
17
|
+
FinalResponseChunk,
|
|
18
|
+
)
|
|
19
|
+
from primfunctions.utils.streaming import update_sentence_buffer, clean_text_for_speech
|
|
20
|
+
from primfunctions.completions.request import StreamOptions
|
|
21
|
+
|
|
22
|
+
from ..base import StreamProcessor, PartialToolCall
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class OpenAiStreamProcessor(StreamProcessor):
|
|
26
|
+
"""Processes OpenAI chat completion stream yielding normalized chunks."""
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def __init__(
|
|
30
|
+
self,
|
|
31
|
+
stream_options: Optional[StreamOptions] = None,
|
|
32
|
+
):
|
|
33
|
+
self.stream_sentences: bool = False
|
|
34
|
+
self.clean_sentences: bool = True
|
|
35
|
+
self.min_sentence_length: int = 6
|
|
36
|
+
self.punctuation_marks: Optional[list[str]] = None
|
|
37
|
+
self.punctuation_language: Optional[str] = None
|
|
38
|
+
|
|
39
|
+
# Override stream options defaults
|
|
40
|
+
if stream_options:
|
|
41
|
+
self.stream_sentences = stream_options.stream_sentences
|
|
42
|
+
self.clean_sentences = stream_options.clean_sentences
|
|
43
|
+
self.min_sentence_length = stream_options.min_sentence_length
|
|
44
|
+
self.punctuation_marks = stream_options.punctuation_marks
|
|
45
|
+
self.punctuation_language = stream_options.punctuation_language
|
|
46
|
+
|
|
47
|
+
self.active_calls: Dict[int, PartialToolCall] = {}
|
|
48
|
+
self.content: str = ""
|
|
49
|
+
self.tool_calls: List[ToolCall] = []
|
|
50
|
+
self.finish_reason: str = ""
|
|
51
|
+
self.usage: Dict[str, Any] = {}
|
|
52
|
+
self.sentence_buffer = ""
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _process_tool_deltas(self, tool_call_deltas: List[OpenAiToolDelta]) -> List[ToolCallChunk]:
|
|
56
|
+
"""Process a tool call delta and return any completed tool call chunks."""
|
|
57
|
+
chunks: List[ToolCall] = []
|
|
58
|
+
|
|
59
|
+
for delta in tool_call_deltas:
|
|
60
|
+
# Tool call already in buffer
|
|
61
|
+
if delta.index in self.active_calls:
|
|
62
|
+
partial_call = self.active_calls[delta.index]
|
|
63
|
+
if delta.function and delta.function.arguments:
|
|
64
|
+
partial_call.arguments_buffer += delta.function.arguments
|
|
65
|
+
|
|
66
|
+
# Check if tool call complete
|
|
67
|
+
if partial_call.is_complete():
|
|
68
|
+
# Complete tool call, remove from active calls and return
|
|
69
|
+
completed_call = partial_call.to_tool_call()
|
|
70
|
+
del self.active_calls[delta.index]
|
|
71
|
+
|
|
72
|
+
# Append full tool call to chunks to stream
|
|
73
|
+
chunks.append(ToolCallChunk(
|
|
74
|
+
tool_call=completed_call
|
|
75
|
+
))
|
|
76
|
+
|
|
77
|
+
# Add tool call to accumulated response
|
|
78
|
+
self.tool_calls.append(completed_call)
|
|
79
|
+
|
|
80
|
+
# New tool call
|
|
81
|
+
else:
|
|
82
|
+
self.active_calls[delta.index] = PartialToolCall(
|
|
83
|
+
id=delta.id,
|
|
84
|
+
type=delta.type,
|
|
85
|
+
function_name=delta.function.name,
|
|
86
|
+
arguments_buffer=delta.function.arguments or "",
|
|
87
|
+
index=delta.index,
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
return chunks
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _process_chunk(
|
|
94
|
+
self,
|
|
95
|
+
chunk: OpenAiCompletionChunk,
|
|
96
|
+
) -> List[ChatCompletionChunk]:
|
|
97
|
+
"""Convert OpenAI ChatCompletionChunk to individual typed chunks."""
|
|
98
|
+
chunks = []
|
|
99
|
+
|
|
100
|
+
# Handle usage chunk
|
|
101
|
+
if chunk.usage:
|
|
102
|
+
self.usage = chunk.usage.model_dump()
|
|
103
|
+
|
|
104
|
+
# Handle no choices
|
|
105
|
+
if not chunk.choices:
|
|
106
|
+
return chunks
|
|
107
|
+
|
|
108
|
+
# Take only first choice (OpenAI pattern)
|
|
109
|
+
choice: OpenAiChunkChoice = chunk.choices[0]
|
|
110
|
+
delta: OpenAiChoiceDelta = choice.delta
|
|
111
|
+
|
|
112
|
+
# Handle content
|
|
113
|
+
if delta.content:
|
|
114
|
+
if self.stream_sentences:
|
|
115
|
+
# Append delta to sentence buffer
|
|
116
|
+
sentence_buffer, complete_sentence = update_sentence_buffer(
|
|
117
|
+
content=delta.content,
|
|
118
|
+
sentence_buffer=self.sentence_buffer,
|
|
119
|
+
punctuation_marks=self.punctuation_marks,
|
|
120
|
+
clean_text=self.clean_sentences,
|
|
121
|
+
# TODO: this completely drops messages like "Sure."
|
|
122
|
+
min_sentence_length=self.min_sentence_length,
|
|
123
|
+
)
|
|
124
|
+
self.sentence_buffer = sentence_buffer
|
|
125
|
+
|
|
126
|
+
if complete_sentence:
|
|
127
|
+
chunks.append(AssistantMessageSentenceChunk(
|
|
128
|
+
sentence=complete_sentence
|
|
129
|
+
))
|
|
130
|
+
|
|
131
|
+
else:
|
|
132
|
+
# Otherwise stream content delta directly
|
|
133
|
+
chunks.append(AssistantMessageDeltaChunk(
|
|
134
|
+
content=delta.content
|
|
135
|
+
))
|
|
136
|
+
|
|
137
|
+
# Add content delta to accumulated response
|
|
138
|
+
self.content += delta.content
|
|
139
|
+
|
|
140
|
+
# Handle tool calls
|
|
141
|
+
if delta.tool_calls:
|
|
142
|
+
chunks.extend(self._process_tool_deltas(delta.tool_calls))
|
|
143
|
+
|
|
144
|
+
# Capture finish reason
|
|
145
|
+
if choice.finish_reason:
|
|
146
|
+
self.finish_reason = choice.finish_reason
|
|
147
|
+
|
|
148
|
+
return chunks
|
|
149
|
+
|
|
150
|
+
async def process_stream(
|
|
151
|
+
self,
|
|
152
|
+
stream: AsyncIterable[OpenAiCompletionChunk],
|
|
153
|
+
) -> AsyncIterable[ChatCompletionChunk]:
|
|
154
|
+
async for chunk in stream:
|
|
155
|
+
for normalized_chunk in self._process_chunk(chunk):
|
|
156
|
+
yield normalized_chunk
|
|
157
|
+
|
|
158
|
+
if self.stream_sentences and self.sentence_buffer:
|
|
159
|
+
# Clean text for speech if requested
|
|
160
|
+
complete_sentence = clean_text_for_speech(self.sentence_buffer) if self.clean_sentences else self.sentence_buffer
|
|
161
|
+
# Handle any remaining text in sentence buffer
|
|
162
|
+
yield AssistantMessageSentenceChunk(
|
|
163
|
+
sentence=complete_sentence
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
# Yield the finish chunk (or default)
|
|
167
|
+
yield FinishReasonChunk(finish_reason=self.finish_reason or "stop")
|
|
168
|
+
|
|
169
|
+
# Yield the usage chunk
|
|
170
|
+
yield UsageChunk(usage=self.usage)
|
|
171
|
+
|
|
172
|
+
# Yield aggregated chat completion response as final chunk
|
|
173
|
+
yield FinalResponseChunk(
|
|
174
|
+
response = ChatCompletionResponse(
|
|
175
|
+
message=AssistantMessage(
|
|
176
|
+
content=self.content or None,
|
|
177
|
+
tool_calls=self.tool_calls or None
|
|
178
|
+
),
|
|
179
|
+
usage=self.usage,
|
|
180
|
+
finish_reason=self.finish_reason,
|
|
181
|
+
)
|
|
182
|
+
)
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from typing import List, Optional
|
|
3
|
+
from openai.types.chat import (
|
|
4
|
+
ChatCompletionToolChoiceOptionParam as OpenAiToolChoice,
|
|
5
|
+
ChatCompletionNamedToolChoiceParam as OpenAiNamedToolChoice,
|
|
6
|
+
ChatCompletionFunctionToolParam as OpenAiToolDefinition,
|
|
7
|
+
ChatCompletionMessageParam as OpenAiMessage,
|
|
8
|
+
ChatCompletionUserMessageParam as OpenAiUserMessage,
|
|
9
|
+
ChatCompletionAssistantMessageParam as OpenAiAssistantMessage,
|
|
10
|
+
ChatCompletionSystemMessageParam as OpenAiSystemMessage,
|
|
11
|
+
ChatCompletionToolMessageParam as OpenAiToolResultMessage,
|
|
12
|
+
ChatCompletionMessageFunctionToolCallParam as OpenAiToolCall,
|
|
13
|
+
)
|
|
14
|
+
from openai.types.shared_params import FunctionDefinition as OpenAiFunctionDefinition
|
|
15
|
+
from openai.types.chat.chat_completion_message_function_tool_call_param import Function as OpenAiFunctionCall
|
|
16
|
+
from primfunctions.completions.messages import (
|
|
17
|
+
ConversationHistory,
|
|
18
|
+
UserMessage,
|
|
19
|
+
AssistantMessage,
|
|
20
|
+
SystemMessage,
|
|
21
|
+
ToolResultMessage,
|
|
22
|
+
ToolCall,
|
|
23
|
+
FunctionCall,
|
|
24
|
+
)
|
|
25
|
+
from primfunctions.completions.request import ToolChoice, ToolDefinition
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def denormalize_tool_calls(normalized_tool_calls: Optional[List[ToolCall]]) -> list[OpenAiToolCall]:
|
|
29
|
+
"""TODO"""
|
|
30
|
+
tool_calls: Optional[list[OpenAiToolCall]] = None
|
|
31
|
+
if normalized_tool_calls:
|
|
32
|
+
tool_calls = []
|
|
33
|
+
for tc in normalized_tool_calls:
|
|
34
|
+
function_call: OpenAiFunctionCall = {
|
|
35
|
+
"name": tc.function.name,
|
|
36
|
+
"arguments": json.dumps(tc.function.arguments),
|
|
37
|
+
}
|
|
38
|
+
tool_call: OpenAiToolCall = {
|
|
39
|
+
"id": tc.id,
|
|
40
|
+
"type": tc.type,
|
|
41
|
+
"function": function_call
|
|
42
|
+
}
|
|
43
|
+
tool_calls.append(tool_call)
|
|
44
|
+
|
|
45
|
+
return tool_calls
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def denormalize_conversation_history(normalized_messages: ConversationHistory) -> list[OpenAiMessage]:
|
|
49
|
+
# Convert conversation history to ChatCompletionMessageParams
|
|
50
|
+
messages: list[OpenAiMessage] = []
|
|
51
|
+
for msg in normalized_messages:
|
|
52
|
+
match msg:
|
|
53
|
+
case UserMessage():
|
|
54
|
+
messages.append(OpenAiUserMessage(
|
|
55
|
+
role="user",
|
|
56
|
+
content=msg.content,
|
|
57
|
+
))
|
|
58
|
+
case AssistantMessage():
|
|
59
|
+
messages.append(OpenAiAssistantMessage(
|
|
60
|
+
role="assistant",
|
|
61
|
+
content=msg.content,
|
|
62
|
+
tool_calls=denormalize_tool_calls(msg.tool_calls),
|
|
63
|
+
))
|
|
64
|
+
case SystemMessage():
|
|
65
|
+
messages.append(OpenAiSystemMessage(
|
|
66
|
+
role="system",
|
|
67
|
+
content=msg.content,
|
|
68
|
+
))
|
|
69
|
+
case ToolResultMessage():
|
|
70
|
+
tool_result: OpenAiToolResultMessage = {
|
|
71
|
+
"role": "tool",
|
|
72
|
+
"content": json.dumps(msg.content),
|
|
73
|
+
"tool_call_id": msg.tool_call_id,
|
|
74
|
+
}
|
|
75
|
+
messages.append(tool_result)
|
|
76
|
+
|
|
77
|
+
return messages
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def denormalize_tools(normalized_tools: Optional[list[ToolDefinition]]) -> Optional[list[OpenAiToolDefinition]]:
|
|
81
|
+
# Convert tools to ChatCompletionFunctionToolParam format if provided
|
|
82
|
+
tools: Optional[list[OpenAiToolDefinition]] = None
|
|
83
|
+
if normalized_tools:
|
|
84
|
+
tools = []
|
|
85
|
+
for tool in normalized_tools:
|
|
86
|
+
function_def: OpenAiFunctionDefinition = {
|
|
87
|
+
"name": tool.function.name,
|
|
88
|
+
"description": tool.function.description,
|
|
89
|
+
"parameters": tool.function.parameters,
|
|
90
|
+
"strict": tool.function.strict,
|
|
91
|
+
}
|
|
92
|
+
tool_def: OpenAiToolDefinition = {
|
|
93
|
+
"type": tool.type,
|
|
94
|
+
"function": function_def,
|
|
95
|
+
}
|
|
96
|
+
tools.append(tool_def)
|
|
97
|
+
|
|
98
|
+
return tools
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def denormalize_tool_choice(normalized_tool_choice: Optional[ToolChoice]) -> Optional[OpenAiToolChoice]:
|
|
102
|
+
"""Convert normalized ToolChoice to OpenAI ChatCompletionToolChoiceOptionParam format."""
|
|
103
|
+
tool_choice: Optional[OpenAiToolChoice] = None
|
|
104
|
+
if normalized_tool_choice:
|
|
105
|
+
# Handle literal values that are directly compatible
|
|
106
|
+
if normalized_tool_choice in ["none", "auto", "required"]:
|
|
107
|
+
tool_choice = normalized_tool_choice
|
|
108
|
+
else:
|
|
109
|
+
# TODO: support ChatCompletionNamedToolChoiceCustomParam
|
|
110
|
+
# Assume tool name
|
|
111
|
+
tool_choice = OpenAiNamedToolChoice(
|
|
112
|
+
type="function",
|
|
113
|
+
function={"name": normalized_tool_choice}
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
return tool_choice
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def normalize_tool_calls(denormalized_tool_calls: Optional[List[OpenAiToolCall]]) -> list[ToolCall]:
|
|
120
|
+
"""Convert denormalized ChatCompletionMessageFunctionToolCallParam to ToolCall format."""
|
|
121
|
+
tool_calls: Optional[list[ToolCall]] = None
|
|
122
|
+
if denormalized_tool_calls:
|
|
123
|
+
tool_calls = []
|
|
124
|
+
for index, tc in enumerate(denormalized_tool_calls):
|
|
125
|
+
tool_calls.append(ToolCall(
|
|
126
|
+
id=tc.id,
|
|
127
|
+
type=tc.type,
|
|
128
|
+
function=FunctionCall(
|
|
129
|
+
name=tc.function.name,
|
|
130
|
+
arguments=json.loads(tc.function.arguments)
|
|
131
|
+
),
|
|
132
|
+
index=index,
|
|
133
|
+
))
|
|
134
|
+
|
|
135
|
+
return tool_calls
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: voicerun_completions
|
|
3
|
+
Version: 0.1.2
|
|
4
|
+
Summary: VoiceRun Completions
|
|
5
|
+
Author: VoiceRun
|
|
6
|
+
Requires-Python: >=3.10
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
Requires-Dist: primfunctions
|
|
9
|
+
Requires-Dist: black>=23.3.0
|
|
10
|
+
Requires-Dist: openai==2.8.0
|
|
11
|
+
Requires-Dist: anthropic==0.74.0
|
|
12
|
+
Requires-Dist: google-genai==1.52.0
|
|
13
|
+
|
|
14
|
+
# voicerun_completions
|
|
15
|
+
|
|
16
|
+
A comprehensive SDK for using with the VoiceRun Completions functionality.
|
|
17
|
+
|
|
18
|
+
## Table of Contents
|
|
19
|
+
- [Prerequisites](#prerequisites)
|
|
20
|
+
- [Installation](#installation)
|
|
21
|
+
|
|
22
|
+
## Prerequisites
|
|
23
|
+
|
|
24
|
+
- Python 3.12+
|
|
25
|
+
- [uv](https://docs.astral.sh/uv/)
|
|
26
|
+
|
|
27
|
+
If you do not have a uv virtual environment, you can create one with:
|
|
28
|
+
```bash
|
|
29
|
+
uv venv --python 3.12
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
**Note:** We recommend using uv to install the package due to the incredible speed of the package manager. The package can still be installed via pip directly, but it will be slower.
|
|
33
|
+
|
|
34
|
+
## Installation
|
|
35
|
+
|
|
36
|
+
### From PyPI
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
pip install voicerun_completions
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### From Source
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
uv pip install .
|
|
46
|
+
```
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
voicerun_completions/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
voicerun_completions/client.py,sha256=SyYXcByOiqFkhVn9HkMgNJNjsH3ERXiy3uI1fylA5jw,5827
|
|
3
|
+
voicerun_completions/providers/base.py,sha256=XCkWBXmvsXcDcMS6akksPNhPkKQXwpxNqJYkteT9RnM,4080
|
|
4
|
+
voicerun_completions/providers/anthropic/anthropic_client.py,sha256=bBb3_bAO1qSDo_onVL3M7H1ak97We6Y0-24ImAzxMZ0,6659
|
|
5
|
+
voicerun_completions/providers/anthropic/streaming.py,sha256=qGYOAqgmqeKzoj4vNw7-2TCFQU6IsmFpdUVWlx4xznw,7754
|
|
6
|
+
voicerun_completions/providers/anthropic/utils.py,sha256=Xwk7nv6N4LXseqU6r0Qh6k5Fna7kZY4QfKm7cTCq93U,7467
|
|
7
|
+
voicerun_completions/providers/google/google_client.py,sha256=XljdWabr-K8THUDDhC4YQ4HII6ElZJNrUeyRSCPJ8gw,5641
|
|
8
|
+
voicerun_completions/providers/google/streaming.py,sha256=VLcMKvDRVmGd8G95xJfw_rwFvElssFINoai-v26a7w0,6630
|
|
9
|
+
voicerun_completions/providers/google/utils.py,sha256=jfumtF2H-wp6pUDQpidOyqWcG68IFuyebb-Wr6BWYuo,5787
|
|
10
|
+
voicerun_completions/providers/openai/openai_client.py,sha256=uu3jdNl9WcLDkABhI68FI9xd_igF8CyLT91JLuHPzu4,5659
|
|
11
|
+
voicerun_completions/providers/openai/streaming.py,sha256=TwxGYxoyplmTrA5U5Pg3GRhm9d8Q1CApmDbo5g3v_aY,6824
|
|
12
|
+
voicerun_completions/providers/openai/utils.py,sha256=VU3WYXF-bhanbQQnzzKrkH3jslvEhZYa3RTJCJCNwG4,5193
|
|
13
|
+
voicerun_completions-0.1.2.dist-info/METADATA,sha256=7-evbAWVPbRBJz5BRCO1_qK_WvWQR7ZR4x7451hVWGI,1002
|
|
14
|
+
voicerun_completions-0.1.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
15
|
+
voicerun_completions-0.1.2.dist-info/top_level.txt,sha256=R9R2ek4izV-Jznppy4FVlmBAZs6VEQo-PeqpkaI4xzg,21
|
|
16
|
+
voicerun_completions-0.1.2.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
voicerun_completions
|