my-llmkit 0.3.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.
- my_llmkit/__init__.py +7 -0
- my_llmkit/chat/__init__.py +93 -0
- my_llmkit/chat/approval.py +64 -0
- my_llmkit/chat/base.py +193 -0
- my_llmkit/chat/claude.py +492 -0
- my_llmkit/chat/events.py +105 -0
- my_llmkit/chat/model_settings.py +226 -0
- my_llmkit/chat/openai_compatible.py +600 -0
- my_llmkit/chat/processor.py +205 -0
- my_llmkit/chat/runner.py +161 -0
- my_llmkit/chat/tools.py +463 -0
- my_llmkit/chat/types.py +562 -0
- my_llmkit/image.py +1138 -0
- my_llmkit/image_cli.py +167 -0
- my_llmkit/log.py +9 -0
- my_llmkit/mcp/__init__.py +30 -0
- my_llmkit/mcp/mcp_client.py +452 -0
- my_llmkit/mcp/mcp_config.py +829 -0
- my_llmkit/models/__init__.py +70 -0
- my_llmkit/models/capabilities.py +113 -0
- my_llmkit/models/info.py +429 -0
- my_llmkit-0.3.0.dist-info/METADATA +684 -0
- my_llmkit-0.3.0.dist-info/RECORD +26 -0
- my_llmkit-0.3.0.dist-info/WHEEL +5 -0
- my_llmkit-0.3.0.dist-info/entry_points.txt +2 -0
- my_llmkit-0.3.0.dist-info/top_level.txt +1 -0
my_llmkit/__init__.py
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# coding=utf-8
|
|
2
|
+
#
|
|
3
|
+
# chat 包对外门面,保持 my_llmkit.chat 导入路径不变
|
|
4
|
+
#
|
|
5
|
+
from dotenv import load_dotenv
|
|
6
|
+
|
|
7
|
+
load_dotenv()
|
|
8
|
+
|
|
9
|
+
from .types import (
|
|
10
|
+
UnifiedToolCall,
|
|
11
|
+
UnifiedUsage,
|
|
12
|
+
UnifiedChunk,
|
|
13
|
+
UnifiedResponse,
|
|
14
|
+
ContentBlock,
|
|
15
|
+
TextContent,
|
|
16
|
+
ImageContent,
|
|
17
|
+
DocumentContent,
|
|
18
|
+
UnifiedToolResult,
|
|
19
|
+
UnifiedMessage,
|
|
20
|
+
UnifiedToolParam,
|
|
21
|
+
)
|
|
22
|
+
from .events import (
|
|
23
|
+
Usage,
|
|
24
|
+
ChatCompletionStreamContentEvent,
|
|
25
|
+
ChatCompletionStreamReasoningContentEvent,
|
|
26
|
+
ChatCompletionStreamToolApprovalRequestEvent,
|
|
27
|
+
ChatCompletionStreamToolApprovalResultEvent,
|
|
28
|
+
ChatCompletionStreamToolCallStartEvent,
|
|
29
|
+
ChatCompletionStreamToolCallResultEvent,
|
|
30
|
+
ChatCompletionStreamUsageEvent,
|
|
31
|
+
)
|
|
32
|
+
from .approval import (
|
|
33
|
+
DenyToolApprovalHandler,
|
|
34
|
+
ToolApprovalDecision,
|
|
35
|
+
ToolApprovalHandler,
|
|
36
|
+
ToolApprovalRequest,
|
|
37
|
+
)
|
|
38
|
+
from .tools import (
|
|
39
|
+
ToolFunction,
|
|
40
|
+
ToolFunctions,
|
|
41
|
+
ToolExecutor,
|
|
42
|
+
make_mcp_tool_name,
|
|
43
|
+
set_tool_approval,
|
|
44
|
+
)
|
|
45
|
+
from .processor import ChatCompletionStreamProcessor
|
|
46
|
+
from .runner import (
|
|
47
|
+
ChatCompletionResult,
|
|
48
|
+
ChatCompletionStreamRunner,
|
|
49
|
+
MaxRoundsExceededError,
|
|
50
|
+
MessagePreparer,
|
|
51
|
+
)
|
|
52
|
+
from .base import LLMChatCompletion
|
|
53
|
+
from .openai_compatible import OpenAICompatibleChatCompletion
|
|
54
|
+
from .claude import ClaudeChatCompletion
|
|
55
|
+
|
|
56
|
+
__all__ = [
|
|
57
|
+
"UnifiedToolCall",
|
|
58
|
+
"UnifiedUsage",
|
|
59
|
+
"UnifiedChunk",
|
|
60
|
+
"UnifiedResponse",
|
|
61
|
+
"ContentBlock",
|
|
62
|
+
"TextContent",
|
|
63
|
+
"ImageContent",
|
|
64
|
+
"DocumentContent",
|
|
65
|
+
"UnifiedToolResult",
|
|
66
|
+
"UnifiedMessage",
|
|
67
|
+
"UnifiedToolParam",
|
|
68
|
+
"Usage",
|
|
69
|
+
"ChatCompletionStreamContentEvent",
|
|
70
|
+
"ChatCompletionStreamReasoningContentEvent",
|
|
71
|
+
"ChatCompletionStreamToolApprovalRequestEvent",
|
|
72
|
+
"ChatCompletionStreamToolApprovalResultEvent",
|
|
73
|
+
"ChatCompletionStreamToolCallStartEvent",
|
|
74
|
+
"ChatCompletionStreamToolCallResultEvent",
|
|
75
|
+
"ChatCompletionStreamUsageEvent",
|
|
76
|
+
"DenyToolApprovalHandler",
|
|
77
|
+
"ToolApprovalDecision",
|
|
78
|
+
"ToolApprovalHandler",
|
|
79
|
+
"ToolApprovalRequest",
|
|
80
|
+
"ToolFunction",
|
|
81
|
+
"ToolFunctions",
|
|
82
|
+
"ToolExecutor",
|
|
83
|
+
"make_mcp_tool_name",
|
|
84
|
+
"set_tool_approval",
|
|
85
|
+
"ChatCompletionStreamProcessor",
|
|
86
|
+
"ChatCompletionResult",
|
|
87
|
+
"ChatCompletionStreamRunner",
|
|
88
|
+
"MaxRoundsExceededError",
|
|
89
|
+
"MessagePreparer",
|
|
90
|
+
"LLMChatCompletion",
|
|
91
|
+
"OpenAICompatibleChatCompletion",
|
|
92
|
+
"ClaudeChatCompletion",
|
|
93
|
+
]
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# coding=utf-8
|
|
2
|
+
#
|
|
3
|
+
# Tool approval primitives
|
|
4
|
+
#
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import uuid
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from typing import Protocol
|
|
10
|
+
|
|
11
|
+
from .types import UnifiedToolCall
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class ToolApprovalRequest:
|
|
16
|
+
approval_id: str
|
|
17
|
+
tool_call_id: str
|
|
18
|
+
tool_name: str
|
|
19
|
+
arguments: str
|
|
20
|
+
reason: str | None = None
|
|
21
|
+
|
|
22
|
+
@classmethod
|
|
23
|
+
def from_tool_call(
|
|
24
|
+
cls,
|
|
25
|
+
tool_call: UnifiedToolCall,
|
|
26
|
+
reason: str | None = None,
|
|
27
|
+
) -> "ToolApprovalRequest":
|
|
28
|
+
return cls(
|
|
29
|
+
approval_id=f"appr_{uuid.uuid4().hex}",
|
|
30
|
+
tool_call_id=tool_call.id,
|
|
31
|
+
tool_name=tool_call.name,
|
|
32
|
+
arguments=tool_call.arguments or "{}",
|
|
33
|
+
reason=reason,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass
|
|
38
|
+
class ToolApprovalDecision:
|
|
39
|
+
approved: bool
|
|
40
|
+
reason: str | None = None
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class ToolApprovalHandler(Protocol):
|
|
44
|
+
async def request_approval(
|
|
45
|
+
self,
|
|
46
|
+
request: ToolApprovalRequest,
|
|
47
|
+
) -> ToolApprovalDecision:
|
|
48
|
+
...
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class DenyToolApprovalHandler:
|
|
52
|
+
def __init__(self, reason: str = "Tool execution denied"):
|
|
53
|
+
self.reason = reason
|
|
54
|
+
|
|
55
|
+
async def request_approval(
|
|
56
|
+
self,
|
|
57
|
+
request: ToolApprovalRequest,
|
|
58
|
+
) -> ToolApprovalDecision:
|
|
59
|
+
return ToolApprovalDecision(approved=False, reason=self.reason)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def denied_tool_result(request: ToolApprovalRequest, reason: str | None = None) -> str:
|
|
63
|
+
detail = reason or "Tool execution was not approved"
|
|
64
|
+
return f"Tool approval denied for {request.tool_name}: {detail}"
|
my_llmkit/chat/base.py
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
# coding=utf-8
|
|
2
|
+
#
|
|
3
|
+
# 抽象基类
|
|
4
|
+
#
|
|
5
|
+
import logging
|
|
6
|
+
from abc import ABC, abstractmethod
|
|
7
|
+
from typing import AsyncIterator, Optional, Type, Union
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
from .model_settings import ModelSettings
|
|
11
|
+
from pydantic import BaseModel
|
|
12
|
+
|
|
13
|
+
from ..mcp.mcp_client import MCPServerType
|
|
14
|
+
from .approval import DenyToolApprovalHandler, ToolApprovalHandler
|
|
15
|
+
from .events import Usage
|
|
16
|
+
from .runner import (
|
|
17
|
+
ChatCompletionResult,
|
|
18
|
+
ChatCompletionStreamRunner,
|
|
19
|
+
MaxRoundsExceededError,
|
|
20
|
+
MessagePreparer,
|
|
21
|
+
)
|
|
22
|
+
from .tools import ToolExecutor, ToolFunctions
|
|
23
|
+
from .types import UnifiedChunk, UnifiedMessage, UnifiedResponse
|
|
24
|
+
|
|
25
|
+
logger = logging.getLogger(__name__)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class LLMChatCompletion(ABC):
|
|
29
|
+
"""
|
|
30
|
+
LLM 客户端抽象基类 - 使用统一类型,与任何 SDK 解耦。
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
def __init__(
|
|
34
|
+
self,
|
|
35
|
+
api_key: str,
|
|
36
|
+
api_base: str,
|
|
37
|
+
model: str,
|
|
38
|
+
model_settings: Optional[ModelSettings] = None,
|
|
39
|
+
timeout: Optional[float] = None,
|
|
40
|
+
):
|
|
41
|
+
self.api_key = api_key
|
|
42
|
+
self.api_base = api_base
|
|
43
|
+
self.model = model
|
|
44
|
+
self.model_settings = model_settings or ModelSettings()
|
|
45
|
+
self.timeout = timeout
|
|
46
|
+
|
|
47
|
+
@abstractmethod
|
|
48
|
+
async def acreate_stream(
|
|
49
|
+
self,
|
|
50
|
+
messages: list[UnifiedMessage],
|
|
51
|
+
tools: Optional[ToolFunctions] = None,
|
|
52
|
+
mcp_servers: Optional[dict[str, MCPServerType]] = None,
|
|
53
|
+
response_format: Optional[Union[Type[BaseModel], dict]] = None,
|
|
54
|
+
) -> AsyncIterator[UnifiedChunk]:
|
|
55
|
+
"""
|
|
56
|
+
发起流式请求,返回统一格式的 UnifiedChunk 流。
|
|
57
|
+
子类负责将厂商特定格式转换为 UnifiedChunk。
|
|
58
|
+
"""
|
|
59
|
+
raise NotImplementedError
|
|
60
|
+
|
|
61
|
+
@abstractmethod
|
|
62
|
+
async def acreate(
|
|
63
|
+
self,
|
|
64
|
+
messages: list[UnifiedMessage],
|
|
65
|
+
tools: Optional[ToolFunctions] = None,
|
|
66
|
+
mcp_servers: Optional[dict[str, MCPServerType]] = None,
|
|
67
|
+
response_format: Optional[Union[Type[BaseModel], dict]] = None,
|
|
68
|
+
) -> UnifiedResponse:
|
|
69
|
+
"""
|
|
70
|
+
发起非流式请求,返回统一格式的 UnifiedResponse。
|
|
71
|
+
子类负责将厂商特定格式转换为 UnifiedResponse。
|
|
72
|
+
"""
|
|
73
|
+
raise NotImplementedError
|
|
74
|
+
|
|
75
|
+
def run_stream(
|
|
76
|
+
self,
|
|
77
|
+
messages: list[UnifiedMessage],
|
|
78
|
+
tools: Optional[ToolFunctions] = None,
|
|
79
|
+
mcp_servers: Optional[dict[str, MCPServerType]] = None,
|
|
80
|
+
max_rounds: int = 10,
|
|
81
|
+
response_format: Optional[Union[Type[BaseModel], dict]] = None,
|
|
82
|
+
approval_handler: Optional[ToolApprovalHandler] = None,
|
|
83
|
+
message_preparer: Optional[MessagePreparer] = None,
|
|
84
|
+
) -> ChatCompletionStreamRunner:
|
|
85
|
+
return ChatCompletionStreamRunner(
|
|
86
|
+
self,
|
|
87
|
+
messages,
|
|
88
|
+
tools=tools,
|
|
89
|
+
mcp_servers=mcp_servers,
|
|
90
|
+
max_rounds=max_rounds,
|
|
91
|
+
response_format=response_format,
|
|
92
|
+
approval_handler=approval_handler,
|
|
93
|
+
message_preparer=message_preparer,
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
async def run(
|
|
97
|
+
self,
|
|
98
|
+
messages: list[UnifiedMessage],
|
|
99
|
+
tools: Optional[ToolFunctions] = None,
|
|
100
|
+
mcp_servers: Optional[dict[str, MCPServerType]] = None,
|
|
101
|
+
max_rounds: int = 10,
|
|
102
|
+
response_format: Optional[Union[Type[BaseModel], dict]] = None,
|
|
103
|
+
message_preparer: Optional[MessagePreparer] = None,
|
|
104
|
+
) -> ChatCompletionResult:
|
|
105
|
+
"""
|
|
106
|
+
非流式多轮对话,直接返回完整结果。
|
|
107
|
+
"""
|
|
108
|
+
inner_messages = messages.copy()
|
|
109
|
+
usages: list[Usage] = []
|
|
110
|
+
last_content: Optional[str] = None
|
|
111
|
+
current_round = 0
|
|
112
|
+
finished = False
|
|
113
|
+
tool_executor = ToolExecutor(tools, mcp_servers)
|
|
114
|
+
approval_handler = DenyToolApprovalHandler(
|
|
115
|
+
"Tool approval is not available in non-streaming mode"
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
while current_round < max_rounds:
|
|
119
|
+
logger.debug("========== Round: %s =========", current_round)
|
|
120
|
+
current_round += 1
|
|
121
|
+
|
|
122
|
+
request_messages = (
|
|
123
|
+
message_preparer(inner_messages)
|
|
124
|
+
if message_preparer
|
|
125
|
+
else inner_messages.copy()
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
# 调用模型(非流式)。工具循环的权威历史仍是 inner_messages。
|
|
129
|
+
response = await self.acreate(
|
|
130
|
+
messages=request_messages,
|
|
131
|
+
tools=tools,
|
|
132
|
+
mcp_servers=mcp_servers,
|
|
133
|
+
response_format=response_format,
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
# 记录 usage
|
|
137
|
+
if response.usage:
|
|
138
|
+
usages.append(
|
|
139
|
+
Usage(
|
|
140
|
+
completion_tokens=response.usage.completion_tokens,
|
|
141
|
+
prompt_tokens=response.usage.prompt_tokens,
|
|
142
|
+
total_tokens=response.usage.total_tokens,
|
|
143
|
+
reasoning_tokens=response.usage.reasoning_tokens,
|
|
144
|
+
cached_tokens=response.usage.cached_tokens,
|
|
145
|
+
model=response.usage.model,
|
|
146
|
+
)
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
# 构建 assistant 消息
|
|
150
|
+
assistant_message = UnifiedMessage(
|
|
151
|
+
role="assistant",
|
|
152
|
+
content=response.content if response.content else None,
|
|
153
|
+
reasoning_content=response.reasoning_content
|
|
154
|
+
if response.reasoning_content
|
|
155
|
+
else None,
|
|
156
|
+
tool_calls=response.tool_calls if response.tool_calls else None,
|
|
157
|
+
vendor_metadata=response.vendor_metadata,
|
|
158
|
+
)
|
|
159
|
+
inner_messages.append(assistant_message)
|
|
160
|
+
|
|
161
|
+
# 如果没有工具调用,结束循环
|
|
162
|
+
if not response.tool_calls:
|
|
163
|
+
last_content = response.content
|
|
164
|
+
finished = True
|
|
165
|
+
break
|
|
166
|
+
|
|
167
|
+
# 执行工具调用
|
|
168
|
+
for tool_call in response.tool_calls:
|
|
169
|
+
tool_result = await tool_executor.execute_result_with_approval(
|
|
170
|
+
tool_call,
|
|
171
|
+
approval_handler,
|
|
172
|
+
)
|
|
173
|
+
tool_message = UnifiedMessage(
|
|
174
|
+
role="tool",
|
|
175
|
+
content=tool_result.text,
|
|
176
|
+
tool_call_id=tool_call.id,
|
|
177
|
+
tool_result=tool_result,
|
|
178
|
+
)
|
|
179
|
+
inner_messages.append(tool_message)
|
|
180
|
+
|
|
181
|
+
if not finished:
|
|
182
|
+
logger.error("Max rounds exceeded: %s", max_rounds)
|
|
183
|
+
raise MaxRoundsExceededError(max_rounds)
|
|
184
|
+
|
|
185
|
+
return ChatCompletionResult(
|
|
186
|
+
last_content=last_content,
|
|
187
|
+
usages=usages,
|
|
188
|
+
messages=inner_messages,
|
|
189
|
+
response_format=response_format
|
|
190
|
+
if isinstance(response_format, type)
|
|
191
|
+
and issubclass(response_format, BaseModel)
|
|
192
|
+
else None,
|
|
193
|
+
)
|