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 ADDED
@@ -0,0 +1,7 @@
1
+ # coding=utf-8
2
+ #
3
+ # my_llmkit 包初始化
4
+ # 导入 log 模块以确保日志配置生效
5
+ #
6
+
7
+ from . import log # noqa: F401
@@ -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
+ )