clovers-agent 0.0.1__tar.gz
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.
- clovers_agent-0.0.1/PKG-INFO +15 -0
- clovers_agent-0.0.1/README.md +0 -0
- clovers_agent-0.0.1/clovers_agent/__init__.py +37 -0
- clovers_agent-0.0.1/clovers_agent/config.py +51 -0
- clovers_agent-0.0.1/clovers_agent/core.py +334 -0
- clovers_agent-0.0.1/clovers_agent/typing/__init__.py +13 -0
- clovers_agent-0.0.1/clovers_agent/typing/clovers.py +11 -0
- clovers_agent-0.0.1/clovers_agent/typing/json_schema.py +29 -0
- clovers_agent-0.0.1/clovers_agent/typing/message.py +33 -0
- clovers_agent-0.0.1/clovers_agent/typing/payload.py +35 -0
- clovers_agent-0.0.1/pyproject.toml +16 -0
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: clovers-agent
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary:
|
|
5
|
+
Author: KarisAya
|
|
6
|
+
Author-email: karisaya@foxmail.com
|
|
7
|
+
Requires-Python: >=3.12,<4.0.0
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
11
|
+
Requires-Dist: clovers (>=0.4.6,<1.0.0)
|
|
12
|
+
Requires-Dist: httpx (>=0.28.1,<0.29.0)
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
|
|
15
|
+
|
|
File without changes
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import httpx
|
|
2
|
+
from datetime import datetime
|
|
3
|
+
from clovers import Plugin, Result
|
|
4
|
+
from .core import ToolManager, CloversAgent
|
|
5
|
+
from .typing import Event
|
|
6
|
+
|
|
7
|
+
__plugin__ = Plugin(build_result=lambda result: Result("text", result), priority=100)
|
|
8
|
+
__plugin__.set_protocol("properties", Event)
|
|
9
|
+
|
|
10
|
+
agent: CloversAgent
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@__plugin__.startup
|
|
14
|
+
async def _():
|
|
15
|
+
global agent
|
|
16
|
+
agent = CloversAgent("CloversAgent", httpx.AsyncClient(timeout=60))
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@__plugin__.handle(None, ["user_id", "group_id", "nickname", "image_list", "to_me"], priority=2, block=False)
|
|
20
|
+
async def _(event: Event):
|
|
21
|
+
session = agent.current_session(event)
|
|
22
|
+
now = datetime.now()
|
|
23
|
+
if event.to_me:
|
|
24
|
+
session.silence.append((f"{event.nickname}[{now.strftime("%I:%M %p")}]@me {event.message}", now.timestamp()))
|
|
25
|
+
else:
|
|
26
|
+
session.silence.append((f"{event.nickname}[{now.strftime("%I:%M %p")}]{event.message}", now.timestamp()))
|
|
27
|
+
return
|
|
28
|
+
if session.running:
|
|
29
|
+
return
|
|
30
|
+
session.running = True
|
|
31
|
+
result = await agent.chat(event)
|
|
32
|
+
session.running = False
|
|
33
|
+
return result
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
__version__ = "0.1.0"
|
|
37
|
+
__all__ = ["ToolManager", "CloversAgent", "Event", "__plugin__"]
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
from pydantic import BaseModel
|
|
2
|
+
from clovers.config import Config as CloversConfig
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class Config(BaseModel):
|
|
6
|
+
path: str = "./data/clovers-agent/"
|
|
7
|
+
"""数据文件路径"""
|
|
8
|
+
plugins: list[str] = []
|
|
9
|
+
"""插件列表"""
|
|
10
|
+
plugin_dirs: list[str] = []
|
|
11
|
+
"""插件路径"""
|
|
12
|
+
memory_timeout: int = 7200
|
|
13
|
+
"""记忆超时时间"""
|
|
14
|
+
topic_coldown: int = 60
|
|
15
|
+
"""话题冷却时间"""
|
|
16
|
+
memory_size: int = 20
|
|
17
|
+
"""记忆长度"""
|
|
18
|
+
url: str
|
|
19
|
+
"""接入点url"""
|
|
20
|
+
model: str
|
|
21
|
+
"""模型版本名"""
|
|
22
|
+
api_key: str
|
|
23
|
+
"""API密钥"""
|
|
24
|
+
style_prompt: str = """你的名字是小叶子,一只性格可爱、偶尔慵懒的白发猫娘。
|
|
25
|
+
你在群聊里,会和不同的群友进行对话。
|
|
26
|
+
你接收的消息格式为 `用户名[时间]信息`,你的回应不该带有`用户名[时间]`。
|
|
27
|
+
信息以 `@me` 开头表示这条信息的at对象是你,你的回应只针对at你的消息,其他信息为群聊语境。
|
|
28
|
+
你应该注意在与哪个用户对话,不要让昵称的含义影响到你的回复。
|
|
29
|
+
回复前请注意你的心情状态,你的情绪转换必须有过渡,严禁一次对话内瞬间变脸。
|
|
30
|
+
|
|
31
|
+
# 回复准则
|
|
32
|
+
- 你的回复要像聊天群中的真实聊天,除非是在进行详细讲解,否则回复长度严格限制在 1 个段落。
|
|
33
|
+
- 避免书面语,多用口语,不要重复用户的问题,也不要说“我明白你的意思了”、“好的”之类的废话。
|
|
34
|
+
- 你偶尔会用()来表示状态和动作,括号内是你的状态和动作。
|
|
35
|
+
- 禁止在回复末尾反问用户。"""
|
|
36
|
+
"""对话风格提示"""
|
|
37
|
+
call_prompt: str = """你的任务是根据当前对话需求,选择并调用最合适的工具。
|
|
38
|
+
|
|
39
|
+
# 执行准则
|
|
40
|
+
- 极简原则:严格按照工具定义的 JSON 模式提取参数,仅输出工具调用代码,禁止任何自然语言描述。
|
|
41
|
+
- 结束调用:当你已经获取了足够的信息来回答用户,或无法通过现有工具获得更多信息时,请直接给出简洁的信息汇总。
|
|
42
|
+
- 直奔主题:汇总信息时不要说“很高兴为你服务”之类的AI客套话,不需要维持特定的说话语气,保持客观专业即可。
|
|
43
|
+
"""
|
|
44
|
+
"""调用提示"""
|
|
45
|
+
|
|
46
|
+
@classmethod
|
|
47
|
+
def sync_config(cls):
|
|
48
|
+
"""获取 `CloversConfig.environ()[__package__]` 配置并将默认配置同步到全局配置中。"""
|
|
49
|
+
__config_dict__: dict = CloversConfig.environ().setdefault(__package__, {})
|
|
50
|
+
__config_dict__.update((__config__ := cls.model_validate(__config_dict__)).model_dump())
|
|
51
|
+
return __config__
|
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import asyncio
|
|
3
|
+
import httpx
|
|
4
|
+
import time
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from importlib import import_module
|
|
8
|
+
from clovers.utils import import_name, list_modules
|
|
9
|
+
from clovers.logger import logger
|
|
10
|
+
from collections import deque
|
|
11
|
+
from collections.abc import Iterable, Callable, Coroutine
|
|
12
|
+
from typing import Literal, TypedDict, Any, Concatenate
|
|
13
|
+
from .typing import Event, ChatMessage, ToolMessage, Payload, FunctionToolInfo
|
|
14
|
+
from .typing.json_schema import JSONSchemaType
|
|
15
|
+
from .config import Config
|
|
16
|
+
|
|
17
|
+
type AgentFunction[**P] = Callable[Concatenate["CloversAgent", Any, P], Coroutine[Any, Any, str]]
|
|
18
|
+
type WrappedAgentFunction[**P] = Callable[Concatenate[str, "CloversAgent", Any, P], Coroutine[Any, Any, tuple[ToolMessage, str]]]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class ToolManager:
|
|
22
|
+
type Parameters = dict[str, JSONSchemaType]
|
|
23
|
+
|
|
24
|
+
class ExtraToolsType(TypedDict):
|
|
25
|
+
info: FunctionToolInfo
|
|
26
|
+
keywords: set[str]
|
|
27
|
+
|
|
28
|
+
def __init__(self, name: str = "") -> None:
|
|
29
|
+
self.name = name
|
|
30
|
+
self.intro_tools: list[FunctionToolInfo] = []
|
|
31
|
+
self.skill_keywords: set[str] = set()
|
|
32
|
+
self.skill_hooks: dict[str, AgentFunction] = {}
|
|
33
|
+
self.extra_tools: list[ToolManager.ExtraToolsType] = []
|
|
34
|
+
self.functions: dict[str, WrappedAgentFunction] = {}
|
|
35
|
+
|
|
36
|
+
def tool(self, name: str, description: str, parameters: Parameters | None, keywords: Iterable[str] | None = None):
|
|
37
|
+
if name in self.functions:
|
|
38
|
+
raise ValueError(f"Tool {name} already exists.")
|
|
39
|
+
info: FunctionToolInfo = {
|
|
40
|
+
"type": "function",
|
|
41
|
+
"function": {
|
|
42
|
+
"name": name,
|
|
43
|
+
"description": description,
|
|
44
|
+
},
|
|
45
|
+
}
|
|
46
|
+
if parameters:
|
|
47
|
+
info["function"]["parameters"] = {
|
|
48
|
+
"type": "object",
|
|
49
|
+
"properties": parameters,
|
|
50
|
+
"required": list(parameters.keys()),
|
|
51
|
+
}
|
|
52
|
+
if not keywords:
|
|
53
|
+
self.intro_tools.append(info)
|
|
54
|
+
else:
|
|
55
|
+
keywords = set(keywords)
|
|
56
|
+
self.skill_keywords.update(keywords)
|
|
57
|
+
self.extra_tools.append({"info": info, "keywords": keywords})
|
|
58
|
+
|
|
59
|
+
def decorator(func: AgentFunction) -> WrappedAgentFunction:
|
|
60
|
+
async def wrapper(tool_call_id, agent: CloversAgent, event, /, **kwargs):
|
|
61
|
+
logger.debug(f"[{agent.name}][TOOL CALL][{name}] called")
|
|
62
|
+
content = await func(agent, event, **kwargs)
|
|
63
|
+
message: ToolMessage = {"role": "tool", "tool_call_id": tool_call_id, "content": content}
|
|
64
|
+
return message, name
|
|
65
|
+
|
|
66
|
+
self.functions[name] = wrapper
|
|
67
|
+
return wrapper
|
|
68
|
+
|
|
69
|
+
return decorator
|
|
70
|
+
|
|
71
|
+
def on_skill(self, category: str):
|
|
72
|
+
def decorator(func: AgentFunction) -> AgentFunction:
|
|
73
|
+
self.skill_hooks[category] = func
|
|
74
|
+
return func
|
|
75
|
+
|
|
76
|
+
return decorator
|
|
77
|
+
|
|
78
|
+
def mixin(self, plugin: "ToolManager"):
|
|
79
|
+
conflict = plugin.functions.keys() & self.functions.keys()
|
|
80
|
+
if conflict:
|
|
81
|
+
return conflict
|
|
82
|
+
self.intro_tools.extend(plugin.intro_tools)
|
|
83
|
+
self.skill_keywords.update(plugin.skill_keywords)
|
|
84
|
+
self.extra_tools.extend(plugin.extra_tools)
|
|
85
|
+
self.functions.update(plugin.functions)
|
|
86
|
+
self.skill_hooks.update(plugin.skill_hooks)
|
|
87
|
+
|
|
88
|
+
def load_plugin(self, name: str | Path, is_path=False):
|
|
89
|
+
"""加载 clovers-agent 插件
|
|
90
|
+
|
|
91
|
+
Args:
|
|
92
|
+
name (str | Path): 插件的包名或路径
|
|
93
|
+
is_path (bool, optional): 是否为路径
|
|
94
|
+
"""
|
|
95
|
+
package = import_name(name, is_path)
|
|
96
|
+
try:
|
|
97
|
+
plugin = getattr(import_module(package), "__plugin__", None)
|
|
98
|
+
assert isinstance(plugin, ToolManager)
|
|
99
|
+
except Exception as e:
|
|
100
|
+
logger.exception(f'[{self.name}][loading plugin] "{package}" load failed', exc_info=e)
|
|
101
|
+
return
|
|
102
|
+
conflict = self.mixin(plugin)
|
|
103
|
+
if conflict:
|
|
104
|
+
logger.error(f'[{self.name}][loading plugin] "{package}" conflict with {conflict}')
|
|
105
|
+
else:
|
|
106
|
+
logger.info(f'[{self.name}][loading plugin] "{package}" loaded')
|
|
107
|
+
plugin.name = plugin.name or package
|
|
108
|
+
|
|
109
|
+
def load_plugins_from_list(self, plugin_list: list[str]):
|
|
110
|
+
"""从包名列表加载插件
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
plugin_list (list[str]): 插件的包名列表
|
|
114
|
+
"""
|
|
115
|
+
for plugin in plugin_list:
|
|
116
|
+
self.load_plugin(plugin)
|
|
117
|
+
|
|
118
|
+
def load_plugins_from_dirs(self, plugin_dirs: list[str]):
|
|
119
|
+
"""从本地目录列表加载插件
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
plugin_dirs (list[str]): 插件的目录列表
|
|
123
|
+
"""
|
|
124
|
+
for plugin_dir in plugin_dirs:
|
|
125
|
+
plugin_dir = Path(plugin_dir)
|
|
126
|
+
if not plugin_dir.exists():
|
|
127
|
+
plugin_dir.mkdir(parents=True, exist_ok=True)
|
|
128
|
+
continue
|
|
129
|
+
for plugin in list_modules(plugin_dir):
|
|
130
|
+
self.load_plugin(plugin)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
class Session:
|
|
134
|
+
type UserMessage = ChatMessage[Literal["user"]]
|
|
135
|
+
type AssistantMessage = ChatMessage[Literal["assistant"]]
|
|
136
|
+
type Timestamp = int | float
|
|
137
|
+
|
|
138
|
+
records: deque[tuple[UserMessage, AssistantMessage, Timestamp]]
|
|
139
|
+
silence: deque[tuple[str, Timestamp]]
|
|
140
|
+
|
|
141
|
+
def __init__(self, size: int) -> None:
|
|
142
|
+
self.records = deque(maxlen=size)
|
|
143
|
+
self.silence = deque()
|
|
144
|
+
self.running: bool = False
|
|
145
|
+
|
|
146
|
+
def memory_filter(self, timeout: int | float):
|
|
147
|
+
"""过滤记忆"""
|
|
148
|
+
while self.records and (self.records[0][2] <= timeout):
|
|
149
|
+
self.records.popleft()
|
|
150
|
+
while self.silence and (self.silence[0][1] <= timeout):
|
|
151
|
+
self.silence.popleft()
|
|
152
|
+
|
|
153
|
+
@property
|
|
154
|
+
def context(self):
|
|
155
|
+
for request, reply, _ in self.records:
|
|
156
|
+
yield request
|
|
157
|
+
yield reply
|
|
158
|
+
|
|
159
|
+
def over(self, request: UserMessage, reply: AssistantMessage, timestamp: int | float):
|
|
160
|
+
self.records.append((request, reply, timestamp))
|
|
161
|
+
self.silence.clear()
|
|
162
|
+
|
|
163
|
+
def clear(self):
|
|
164
|
+
self.records.clear()
|
|
165
|
+
self.silence.clear()
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
class CloversAgent(ToolManager):
|
|
169
|
+
"""OpenAI API"""
|
|
170
|
+
|
|
171
|
+
def __init__(self, name: str, async_client: httpx.AsyncClient) -> None:
|
|
172
|
+
super().__init__(name)
|
|
173
|
+
self.async_client = async_client
|
|
174
|
+
config = Config.sync_config()
|
|
175
|
+
self.model = config.model
|
|
176
|
+
self.url = f"{config.url.rstrip("/")}/chat/completions"
|
|
177
|
+
self.headers = {"Authorization": f"Bearer {config.api_key}", "Content-Type": "application/json"}
|
|
178
|
+
self.style_prompt = config.style_prompt
|
|
179
|
+
self.call_prompt = config.call_prompt
|
|
180
|
+
self._memory_size = config.memory_size
|
|
181
|
+
self.memory_timeout = config.memory_timeout
|
|
182
|
+
self.topic_coldown = config.topic_coldown
|
|
183
|
+
self.sessions: dict[str, Session] = {}
|
|
184
|
+
self.current_input: Session.UserMessage | None = None
|
|
185
|
+
self.load_plugins_from_list(config.plugins)
|
|
186
|
+
self.load_plugins_from_dirs(config.plugin_dirs)
|
|
187
|
+
self.toolmap: dict[str, FunctionToolInfo] = {}
|
|
188
|
+
if self.skill_keywords:
|
|
189
|
+
skill_keywords = list(self.skill_keywords)
|
|
190
|
+
self.tool(
|
|
191
|
+
"skill_menu",
|
|
192
|
+
"获取更多技能,如果assistant无法单独完成用户指令,则需要调用此方法获取更多技能。",
|
|
193
|
+
{"category": {"type": "string", "description": "选择需要的技能关键词", "enum": skill_keywords}},
|
|
194
|
+
)(self.skill_menu)
|
|
195
|
+
logger.info(f"[{self.name}][LOAD SKILLS] {skill_keywords}")
|
|
196
|
+
for tool in self.intro_tools:
|
|
197
|
+
self.toolmap[tool["function"]["name"]] = tool
|
|
198
|
+
for tool in self.extra_tools:
|
|
199
|
+
self.toolmap[tool["info"]["function"]["name"]] = tool["info"]
|
|
200
|
+
|
|
201
|
+
@staticmethod
|
|
202
|
+
async def skill_menu(agent: "CloversAgent", event: Event, category: str):
|
|
203
|
+
tip = f"已获取技能:{category}"
|
|
204
|
+
event.properties["skill_menu"] = category
|
|
205
|
+
hook = agent.skill_hooks.get(category)
|
|
206
|
+
if hook:
|
|
207
|
+
info = await hook(agent, event)
|
|
208
|
+
if info:
|
|
209
|
+
tip += f",{info}"
|
|
210
|
+
return tip
|
|
211
|
+
|
|
212
|
+
@staticmethod
|
|
213
|
+
def build_content(text: str, image_list: list[str] | None):
|
|
214
|
+
if not image_list:
|
|
215
|
+
return text
|
|
216
|
+
else:
|
|
217
|
+
content = []
|
|
218
|
+
if text:
|
|
219
|
+
content.append({"type": "text", "text": text})
|
|
220
|
+
if image_list:
|
|
221
|
+
content.extend({"type": "image_url", "image_url": {"url": image_url}} for image_url in image_list)
|
|
222
|
+
return content
|
|
223
|
+
|
|
224
|
+
async def call_api(self, payload: Payload):
|
|
225
|
+
resp = await self.async_client.post(self.url, headers=self.headers, json=payload)
|
|
226
|
+
try:
|
|
227
|
+
resp.raise_for_status()
|
|
228
|
+
except:
|
|
229
|
+
logger.error(json.dumps(payload, indent=4, ensure_ascii=False))
|
|
230
|
+
raise
|
|
231
|
+
return resp.json()["choices"][0]["message"]
|
|
232
|
+
|
|
233
|
+
async def function_call(self, event: Event, call_infos: list[dict]) -> list[tuple[ToolMessage, str]]:
|
|
234
|
+
task_queue = []
|
|
235
|
+
for call_info in call_infos:
|
|
236
|
+
func = self.functions[call_info["function"]["name"]]
|
|
237
|
+
kwargs = json.loads(call_info["function"]["arguments"])
|
|
238
|
+
task_queue.append(func(call_info["id"], self, event, **kwargs))
|
|
239
|
+
return await asyncio.gather(*task_queue)
|
|
240
|
+
|
|
241
|
+
def build_payload(self, context: Iterable[ChatMessage] | None = None, system_prompt: str | None = None) -> Payload:
|
|
242
|
+
payload: Payload = {"model": self.model, "messages": []}
|
|
243
|
+
if system_prompt:
|
|
244
|
+
payload["messages"].append({"role": "system", "content": system_prompt})
|
|
245
|
+
if context:
|
|
246
|
+
payload["messages"].extend(context)
|
|
247
|
+
return payload
|
|
248
|
+
|
|
249
|
+
@staticmethod
|
|
250
|
+
def session_id(event: Event):
|
|
251
|
+
return event.group_id or f"private-{event.user_id}"
|
|
252
|
+
|
|
253
|
+
def current_session(self, event: Event):
|
|
254
|
+
session_id = self.session_id(event)
|
|
255
|
+
if session_id not in self.sessions:
|
|
256
|
+
self.sessions[session_id] = Session(self._memory_size)
|
|
257
|
+
return self.sessions[session_id]
|
|
258
|
+
|
|
259
|
+
def select_tools(self, keyword: str):
|
|
260
|
+
return [d["info"] for d in self.extra_tools if keyword in d["keywords"]]
|
|
261
|
+
|
|
262
|
+
async def summary_context(self, event: Event) -> str:
|
|
263
|
+
session = self.current_session(event)
|
|
264
|
+
payload = self.build_payload(context=session.context)
|
|
265
|
+
payload["messages"].append({"role": "user", "content": "请对以上对话进行深度总结。"})
|
|
266
|
+
payload["response_format"] = {
|
|
267
|
+
"type": "json_schema",
|
|
268
|
+
"json_schema": {
|
|
269
|
+
"name": "topic_summary",
|
|
270
|
+
"strict": True,
|
|
271
|
+
"schema": {
|
|
272
|
+
"type": "object",
|
|
273
|
+
"properties": {"summary": {"type": "string", "description": "对话内容的精炼总结,保留核心内容和结论。"}},
|
|
274
|
+
"required": ["summary"],
|
|
275
|
+
"additionalProperties": False,
|
|
276
|
+
},
|
|
277
|
+
},
|
|
278
|
+
}
|
|
279
|
+
return json.loads((await self.call_api(payload))["content"])["summary"].strip()
|
|
280
|
+
|
|
281
|
+
async def call_unit(self, event: Event, payload: Payload):
|
|
282
|
+
system_prompt = f"{self.style_prompt}\nDate:{datetime.now().strftime('%m-%d')}"
|
|
283
|
+
system_message: ChatMessage = {"role": "system", "content": system_prompt}
|
|
284
|
+
payload["messages"].insert(0, system_message)
|
|
285
|
+
payload["tools"] = self.intro_tools
|
|
286
|
+
resp = await self.call_api(payload)
|
|
287
|
+
# 退出条件:不需要额外技能
|
|
288
|
+
if not (tool_calls := resp.get("tool_calls")):
|
|
289
|
+
return resp["content"].strip()
|
|
290
|
+
event.properties["skill_menu"] = ""
|
|
291
|
+
intro_prompt = "".join(msg[0]["content"] for msg in await self.function_call(event, tool_calls) if msg[1])
|
|
292
|
+
# 退出条件:不需要额外技能
|
|
293
|
+
if event.skill_menu:
|
|
294
|
+
used_tools = {"skill_menu"}
|
|
295
|
+
system_message["content"] = f"{self.call_prompt}\n\n{intro_prompt}"
|
|
296
|
+
for _ in range(30):
|
|
297
|
+
payload["tools"] = [self.toolmap[k] for k in used_tools]
|
|
298
|
+
if event.skill_menu:
|
|
299
|
+
select_tools = self.select_tools(event.skill_menu)
|
|
300
|
+
payload["tools"].extend(tool for tool in select_tools if tool["function"]["name"] not in used_tools)
|
|
301
|
+
message = await self.call_api(payload)
|
|
302
|
+
if not (tool_calls := message.get("tool_calls")):
|
|
303
|
+
break
|
|
304
|
+
payload["messages"].append(message)
|
|
305
|
+
event.properties["skill_menu"] = ""
|
|
306
|
+
for msg, key in await self.function_call(event, tool_calls):
|
|
307
|
+
used_tools.add(key)
|
|
308
|
+
payload["messages"].append(msg)
|
|
309
|
+
del payload["tools"]
|
|
310
|
+
system_message["content"] = f"{system_prompt}\n\n{intro_prompt}"
|
|
311
|
+
return (await self.call_api(payload))["content"].strip()
|
|
312
|
+
|
|
313
|
+
async def chat(self, event: Event):
|
|
314
|
+
session = self.current_session(event)
|
|
315
|
+
payload: Payload = {"model": self.model, "messages": []}
|
|
316
|
+
now = int(time.time())
|
|
317
|
+
session.memory_filter(now - self.memory_timeout)
|
|
318
|
+
if len(session.records) >= 5 and session.records[0][2] < now - self.memory_timeout:
|
|
319
|
+
summary = await self.summary_context(event)
|
|
320
|
+
logger.debug(f"[{self.name}][SUMMARY] {summary}")
|
|
321
|
+
session.clear()
|
|
322
|
+
session.silence.append((summary, now))
|
|
323
|
+
payload["messages"].extend(session.context)
|
|
324
|
+
content = self.build_content("\n".join(x[0] for x in session.silence), event.image_list)
|
|
325
|
+
self.current_input = {"role": "user", "content": content}
|
|
326
|
+
payload["messages"].append(self.current_input)
|
|
327
|
+
try:
|
|
328
|
+
resp = await self.call_unit(event, payload)
|
|
329
|
+
except Exception as e:
|
|
330
|
+
logger.exception(e, exc_info=e)
|
|
331
|
+
return
|
|
332
|
+
session.over(self.current_input, {"role": "assistant", "content": resp}, now)
|
|
333
|
+
self.current_input = None
|
|
334
|
+
return resp
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from typing import TypedDict, Literal, NotRequired
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
type BaseType = Literal["string", "boolean", "integer", "number"]
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class BaseJSONSchemaType[T: BaseType | list[BaseType]](TypedDict):
|
|
8
|
+
type: T
|
|
9
|
+
description: NotRequired[str]
|
|
10
|
+
enum: NotRequired[list[T]]
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ArrayJSONSchemaType(TypedDict):
|
|
14
|
+
type: Literal["array"]
|
|
15
|
+
description: NotRequired[str]
|
|
16
|
+
minItems: NotRequired[int]
|
|
17
|
+
maxItems: NotRequired[int]
|
|
18
|
+
items: NotRequired[BaseJSONSchemaType | list[BaseJSONSchemaType]]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class ObjectJSONSchemaType(TypedDict):
|
|
22
|
+
type: Literal["object"]
|
|
23
|
+
properties: NotRequired[dict[str, "JSONSchemaType"]]
|
|
24
|
+
description: NotRequired[str]
|
|
25
|
+
required: NotRequired[list[str]]
|
|
26
|
+
additionalProperties: NotRequired[bool]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
type JSONSchemaType = BaseJSONSchemaType | ArrayJSONSchemaType | ObjectJSONSchemaType
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
from typing import TypedDict, Literal, NotRequired
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class TextSegment(TypedDict):
|
|
5
|
+
type: Literal["text"]
|
|
6
|
+
text: str
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class ImageSegment(TypedDict):
|
|
10
|
+
type: Literal["image_url"]
|
|
11
|
+
image_url: dict[Literal["url"], str]
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
type ContentSegment = TextSegment | ImageSegment
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ChatMessage[Role: Literal["system", "user", "assistant"]](TypedDict):
|
|
18
|
+
"""对话消息"""
|
|
19
|
+
|
|
20
|
+
role: Role
|
|
21
|
+
content: str | list[ContentSegment]
|
|
22
|
+
tools: NotRequired[list[dict]]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class ToolMessage(TypedDict):
|
|
26
|
+
"""工具消息"""
|
|
27
|
+
|
|
28
|
+
role: Literal["tool"]
|
|
29
|
+
content: str
|
|
30
|
+
tool_call_id: str
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
type Message = ChatMessage | ToolMessage
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
from typing import Literal, TypedDict, NotRequired
|
|
2
|
+
from .message import Message
|
|
3
|
+
from .json_schema import JSONSchemaType
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class FunctionToolDefinition(TypedDict):
|
|
7
|
+
name: str
|
|
8
|
+
"""工具名称`"""
|
|
9
|
+
description: str
|
|
10
|
+
"""工具描述`"""
|
|
11
|
+
parameters: NotRequired[JSONSchemaType]
|
|
12
|
+
"""工具参数`"""
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class FunctionToolInfo(TypedDict):
|
|
16
|
+
type: Literal["function"]
|
|
17
|
+
function: FunctionToolDefinition
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class JsonSchemaFormat(TypedDict):
|
|
21
|
+
name: str
|
|
22
|
+
strict: bool
|
|
23
|
+
schema: JSONSchemaType
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class ResponseFormat(TypedDict):
|
|
27
|
+
type: Literal["json_schema"]
|
|
28
|
+
json_schema: JsonSchemaFormat
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class Payload(TypedDict):
|
|
32
|
+
model: str
|
|
33
|
+
messages: list[Message]
|
|
34
|
+
tools: NotRequired[list[FunctionToolInfo]]
|
|
35
|
+
response_format: NotRequired[ResponseFormat]
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "clovers-agent"
|
|
3
|
+
version = "0.0.1"
|
|
4
|
+
description = ""
|
|
5
|
+
authors = [{ name = "KarisAya", email = "karisaya@foxmail.com" }]
|
|
6
|
+
readme = "README.md"
|
|
7
|
+
requires-python = ">=3.12,<4.0.0"
|
|
8
|
+
dependencies = ["clovers (>=0.4.6,<1.0.0)", "httpx (>=0.28.1,<0.29.0)"]
|
|
9
|
+
|
|
10
|
+
[build-system]
|
|
11
|
+
requires = ["poetry-core>=2.0.0,<3.0.0"]
|
|
12
|
+
build-backend = "poetry.core.masonry.api"
|
|
13
|
+
|
|
14
|
+
[tool.poetry.group.dev.dependencies]
|
|
15
|
+
sqlalchemy = ">=2.0.48,<3.0.0"
|
|
16
|
+
sqlmodel = ">=0.0.37,<0.0.38"
|