AstrBot 4.12.4__py3-none-any.whl → 4.13.1__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.
- astrbot/builtin_stars/astrbot/process_llm_request.py +42 -1
- astrbot/cli/__init__.py +1 -1
- astrbot/core/agent/runners/tool_loop_agent_runner.py +91 -1
- astrbot/core/agent/tool.py +61 -20
- astrbot/core/astr_agent_tool_exec.py +2 -2
- astrbot/core/{sandbox → computer}/booters/base.py +4 -4
- astrbot/core/{sandbox → computer}/booters/boxlite.py +2 -2
- astrbot/core/computer/booters/local.py +234 -0
- astrbot/core/{sandbox → computer}/booters/shipyard.py +2 -2
- astrbot/core/computer/computer_client.py +102 -0
- astrbot/core/{sandbox → computer}/tools/__init__.py +2 -1
- astrbot/core/{sandbox → computer}/tools/fs.py +1 -1
- astrbot/core/computer/tools/python.py +94 -0
- astrbot/core/{sandbox → computer}/tools/shell.py +13 -5
- astrbot/core/config/default.py +61 -9
- astrbot/core/db/__init__.py +3 -0
- astrbot/core/db/po.py +23 -61
- astrbot/core/db/sqlite.py +19 -1
- astrbot/core/message/components.py +2 -2
- astrbot/core/persona_mgr.py +8 -0
- astrbot/core/pipeline/context_utils.py +2 -2
- astrbot/core/pipeline/preprocess_stage/stage.py +1 -1
- astrbot/core/pipeline/process_stage/method/agent_sub_stages/internal.py +21 -6
- astrbot/core/pipeline/process_stage/utils.py +19 -4
- astrbot/core/pipeline/scheduler.py +1 -1
- astrbot/core/platform/sources/aiocqhttp/aiocqhttp_message_event.py +3 -3
- astrbot/core/platform/sources/qqofficial/qqofficial_message_event.py +5 -7
- astrbot/core/provider/manager.py +31 -0
- astrbot/core/provider/sources/gemini_source.py +12 -9
- astrbot/core/skills/__init__.py +3 -0
- astrbot/core/skills/skill_manager.py +238 -0
- astrbot/core/star/command_management.py +1 -1
- astrbot/core/star/config.py +1 -1
- astrbot/core/star/filter/command.py +1 -1
- astrbot/core/star/filter/custom_filter.py +2 -2
- astrbot/core/star/register/star_handler.py +1 -1
- astrbot/core/utils/astrbot_path.py +6 -0
- astrbot/dashboard/routes/__init__.py +2 -0
- astrbot/dashboard/routes/config.py +236 -2
- astrbot/dashboard/routes/persona.py +7 -0
- astrbot/dashboard/routes/skills.py +148 -0
- astrbot/dashboard/routes/util.py +102 -0
- astrbot/dashboard/server.py +19 -5
- {astrbot-4.12.4.dist-info → astrbot-4.13.1.dist-info}/METADATA +2 -2
- {astrbot-4.12.4.dist-info → astrbot-4.13.1.dist-info}/RECORD +52 -47
- astrbot/core/sandbox/sandbox_client.py +0 -52
- astrbot/core/sandbox/tools/python.py +0 -74
- /astrbot/core/{sandbox → computer}/olayer/__init__.py +0 -0
- /astrbot/core/{sandbox → computer}/olayer/filesystem.py +0 -0
- /astrbot/core/{sandbox → computer}/olayer/python.py +0 -0
- /astrbot/core/{sandbox → computer}/olayer/shell.py +0 -0
- {astrbot-4.12.4.dist-info → astrbot-4.13.1.dist-info}/WHEEL +0 -0
- {astrbot-4.12.4.dist-info → astrbot-4.13.1.dist-info}/entry_points.txt +0 -0
- {astrbot-4.12.4.dist-info → astrbot-4.13.1.dist-info}/licenses/LICENSE +0 -0
astrbot/core/db/po.py
CHANGED
|
@@ -6,6 +6,14 @@ from typing import TypedDict
|
|
|
6
6
|
from sqlmodel import JSON, Field, SQLModel, Text, UniqueConstraint
|
|
7
7
|
|
|
8
8
|
|
|
9
|
+
class TimestampMixin(SQLModel):
|
|
10
|
+
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
|
11
|
+
updated_at: datetime = Field(
|
|
12
|
+
default_factory=lambda: datetime.now(timezone.utc),
|
|
13
|
+
sa_column_kwargs={"onupdate": lambda: datetime.now(timezone.utc)},
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
9
17
|
class PlatformStat(SQLModel, table=True):
|
|
10
18
|
"""This class represents the statistics of bot usage across different platforms.
|
|
11
19
|
|
|
@@ -30,7 +38,7 @@ class PlatformStat(SQLModel, table=True):
|
|
|
30
38
|
)
|
|
31
39
|
|
|
32
40
|
|
|
33
|
-
class ConversationV2(SQLModel, table=True):
|
|
41
|
+
class ConversationV2(TimestampMixin, SQLModel, table=True):
|
|
34
42
|
__tablename__: str = "conversations"
|
|
35
43
|
|
|
36
44
|
inner_conversation_id: int | None = Field(
|
|
@@ -47,11 +55,7 @@ class ConversationV2(SQLModel, table=True):
|
|
|
47
55
|
platform_id: str = Field(nullable=False)
|
|
48
56
|
user_id: str = Field(nullable=False)
|
|
49
57
|
content: list | None = Field(default=None, sa_type=JSON)
|
|
50
|
-
|
|
51
|
-
updated_at: datetime = Field(
|
|
52
|
-
default_factory=lambda: datetime.now(timezone.utc),
|
|
53
|
-
sa_column_kwargs={"onupdate": datetime.now(timezone.utc)},
|
|
54
|
-
)
|
|
58
|
+
|
|
55
59
|
title: str | None = Field(default=None, max_length=255)
|
|
56
60
|
persona_id: str | None = Field(default=None)
|
|
57
61
|
token_usage: int = Field(default=0, nullable=False)
|
|
@@ -68,7 +72,7 @@ class ConversationV2(SQLModel, table=True):
|
|
|
68
72
|
)
|
|
69
73
|
|
|
70
74
|
|
|
71
|
-
class PersonaFolder(SQLModel, table=True):
|
|
75
|
+
class PersonaFolder(TimestampMixin, SQLModel, table=True):
|
|
72
76
|
"""Persona 文件夹,支持递归层级结构。
|
|
73
77
|
|
|
74
78
|
用于组织和管理多个 Persona,类似于文件系统的目录结构。
|
|
@@ -92,11 +96,6 @@ class PersonaFolder(SQLModel, table=True):
|
|
|
92
96
|
"""父文件夹ID,NULL表示根目录"""
|
|
93
97
|
description: str | None = Field(default=None, sa_type=Text)
|
|
94
98
|
sort_order: int = Field(default=0)
|
|
95
|
-
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
|
96
|
-
updated_at: datetime = Field(
|
|
97
|
-
default_factory=lambda: datetime.now(timezone.utc),
|
|
98
|
-
sa_column_kwargs={"onupdate": datetime.now(timezone.utc)},
|
|
99
|
-
)
|
|
100
99
|
|
|
101
100
|
__table_args__ = (
|
|
102
101
|
UniqueConstraint(
|
|
@@ -106,7 +105,7 @@ class PersonaFolder(SQLModel, table=True):
|
|
|
106
105
|
)
|
|
107
106
|
|
|
108
107
|
|
|
109
|
-
class Persona(SQLModel, table=True):
|
|
108
|
+
class Persona(TimestampMixin, SQLModel, table=True):
|
|
110
109
|
"""Persona is a set of instructions for LLMs to follow.
|
|
111
110
|
|
|
112
111
|
It can be used to customize the behavior of LLMs.
|
|
@@ -125,15 +124,12 @@ class Persona(SQLModel, table=True):
|
|
|
125
124
|
"""a list of strings, each representing a dialog to start with"""
|
|
126
125
|
tools: list | None = Field(default=None, sa_type=JSON)
|
|
127
126
|
"""None means use ALL tools for default, empty list means no tools, otherwise a list of tool names."""
|
|
127
|
+
skills: list | None = Field(default=None, sa_type=JSON)
|
|
128
|
+
"""None means use ALL skills for default, empty list means no skills, otherwise a list of skill names."""
|
|
128
129
|
folder_id: str | None = Field(default=None, max_length=36)
|
|
129
130
|
"""所属文件夹ID,NULL 表示在根目录"""
|
|
130
131
|
sort_order: int = Field(default=0)
|
|
131
132
|
"""排序顺序"""
|
|
132
|
-
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
|
133
|
-
updated_at: datetime = Field(
|
|
134
|
-
default_factory=lambda: datetime.now(timezone.utc),
|
|
135
|
-
sa_column_kwargs={"onupdate": datetime.now(timezone.utc)},
|
|
136
|
-
)
|
|
137
133
|
|
|
138
134
|
__table_args__ = (
|
|
139
135
|
UniqueConstraint(
|
|
@@ -143,7 +139,7 @@ class Persona(SQLModel, table=True):
|
|
|
143
139
|
)
|
|
144
140
|
|
|
145
141
|
|
|
146
|
-
class Preference(SQLModel, table=True):
|
|
142
|
+
class Preference(TimestampMixin, SQLModel, table=True):
|
|
147
143
|
"""This class represents preferences for bots."""
|
|
148
144
|
|
|
149
145
|
__tablename__: str = "preferences"
|
|
@@ -159,11 +155,6 @@ class Preference(SQLModel, table=True):
|
|
|
159
155
|
"""ID of the scope, such as 'global', 'umo', 'plugin_name'."""
|
|
160
156
|
key: str = Field(nullable=False)
|
|
161
157
|
value: dict = Field(sa_type=JSON, nullable=False)
|
|
162
|
-
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
|
163
|
-
updated_at: datetime = Field(
|
|
164
|
-
default_factory=lambda: datetime.now(timezone.utc),
|
|
165
|
-
sa_column_kwargs={"onupdate": datetime.now(timezone.utc)},
|
|
166
|
-
)
|
|
167
158
|
|
|
168
159
|
__table_args__ = (
|
|
169
160
|
UniqueConstraint(
|
|
@@ -175,7 +166,7 @@ class Preference(SQLModel, table=True):
|
|
|
175
166
|
)
|
|
176
167
|
|
|
177
168
|
|
|
178
|
-
class PlatformMessageHistory(SQLModel, table=True):
|
|
169
|
+
class PlatformMessageHistory(TimestampMixin, SQLModel, table=True):
|
|
179
170
|
"""This class represents the message history for a specific platform.
|
|
180
171
|
|
|
181
172
|
It is used to store messages that are not LLM-generated, such as user messages
|
|
@@ -196,14 +187,9 @@ class PlatformMessageHistory(SQLModel, table=True):
|
|
|
196
187
|
default=None,
|
|
197
188
|
) # Name of the sender in the platform
|
|
198
189
|
content: dict = Field(sa_type=JSON, nullable=False) # a message chain list
|
|
199
|
-
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
|
200
|
-
updated_at: datetime = Field(
|
|
201
|
-
default_factory=lambda: datetime.now(timezone.utc),
|
|
202
|
-
sa_column_kwargs={"onupdate": datetime.now(timezone.utc)},
|
|
203
|
-
)
|
|
204
190
|
|
|
205
191
|
|
|
206
|
-
class PlatformSession(SQLModel, table=True):
|
|
192
|
+
class PlatformSession(TimestampMixin, SQLModel, table=True):
|
|
207
193
|
"""Platform session table for managing user sessions across different platforms.
|
|
208
194
|
|
|
209
195
|
A session represents a chat window for a specific user on a specific platform.
|
|
@@ -231,11 +217,6 @@ class PlatformSession(SQLModel, table=True):
|
|
|
231
217
|
"""Display name for the session"""
|
|
232
218
|
is_group: int = Field(default=0, nullable=False)
|
|
233
219
|
"""0 for private chat, 1 for group chat (not implemented yet)"""
|
|
234
|
-
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
|
235
|
-
updated_at: datetime = Field(
|
|
236
|
-
default_factory=lambda: datetime.now(timezone.utc),
|
|
237
|
-
sa_column_kwargs={"onupdate": datetime.now(timezone.utc)},
|
|
238
|
-
)
|
|
239
220
|
|
|
240
221
|
__table_args__ = (
|
|
241
222
|
UniqueConstraint(
|
|
@@ -245,7 +226,7 @@ class PlatformSession(SQLModel, table=True):
|
|
|
245
226
|
)
|
|
246
227
|
|
|
247
228
|
|
|
248
|
-
class Attachment(SQLModel, table=True):
|
|
229
|
+
class Attachment(TimestampMixin, SQLModel, table=True):
|
|
249
230
|
"""This class represents attachments for messages in AstrBot.
|
|
250
231
|
|
|
251
232
|
Attachments can be images, files, or other media types.
|
|
@@ -267,11 +248,6 @@ class Attachment(SQLModel, table=True):
|
|
|
267
248
|
path: str = Field(nullable=False) # Path to the file on disk
|
|
268
249
|
type: str = Field(nullable=False) # Type of the file (e.g., 'image', 'file')
|
|
269
250
|
mime_type: str = Field(nullable=False) # MIME type of the file
|
|
270
|
-
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
|
271
|
-
updated_at: datetime = Field(
|
|
272
|
-
default_factory=lambda: datetime.now(timezone.utc),
|
|
273
|
-
sa_column_kwargs={"onupdate": datetime.now(timezone.utc)},
|
|
274
|
-
)
|
|
275
251
|
|
|
276
252
|
__table_args__ = (
|
|
277
253
|
UniqueConstraint(
|
|
@@ -281,7 +257,7 @@ class Attachment(SQLModel, table=True):
|
|
|
281
257
|
)
|
|
282
258
|
|
|
283
259
|
|
|
284
|
-
class ChatUIProject(SQLModel, table=True):
|
|
260
|
+
class ChatUIProject(TimestampMixin, SQLModel, table=True):
|
|
285
261
|
"""This class represents projects for organizing ChatUI conversations.
|
|
286
262
|
|
|
287
263
|
Projects allow users to group related conversations together.
|
|
@@ -308,11 +284,6 @@ class ChatUIProject(SQLModel, table=True):
|
|
|
308
284
|
"""Title of the project"""
|
|
309
285
|
description: str | None = Field(default=None, max_length=1000)
|
|
310
286
|
"""Description of the project"""
|
|
311
|
-
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
|
312
|
-
updated_at: datetime = Field(
|
|
313
|
-
default_factory=lambda: datetime.now(timezone.utc),
|
|
314
|
-
sa_column_kwargs={"onupdate": datetime.now(timezone.utc)},
|
|
315
|
-
)
|
|
316
287
|
|
|
317
288
|
__table_args__ = (
|
|
318
289
|
UniqueConstraint(
|
|
@@ -336,7 +307,6 @@ class SessionProjectRelation(SQLModel, table=True):
|
|
|
336
307
|
"""Session ID from PlatformSession"""
|
|
337
308
|
project_id: str = Field(nullable=False, max_length=36)
|
|
338
309
|
"""Project ID from ChatUIProject"""
|
|
339
|
-
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
|
340
310
|
|
|
341
311
|
__table_args__ = (
|
|
342
312
|
UniqueConstraint(
|
|
@@ -346,7 +316,7 @@ class SessionProjectRelation(SQLModel, table=True):
|
|
|
346
316
|
)
|
|
347
317
|
|
|
348
318
|
|
|
349
|
-
class CommandConfig(SQLModel, table=True):
|
|
319
|
+
class CommandConfig(TimestampMixin, SQLModel, table=True):
|
|
350
320
|
"""Per-command configuration overrides for dashboard management."""
|
|
351
321
|
|
|
352
322
|
__tablename__ = "command_configs" # type: ignore
|
|
@@ -366,14 +336,9 @@ class CommandConfig(SQLModel, table=True):
|
|
|
366
336
|
note: str | None = Field(default=None, sa_type=Text)
|
|
367
337
|
extra_data: dict | None = Field(default=None, sa_type=JSON)
|
|
368
338
|
auto_managed: bool = Field(default=False, nullable=False)
|
|
369
|
-
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
|
370
|
-
updated_at: datetime = Field(
|
|
371
|
-
default_factory=lambda: datetime.now(timezone.utc),
|
|
372
|
-
sa_column_kwargs={"onupdate": datetime.now(timezone.utc)},
|
|
373
|
-
)
|
|
374
339
|
|
|
375
340
|
|
|
376
|
-
class CommandConflict(SQLModel, table=True):
|
|
341
|
+
class CommandConflict(TimestampMixin, SQLModel, table=True):
|
|
377
342
|
"""Conflict tracking for duplicated command names."""
|
|
378
343
|
|
|
379
344
|
__tablename__ = "command_conflicts" # type: ignore
|
|
@@ -390,11 +355,6 @@ class CommandConflict(SQLModel, table=True):
|
|
|
390
355
|
note: str | None = Field(default=None, sa_type=Text)
|
|
391
356
|
extra_data: dict | None = Field(default=None, sa_type=JSON)
|
|
392
357
|
auto_generated: bool = Field(default=False, nullable=False)
|
|
393
|
-
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
|
394
|
-
updated_at: datetime = Field(
|
|
395
|
-
default_factory=lambda: datetime.now(timezone.utc),
|
|
396
|
-
sa_column_kwargs={"onupdate": datetime.now(timezone.utc)},
|
|
397
|
-
)
|
|
398
358
|
|
|
399
359
|
__table_args__ = (
|
|
400
360
|
UniqueConstraint(
|
|
@@ -442,6 +402,8 @@ class Personality(TypedDict):
|
|
|
442
402
|
"""情感模拟对话预设。在 v4.0.0 版本及之后,已被废弃。"""
|
|
443
403
|
tools: list[str] | None
|
|
444
404
|
"""工具列表。None 表示使用所有工具,空列表表示不使用任何工具"""
|
|
405
|
+
skills: list[str] | None
|
|
406
|
+
"""Skills 列表。None 表示使用所有 Skills,空列表表示不使用任何 Skills"""
|
|
445
407
|
|
|
446
408
|
# cache
|
|
447
409
|
_begin_dialogs_processed: list[dict]
|
astrbot/core/db/sqlite.py
CHANGED
|
@@ -52,8 +52,9 @@ class SQLiteDatabase(BaseDatabase):
|
|
|
52
52
|
await conn.execute(text("PRAGMA temp_store=MEMORY"))
|
|
53
53
|
await conn.execute(text("PRAGMA mmap_size=134217728"))
|
|
54
54
|
await conn.execute(text("PRAGMA optimize"))
|
|
55
|
-
# 确保 personas 表有 folder_id
|
|
55
|
+
# 确保 personas 表有 folder_id、sort_order、skills 列(前向兼容)
|
|
56
56
|
await self._ensure_persona_folder_columns(conn)
|
|
57
|
+
await self._ensure_persona_skills_column(conn)
|
|
57
58
|
await conn.commit()
|
|
58
59
|
|
|
59
60
|
async def _ensure_persona_folder_columns(self, conn) -> None:
|
|
@@ -76,6 +77,18 @@ class SQLiteDatabase(BaseDatabase):
|
|
|
76
77
|
text("ALTER TABLE personas ADD COLUMN sort_order INTEGER DEFAULT 0")
|
|
77
78
|
)
|
|
78
79
|
|
|
80
|
+
async def _ensure_persona_skills_column(self, conn) -> None:
|
|
81
|
+
"""确保 personas 表有 skills 列。
|
|
82
|
+
|
|
83
|
+
这是为了支持旧版数据库的平滑升级。新版数据库通过 SQLModel
|
|
84
|
+
的 metadata.create_all 自动创建这些列。
|
|
85
|
+
"""
|
|
86
|
+
result = await conn.execute(text("PRAGMA table_info(personas)"))
|
|
87
|
+
columns = {row[1] for row in result.fetchall()}
|
|
88
|
+
|
|
89
|
+
if "skills" not in columns:
|
|
90
|
+
await conn.execute(text("ALTER TABLE personas ADD COLUMN skills JSON"))
|
|
91
|
+
|
|
79
92
|
# ====
|
|
80
93
|
# Platform Statistics
|
|
81
94
|
# ====
|
|
@@ -564,6 +577,7 @@ class SQLiteDatabase(BaseDatabase):
|
|
|
564
577
|
system_prompt,
|
|
565
578
|
begin_dialogs=None,
|
|
566
579
|
tools=None,
|
|
580
|
+
skills=None,
|
|
567
581
|
folder_id=None,
|
|
568
582
|
sort_order=0,
|
|
569
583
|
):
|
|
@@ -576,6 +590,7 @@ class SQLiteDatabase(BaseDatabase):
|
|
|
576
590
|
system_prompt=system_prompt,
|
|
577
591
|
begin_dialogs=begin_dialogs or [],
|
|
578
592
|
tools=tools,
|
|
593
|
+
skills=skills,
|
|
579
594
|
folder_id=folder_id,
|
|
580
595
|
sort_order=sort_order,
|
|
581
596
|
)
|
|
@@ -606,6 +621,7 @@ class SQLiteDatabase(BaseDatabase):
|
|
|
606
621
|
system_prompt=None,
|
|
607
622
|
begin_dialogs=None,
|
|
608
623
|
tools=NOT_GIVEN,
|
|
624
|
+
skills=NOT_GIVEN,
|
|
609
625
|
):
|
|
610
626
|
"""Update a persona's system prompt or begin dialogs."""
|
|
611
627
|
async with self.get_db() as session:
|
|
@@ -619,6 +635,8 @@ class SQLiteDatabase(BaseDatabase):
|
|
|
619
635
|
values["begin_dialogs"] = begin_dialogs
|
|
620
636
|
if tools is not NOT_GIVEN:
|
|
621
637
|
values["tools"] = tools
|
|
638
|
+
if skills is not NOT_GIVEN:
|
|
639
|
+
values["skills"] = skills
|
|
622
640
|
if not values:
|
|
623
641
|
return None
|
|
624
642
|
query = query.values(**values)
|
|
@@ -567,7 +567,7 @@ class Node(BaseMessageComponent):
|
|
|
567
567
|
async def to_dict(self):
|
|
568
568
|
data_content = []
|
|
569
569
|
for comp in self.content:
|
|
570
|
-
if isinstance(comp,
|
|
570
|
+
if isinstance(comp, Image | Record):
|
|
571
571
|
# For Image and Record segments, we convert them to base64
|
|
572
572
|
bs64 = await comp.convert_to_base64()
|
|
573
573
|
data_content.append(
|
|
@@ -584,7 +584,7 @@ class Node(BaseMessageComponent):
|
|
|
584
584
|
# For File segments, we need to handle the file differently
|
|
585
585
|
d = await comp.to_dict()
|
|
586
586
|
data_content.append(d)
|
|
587
|
-
elif isinstance(comp,
|
|
587
|
+
elif isinstance(comp, Node | Nodes):
|
|
588
588
|
# For Node segments, we recursively convert them to dict
|
|
589
589
|
d = await comp.to_dict()
|
|
590
590
|
data_content.append(d)
|
astrbot/core/persona_mgr.py
CHANGED
|
@@ -10,6 +10,7 @@ DEFAULT_PERSONALITY = Personality(
|
|
|
10
10
|
begin_dialogs=[],
|
|
11
11
|
mood_imitation_dialogs=[],
|
|
12
12
|
tools=None,
|
|
13
|
+
skills=None,
|
|
13
14
|
_begin_dialogs_processed=[],
|
|
14
15
|
_mood_imitation_dialogs_processed="",
|
|
15
16
|
)
|
|
@@ -71,6 +72,7 @@ class PersonaManager:
|
|
|
71
72
|
system_prompt: str | None = None,
|
|
72
73
|
begin_dialogs: list[str] | None = None,
|
|
73
74
|
tools: list[str] | None = None,
|
|
75
|
+
skills: list[str] | None = None,
|
|
74
76
|
):
|
|
75
77
|
"""更新指定 persona 的信息。tools 参数为 None 时表示使用所有工具,空列表表示不使用任何工具"""
|
|
76
78
|
existing_persona = await self.db.get_persona_by_id(persona_id)
|
|
@@ -81,6 +83,7 @@ class PersonaManager:
|
|
|
81
83
|
system_prompt,
|
|
82
84
|
begin_dialogs,
|
|
83
85
|
tools=tools,
|
|
86
|
+
skills=skills,
|
|
84
87
|
)
|
|
85
88
|
if persona:
|
|
86
89
|
for i, p in enumerate(self.personas):
|
|
@@ -239,6 +242,7 @@ class PersonaManager:
|
|
|
239
242
|
system_prompt: str,
|
|
240
243
|
begin_dialogs: list[str] | None = None,
|
|
241
244
|
tools: list[str] | None = None,
|
|
245
|
+
skills: list[str] | None = None,
|
|
242
246
|
folder_id: str | None = None,
|
|
243
247
|
sort_order: int = 0,
|
|
244
248
|
) -> Persona:
|
|
@@ -249,6 +253,7 @@ class PersonaManager:
|
|
|
249
253
|
system_prompt: 系统提示词
|
|
250
254
|
begin_dialogs: 预设对话列表
|
|
251
255
|
tools: 工具列表,None 表示使用所有工具,空列表表示不使用任何工具
|
|
256
|
+
skills: Skills 列表,None 表示使用所有 Skills,空列表表示不使用任何 Skills
|
|
252
257
|
folder_id: 所属文件夹 ID,None 表示根目录
|
|
253
258
|
sort_order: 排序顺序
|
|
254
259
|
"""
|
|
@@ -259,6 +264,7 @@ class PersonaManager:
|
|
|
259
264
|
system_prompt,
|
|
260
265
|
begin_dialogs,
|
|
261
266
|
tools=tools,
|
|
267
|
+
skills=skills,
|
|
262
268
|
folder_id=folder_id,
|
|
263
269
|
sort_order=sort_order,
|
|
264
270
|
)
|
|
@@ -284,6 +290,7 @@ class PersonaManager:
|
|
|
284
290
|
"begin_dialogs": persona.begin_dialogs or [],
|
|
285
291
|
"mood_imitation_dialogs": [], # deprecated
|
|
286
292
|
"tools": persona.tools,
|
|
293
|
+
"skills": persona.skills,
|
|
287
294
|
}
|
|
288
295
|
for persona in self.personas
|
|
289
296
|
]
|
|
@@ -339,6 +346,7 @@ class PersonaManager:
|
|
|
339
346
|
system_prompt=selected_default_persona["prompt"],
|
|
340
347
|
begin_dialogs=selected_default_persona["begin_dialogs"],
|
|
341
348
|
tools=selected_default_persona["tools"] or None,
|
|
349
|
+
skills=selected_default_persona["skills"] or None,
|
|
342
350
|
)
|
|
343
351
|
|
|
344
352
|
return v3_persona_config, personas_v3, selected_default_persona
|
|
@@ -48,7 +48,7 @@ async def call_handler(
|
|
|
48
48
|
# 这里逐步执行异步生成器, 对于每个 yield 返回的 ret, 执行下面的代码
|
|
49
49
|
# 返回值只能是 MessageEventResult 或者 None(无返回值)
|
|
50
50
|
_has_yielded = True
|
|
51
|
-
if isinstance(ret,
|
|
51
|
+
if isinstance(ret, MessageEventResult | CommandResult):
|
|
52
52
|
# 如果返回值是 MessageEventResult, 设置结果并继续
|
|
53
53
|
event.set_result(ret)
|
|
54
54
|
yield
|
|
@@ -65,7 +65,7 @@ async def call_handler(
|
|
|
65
65
|
elif inspect.iscoroutine(ready_to_call):
|
|
66
66
|
# 如果只是一个协程, 直接执行
|
|
67
67
|
ret = await ready_to_call
|
|
68
|
-
if isinstance(ret,
|
|
68
|
+
if isinstance(ret, MessageEventResult | CommandResult):
|
|
69
69
|
event.set_result(ret)
|
|
70
70
|
yield
|
|
71
71
|
else:
|
|
@@ -52,7 +52,7 @@ class PreProcessStage(Stage):
|
|
|
52
52
|
message_chain = event.get_messages()
|
|
53
53
|
|
|
54
54
|
for idx, component in enumerate(message_chain):
|
|
55
|
-
if isinstance(component,
|
|
55
|
+
if isinstance(component, Record | Image) and component.url:
|
|
56
56
|
for mapping in mappings:
|
|
57
57
|
from_, to_ = mapping.split(":")
|
|
58
58
|
from_ = from_.removesuffix("/")
|
|
@@ -46,6 +46,7 @@ from ...utils import (
|
|
|
46
46
|
PYTHON_TOOL,
|
|
47
47
|
SANDBOX_MODE_PROMPT,
|
|
48
48
|
TOOL_CALL_PROMPT,
|
|
49
|
+
TOOL_CALL_PROMPT_SKILLS_LIKE_MODE,
|
|
49
50
|
decoded_blocked,
|
|
50
51
|
retrieve_knowledge_base,
|
|
51
52
|
)
|
|
@@ -62,6 +63,13 @@ class InternalAgentSubStage(Stage):
|
|
|
62
63
|
]
|
|
63
64
|
self.max_step: int = settings.get("max_agent_step", 30)
|
|
64
65
|
self.tool_call_timeout: int = settings.get("tool_call_timeout", 60)
|
|
66
|
+
self.tool_schema_mode: str = settings.get("tool_schema_mode", "full")
|
|
67
|
+
if self.tool_schema_mode not in ("skills_like", "full"):
|
|
68
|
+
logger.warning(
|
|
69
|
+
"Unsupported tool_schema_mode: %s, fallback to skills_like",
|
|
70
|
+
self.tool_schema_mode,
|
|
71
|
+
)
|
|
72
|
+
self.tool_schema_mode = "full"
|
|
65
73
|
if isinstance(self.max_step, bool): # workaround: #2622
|
|
66
74
|
self.max_step = 30
|
|
67
75
|
self.show_tool_use: bool = settings.get("show_tool_use_status", True)
|
|
@@ -517,7 +525,7 @@ class InternalAgentSubStage(Stage):
|
|
|
517
525
|
has_valid_message = bool(event.message_str and event.message_str.strip())
|
|
518
526
|
# 检查是否有图片或其他媒体内容
|
|
519
527
|
has_media_content = any(
|
|
520
|
-
isinstance(comp,
|
|
528
|
+
isinstance(comp, Image | File) for comp in event.message_obj.message
|
|
521
529
|
)
|
|
522
530
|
|
|
523
531
|
if (
|
|
@@ -574,9 +582,7 @@ class InternalAgentSubStage(Stage):
|
|
|
574
582
|
req.extra_user_content_parts.append(
|
|
575
583
|
TextPart(text=f"[Image Attachment: path {image_path}]")
|
|
576
584
|
)
|
|
577
|
-
elif isinstance(comp, File)
|
|
578
|
-
"enable", False
|
|
579
|
-
):
|
|
585
|
+
elif isinstance(comp, File):
|
|
580
586
|
file_path = await comp.get_file()
|
|
581
587
|
file_name = comp.name or os.path.basename(file_path)
|
|
582
588
|
req.extra_user_content_parts.append(
|
|
@@ -603,7 +609,10 @@ class InternalAgentSubStage(Stage):
|
|
|
603
609
|
logger.error(f"Error occurred while applying file extract: {e}")
|
|
604
610
|
|
|
605
611
|
if not req.prompt and not req.image_urls:
|
|
606
|
-
|
|
612
|
+
if not event.get_group_id() and req.extra_user_content_parts:
|
|
613
|
+
req.prompt = "<attachment>"
|
|
614
|
+
else:
|
|
615
|
+
return
|
|
607
616
|
|
|
608
617
|
# call event hook
|
|
609
618
|
if await call_event_hook(event, EventType.OnLLMRequestEvent, req):
|
|
@@ -672,7 +681,12 @@ class InternalAgentSubStage(Stage):
|
|
|
672
681
|
|
|
673
682
|
# 注入基本 prompt
|
|
674
683
|
if req.func_tool and req.func_tool.tools:
|
|
675
|
-
|
|
684
|
+
tool_prompt = (
|
|
685
|
+
TOOL_CALL_PROMPT
|
|
686
|
+
if self.tool_schema_mode == "full"
|
|
687
|
+
else TOOL_CALL_PROMPT_SKILLS_LIKE_MODE
|
|
688
|
+
)
|
|
689
|
+
req.system_prompt += f"\n{tool_prompt}\n"
|
|
676
690
|
|
|
677
691
|
action_type = event.get_extra("action_type")
|
|
678
692
|
if action_type == "live":
|
|
@@ -693,6 +707,7 @@ class InternalAgentSubStage(Stage):
|
|
|
693
707
|
llm_compress_provider=self._get_compress_provider(),
|
|
694
708
|
truncate_turns=self.dequeue_context_length,
|
|
695
709
|
enforce_max_turns=self.max_context_length,
|
|
710
|
+
tool_schema_mode=self.tool_schema_mode,
|
|
696
711
|
)
|
|
697
712
|
|
|
698
713
|
# 检测 Live Mode
|
|
@@ -7,10 +7,11 @@ from astrbot.api import logger, sp
|
|
|
7
7
|
from astrbot.core.agent.run_context import ContextWrapper
|
|
8
8
|
from astrbot.core.agent.tool import FunctionTool, ToolExecResult
|
|
9
9
|
from astrbot.core.astr_agent_context import AstrAgentContext
|
|
10
|
-
from astrbot.core.
|
|
10
|
+
from astrbot.core.computer.tools import (
|
|
11
11
|
ExecuteShellTool,
|
|
12
12
|
FileDownloadTool,
|
|
13
13
|
FileUploadTool,
|
|
14
|
+
LocalPythonTool,
|
|
14
15
|
PythonTool,
|
|
15
16
|
)
|
|
16
17
|
from astrbot.core.star.context import Context
|
|
@@ -39,11 +40,23 @@ SANDBOX_MODE_PROMPT = (
|
|
|
39
40
|
|
|
40
41
|
TOOL_CALL_PROMPT = (
|
|
41
42
|
"You MUST NOT return an empty response, especially after invoking a tool."
|
|
42
|
-
"Before calling any tool, provide a brief explanatory message to the user stating the purpose of the tool call."
|
|
43
|
-
"
|
|
44
|
-
"
|
|
43
|
+
" Before calling any tool, provide a brief explanatory message to the user stating the purpose of the tool call."
|
|
44
|
+
" Use the provided tool schema to format arguments and do not guess parameters that are not defined."
|
|
45
|
+
" After the tool call is completed, you must briefly summarize the results returned by the tool for the user."
|
|
46
|
+
" Keep the role-play and style consistent throughout the conversation."
|
|
45
47
|
)
|
|
46
48
|
|
|
49
|
+
TOOL_CALL_PROMPT_SKILLS_LIKE_MODE = (
|
|
50
|
+
"You MUST NOT return an empty response, especially after invoking a tool."
|
|
51
|
+
" Before calling any tool, provide a brief explanatory message to the user stating the purpose of the tool call."
|
|
52
|
+
" Tool schemas are provided in two stages: first only name and description; "
|
|
53
|
+
"if you decide to use a tool, the full parameter schema will be provided in "
|
|
54
|
+
"a follow-up step. Do not guess arguments before you see the schema."
|
|
55
|
+
" After the tool call is completed, you must briefly summarize the results returned by the tool for the user."
|
|
56
|
+
" Keep the role-play and style consistent throughout the conversation."
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
|
|
47
60
|
CHATUI_SPECIAL_DEFAULT_PERSONA_PROMPT = (
|
|
48
61
|
"You are a calm, patient friend with a systems-oriented way of thinking.\n"
|
|
49
62
|
"When someone expresses strong emotional needs, you begin by offering a concise, grounding response "
|
|
@@ -194,7 +207,9 @@ async def retrieve_knowledge_base(
|
|
|
194
207
|
KNOWLEDGE_BASE_QUERY_TOOL = KnowledgeBaseQueryTool()
|
|
195
208
|
|
|
196
209
|
EXECUTE_SHELL_TOOL = ExecuteShellTool()
|
|
210
|
+
LOCAL_EXECUTE_SHELL_TOOL = ExecuteShellTool(is_local=True)
|
|
197
211
|
PYTHON_TOOL = PythonTool()
|
|
212
|
+
LOCAL_PYTHON_TOOL = LocalPythonTool()
|
|
198
213
|
FILE_UPLOAD_TOOL = FileUploadTool()
|
|
199
214
|
FILE_DOWNLOAD_TOOL = FileDownloadTool()
|
|
200
215
|
|
|
@@ -82,7 +82,7 @@ class PipelineScheduler:
|
|
|
82
82
|
await self._process_stages(event)
|
|
83
83
|
|
|
84
84
|
# 如果没有发送操作, 则发送一个空消息, 以便于后续的处理
|
|
85
|
-
if isinstance(event,
|
|
85
|
+
if isinstance(event, WebChatMessageEvent | WecomAIBotMessageEvent):
|
|
86
86
|
await event.send(None)
|
|
87
87
|
|
|
88
88
|
logger.debug("pipeline 执行完毕。")
|
|
@@ -33,7 +33,7 @@ class AiocqhttpMessageEvent(AstrMessageEvent):
|
|
|
33
33
|
@staticmethod
|
|
34
34
|
async def _from_segment_to_dict(segment: BaseMessageComponent) -> dict:
|
|
35
35
|
"""修复部分字段"""
|
|
36
|
-
if isinstance(segment,
|
|
36
|
+
if isinstance(segment, Image | Record):
|
|
37
37
|
# For Image and Record segments, we convert them to base64
|
|
38
38
|
bs64 = await segment.convert_to_base64()
|
|
39
39
|
return {
|
|
@@ -110,7 +110,7 @@ class AiocqhttpMessageEvent(AstrMessageEvent):
|
|
|
110
110
|
"""
|
|
111
111
|
# 转发消息、文件消息不能和普通消息混在一起发送
|
|
112
112
|
send_one_by_one = any(
|
|
113
|
-
isinstance(seg,
|
|
113
|
+
isinstance(seg, Node | Nodes | File) for seg in message_chain.chain
|
|
114
114
|
)
|
|
115
115
|
if not send_one_by_one:
|
|
116
116
|
ret = await cls._parse_onebot_json(message_chain)
|
|
@@ -119,7 +119,7 @@ class AiocqhttpMessageEvent(AstrMessageEvent):
|
|
|
119
119
|
await cls._dispatch_send(bot, event, is_group, session_id, ret)
|
|
120
120
|
return
|
|
121
121
|
for seg in message_chain.chain:
|
|
122
|
-
if isinstance(seg,
|
|
122
|
+
if isinstance(seg, Node | Nodes):
|
|
123
123
|
# 合并转发消息
|
|
124
124
|
if isinstance(seg, Node):
|
|
125
125
|
nodes = Nodes([seg])
|
|
@@ -90,12 +90,10 @@ class QQOfficialMessageEvent(AstrMessageEvent):
|
|
|
90
90
|
|
|
91
91
|
if not isinstance(
|
|
92
92
|
source,
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
botpy.message.C2CMessage,
|
|
98
|
-
),
|
|
93
|
+
botpy.message.Message
|
|
94
|
+
| botpy.message.GroupMessage
|
|
95
|
+
| botpy.message.DirectMessage
|
|
96
|
+
| botpy.message.C2CMessage,
|
|
99
97
|
):
|
|
100
98
|
logger.warning(f"[QQOfficial] 不支持的消息源类型: {type(source)}")
|
|
101
99
|
return None
|
|
@@ -120,7 +118,7 @@ class QQOfficialMessageEvent(AstrMessageEvent):
|
|
|
120
118
|
"msg_id": self.message_obj.message_id,
|
|
121
119
|
}
|
|
122
120
|
|
|
123
|
-
if not isinstance(source,
|
|
121
|
+
if not isinstance(source, botpy.message.Message | botpy.message.DirectMessage):
|
|
124
122
|
payload["msg_seq"] = random.randint(1, 10000)
|
|
125
123
|
|
|
126
124
|
ret = None
|
astrbot/core/provider/manager.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
import copy
|
|
3
|
+
import os
|
|
3
4
|
import traceback
|
|
4
5
|
from typing import Protocol, runtime_checkable
|
|
5
6
|
|
|
@@ -406,10 +407,40 @@ class ProviderManager:
|
|
|
406
407
|
pc = merged_config
|
|
407
408
|
return pc
|
|
408
409
|
|
|
410
|
+
def _resolve_env_key_list(self, provider_config: dict) -> dict:
|
|
411
|
+
keys = provider_config.get("key", [])
|
|
412
|
+
if not isinstance(keys, list):
|
|
413
|
+
return provider_config
|
|
414
|
+
resolved_keys = []
|
|
415
|
+
for idx, key in enumerate(keys):
|
|
416
|
+
if isinstance(key, str) and key.startswith("$"):
|
|
417
|
+
env_key = key[1:]
|
|
418
|
+
if env_key.startswith("{") and env_key.endswith("}"):
|
|
419
|
+
env_key = env_key[1:-1]
|
|
420
|
+
if env_key:
|
|
421
|
+
env_val = os.getenv(env_key)
|
|
422
|
+
if env_val is None:
|
|
423
|
+
provider_id = provider_config.get("id")
|
|
424
|
+
logger.warning(
|
|
425
|
+
f"Provider {provider_id} 配置项 key[{idx}] 使用环境变量 {env_key} 但未设置。",
|
|
426
|
+
)
|
|
427
|
+
resolved_keys.append("")
|
|
428
|
+
else:
|
|
429
|
+
resolved_keys.append(env_val)
|
|
430
|
+
else:
|
|
431
|
+
resolved_keys.append(key)
|
|
432
|
+
else:
|
|
433
|
+
resolved_keys.append(key)
|
|
434
|
+
provider_config["key"] = resolved_keys
|
|
435
|
+
return provider_config
|
|
436
|
+
|
|
409
437
|
async def load_provider(self, provider_config: dict):
|
|
410
438
|
# 如果 provider_source_id 存在且不为空,则从 provider_sources 中找到对应的配置并合并
|
|
411
439
|
provider_config = self.get_merged_provider_config(provider_config)
|
|
412
440
|
|
|
441
|
+
if provider_config.get("provider_type", "") == "chat_completion":
|
|
442
|
+
provider_config = self._resolve_env_key_list(provider_config)
|
|
443
|
+
|
|
413
444
|
if not provider_config["enable"]:
|
|
414
445
|
logger.info(f"Provider {provider_config['id']} is disabled, skipping")
|
|
415
446
|
return
|
|
@@ -382,15 +382,18 @@ class ProviderGoogleGenAI(Provider):
|
|
|
382
382
|
append_or_extend(gemini_contents, parts, types.ModelContent)
|
|
383
383
|
|
|
384
384
|
elif role == "tool" and not native_tool_enabled:
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
385
|
+
func_name = message.get("name", message["tool_call_id"])
|
|
386
|
+
part = types.Part.from_function_response(
|
|
387
|
+
name=func_name,
|
|
388
|
+
response={
|
|
389
|
+
"name": func_name,
|
|
390
|
+
"content": message["content"],
|
|
391
|
+
},
|
|
392
|
+
)
|
|
393
|
+
if part.function_response:
|
|
394
|
+
part.function_response.id = message["tool_call_id"]
|
|
395
|
+
|
|
396
|
+
parts = [part]
|
|
394
397
|
append_or_extend(gemini_contents, parts, types.UserContent)
|
|
395
398
|
|
|
396
399
|
if gemini_contents and isinstance(gemini_contents[0], types.ModelContent):
|