AstrBot 4.9.2__py3-none-any.whl → 4.10.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.
- astrbot/cli/__init__.py +1 -1
- astrbot/core/agent/message.py +6 -4
- astrbot/core/agent/response.py +22 -1
- astrbot/core/agent/run_context.py +1 -1
- astrbot/core/agent/runners/tool_loop_agent_runner.py +99 -20
- astrbot/core/astr_agent_context.py +3 -1
- astrbot/core/astr_agent_run_util.py +42 -3
- astrbot/core/astr_agent_tool_exec.py +34 -4
- astrbot/core/config/default.py +127 -184
- astrbot/core/core_lifecycle.py +3 -0
- astrbot/core/db/__init__.py +72 -0
- astrbot/core/db/po.py +59 -0
- astrbot/core/db/sqlite.py +240 -0
- astrbot/core/message/components.py +4 -5
- astrbot/core/pipeline/process_stage/method/agent_sub_stages/internal.py +6 -1
- astrbot/core/pipeline/respond/stage.py +1 -1
- astrbot/core/platform/sources/telegram/tg_event.py +9 -0
- astrbot/core/platform/sources/webchat/webchat_event.py +22 -18
- astrbot/core/provider/entities.py +41 -0
- astrbot/core/provider/manager.py +203 -93
- astrbot/core/provider/sources/anthropic_source.py +55 -11
- astrbot/core/provider/sources/gemini_source.py +84 -33
- astrbot/core/provider/sources/openai_source.py +21 -6
- astrbot/core/star/command_management.py +449 -0
- astrbot/core/star/context.py +4 -0
- astrbot/core/star/filter/command.py +1 -0
- astrbot/core/star/filter/command_group.py +1 -0
- astrbot/core/star/star_handler.py +4 -0
- astrbot/core/star/star_manager.py +2 -0
- astrbot/core/utils/llm_metadata.py +63 -0
- astrbot/core/utils/migra_helper.py +93 -0
- astrbot/dashboard/routes/__init__.py +2 -0
- astrbot/dashboard/routes/chat.py +56 -13
- astrbot/dashboard/routes/command.py +82 -0
- astrbot/dashboard/routes/config.py +291 -33
- astrbot/dashboard/routes/stat.py +96 -0
- astrbot/dashboard/routes/tools.py +20 -4
- astrbot/dashboard/server.py +1 -0
- {astrbot-4.9.2.dist-info → astrbot-4.10.0.dist-info}/METADATA +2 -2
- {astrbot-4.9.2.dist-info → astrbot-4.10.0.dist-info}/RECORD +43 -40
- {astrbot-4.9.2.dist-info → astrbot-4.10.0.dist-info}/WHEEL +0 -0
- {astrbot-4.9.2.dist-info → astrbot-4.10.0.dist-info}/entry_points.txt +0 -0
- {astrbot-4.9.2.dist-info → astrbot-4.10.0.dist-info}/licenses/LICENSE +0 -0
astrbot/core/db/po.py
CHANGED
|
@@ -234,6 +234,65 @@ class Attachment(SQLModel, table=True):
|
|
|
234
234
|
)
|
|
235
235
|
|
|
236
236
|
|
|
237
|
+
class CommandConfig(SQLModel, table=True):
|
|
238
|
+
"""Per-command configuration overrides for dashboard management."""
|
|
239
|
+
|
|
240
|
+
__tablename__ = "command_configs" # type: ignore
|
|
241
|
+
|
|
242
|
+
handler_full_name: str = Field(
|
|
243
|
+
primary_key=True,
|
|
244
|
+
max_length=512,
|
|
245
|
+
)
|
|
246
|
+
plugin_name: str = Field(nullable=False, max_length=255)
|
|
247
|
+
module_path: str = Field(nullable=False, max_length=255)
|
|
248
|
+
original_command: str = Field(nullable=False, max_length=255)
|
|
249
|
+
resolved_command: str | None = Field(default=None, max_length=255)
|
|
250
|
+
enabled: bool = Field(default=True, nullable=False)
|
|
251
|
+
keep_original_alias: bool = Field(default=False, nullable=False)
|
|
252
|
+
conflict_key: str | None = Field(default=None, max_length=255)
|
|
253
|
+
resolution_strategy: str | None = Field(default=None, max_length=64)
|
|
254
|
+
note: str | None = Field(default=None, sa_type=Text)
|
|
255
|
+
extra_data: dict | None = Field(default=None, sa_type=JSON)
|
|
256
|
+
auto_managed: bool = Field(default=False, nullable=False)
|
|
257
|
+
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
|
258
|
+
updated_at: datetime = Field(
|
|
259
|
+
default_factory=lambda: datetime.now(timezone.utc),
|
|
260
|
+
sa_column_kwargs={"onupdate": datetime.now(timezone.utc)},
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
class CommandConflict(SQLModel, table=True):
|
|
265
|
+
"""Conflict tracking for duplicated command names."""
|
|
266
|
+
|
|
267
|
+
__tablename__ = "command_conflicts" # type: ignore
|
|
268
|
+
|
|
269
|
+
id: int | None = Field(
|
|
270
|
+
default=None, primary_key=True, sa_column_kwargs={"autoincrement": True}
|
|
271
|
+
)
|
|
272
|
+
conflict_key: str = Field(nullable=False, max_length=255)
|
|
273
|
+
handler_full_name: str = Field(nullable=False, max_length=512)
|
|
274
|
+
plugin_name: str = Field(nullable=False, max_length=255)
|
|
275
|
+
status: str = Field(default="pending", max_length=32)
|
|
276
|
+
resolution: str | None = Field(default=None, max_length=64)
|
|
277
|
+
resolved_command: str | None = Field(default=None, max_length=255)
|
|
278
|
+
note: str | None = Field(default=None, sa_type=Text)
|
|
279
|
+
extra_data: dict | None = Field(default=None, sa_type=JSON)
|
|
280
|
+
auto_generated: bool = Field(default=False, nullable=False)
|
|
281
|
+
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
|
282
|
+
updated_at: datetime = Field(
|
|
283
|
+
default_factory=lambda: datetime.now(timezone.utc),
|
|
284
|
+
sa_column_kwargs={"onupdate": datetime.now(timezone.utc)},
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
__table_args__ = (
|
|
288
|
+
UniqueConstraint(
|
|
289
|
+
"conflict_key",
|
|
290
|
+
"handler_full_name",
|
|
291
|
+
name="uix_conflict_handler",
|
|
292
|
+
),
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
|
|
237
296
|
@dataclass
|
|
238
297
|
class Conversation:
|
|
239
298
|
"""LLM 对话类
|
astrbot/core/db/sqlite.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
import threading
|
|
3
3
|
import typing as T
|
|
4
|
+
from collections.abc import Awaitable, Callable
|
|
4
5
|
from datetime import datetime, timedelta, timezone
|
|
5
6
|
|
|
6
7
|
from sqlalchemy import CursorResult
|
|
@@ -10,6 +11,8 @@ from sqlmodel import col, delete, desc, func, or_, select, text, update
|
|
|
10
11
|
from astrbot.core.db import BaseDatabase
|
|
11
12
|
from astrbot.core.db.po import (
|
|
12
13
|
Attachment,
|
|
14
|
+
CommandConfig,
|
|
15
|
+
CommandConflict,
|
|
13
16
|
ConversationV2,
|
|
14
17
|
Persona,
|
|
15
18
|
PlatformMessageHistory,
|
|
@@ -26,6 +29,7 @@ from astrbot.core.db.po import (
|
|
|
26
29
|
)
|
|
27
30
|
|
|
28
31
|
NOT_GIVEN = T.TypeVar("NOT_GIVEN")
|
|
32
|
+
TxResult = T.TypeVar("TxResult")
|
|
29
33
|
|
|
30
34
|
|
|
31
35
|
class SQLiteDatabase(BaseDatabase):
|
|
@@ -670,6 +674,242 @@ class SQLiteDatabase(BaseDatabase):
|
|
|
670
674
|
)
|
|
671
675
|
await session.commit()
|
|
672
676
|
|
|
677
|
+
# ====
|
|
678
|
+
# Command Configuration & Conflict Tracking
|
|
679
|
+
# ====
|
|
680
|
+
|
|
681
|
+
async def _run_in_tx(
|
|
682
|
+
self,
|
|
683
|
+
fn: Callable[[AsyncSession], Awaitable[TxResult]],
|
|
684
|
+
) -> TxResult:
|
|
685
|
+
async with self.get_db() as session:
|
|
686
|
+
session: AsyncSession
|
|
687
|
+
async with session.begin():
|
|
688
|
+
return await fn(session)
|
|
689
|
+
|
|
690
|
+
@staticmethod
|
|
691
|
+
def _apply_updates(model, **updates) -> None:
|
|
692
|
+
for field, value in updates.items():
|
|
693
|
+
if value is not None:
|
|
694
|
+
setattr(model, field, value)
|
|
695
|
+
|
|
696
|
+
@staticmethod
|
|
697
|
+
def _new_command_config(
|
|
698
|
+
handler_full_name: str,
|
|
699
|
+
plugin_name: str,
|
|
700
|
+
module_path: str,
|
|
701
|
+
original_command: str,
|
|
702
|
+
*,
|
|
703
|
+
resolved_command: str | None = None,
|
|
704
|
+
enabled: bool | None = None,
|
|
705
|
+
keep_original_alias: bool | None = None,
|
|
706
|
+
conflict_key: str | None = None,
|
|
707
|
+
resolution_strategy: str | None = None,
|
|
708
|
+
note: str | None = None,
|
|
709
|
+
extra_data: dict | None = None,
|
|
710
|
+
auto_managed: bool | None = None,
|
|
711
|
+
) -> CommandConfig:
|
|
712
|
+
return CommandConfig(
|
|
713
|
+
handler_full_name=handler_full_name,
|
|
714
|
+
plugin_name=plugin_name,
|
|
715
|
+
module_path=module_path,
|
|
716
|
+
original_command=original_command,
|
|
717
|
+
resolved_command=resolved_command,
|
|
718
|
+
enabled=True if enabled is None else enabled,
|
|
719
|
+
keep_original_alias=False
|
|
720
|
+
if keep_original_alias is None
|
|
721
|
+
else keep_original_alias,
|
|
722
|
+
conflict_key=conflict_key or original_command,
|
|
723
|
+
resolution_strategy=resolution_strategy,
|
|
724
|
+
note=note,
|
|
725
|
+
extra_data=extra_data,
|
|
726
|
+
auto_managed=bool(auto_managed),
|
|
727
|
+
)
|
|
728
|
+
|
|
729
|
+
@staticmethod
|
|
730
|
+
def _new_command_conflict(
|
|
731
|
+
conflict_key: str,
|
|
732
|
+
handler_full_name: str,
|
|
733
|
+
plugin_name: str,
|
|
734
|
+
*,
|
|
735
|
+
status: str | None = None,
|
|
736
|
+
resolution: str | None = None,
|
|
737
|
+
resolved_command: str | None = None,
|
|
738
|
+
note: str | None = None,
|
|
739
|
+
extra_data: dict | None = None,
|
|
740
|
+
auto_generated: bool | None = None,
|
|
741
|
+
) -> CommandConflict:
|
|
742
|
+
return CommandConflict(
|
|
743
|
+
conflict_key=conflict_key,
|
|
744
|
+
handler_full_name=handler_full_name,
|
|
745
|
+
plugin_name=plugin_name,
|
|
746
|
+
status=status or "pending",
|
|
747
|
+
resolution=resolution,
|
|
748
|
+
resolved_command=resolved_command,
|
|
749
|
+
note=note,
|
|
750
|
+
extra_data=extra_data,
|
|
751
|
+
auto_generated=bool(auto_generated),
|
|
752
|
+
)
|
|
753
|
+
|
|
754
|
+
async def get_command_configs(self) -> list[CommandConfig]:
|
|
755
|
+
async with self.get_db() as session:
|
|
756
|
+
session: AsyncSession
|
|
757
|
+
result = await session.execute(select(CommandConfig))
|
|
758
|
+
return list(result.scalars().all())
|
|
759
|
+
|
|
760
|
+
async def get_command_config(
|
|
761
|
+
self,
|
|
762
|
+
handler_full_name: str,
|
|
763
|
+
) -> CommandConfig | None:
|
|
764
|
+
async with self.get_db() as session:
|
|
765
|
+
session: AsyncSession
|
|
766
|
+
return await session.get(CommandConfig, handler_full_name)
|
|
767
|
+
|
|
768
|
+
async def upsert_command_config(
|
|
769
|
+
self,
|
|
770
|
+
handler_full_name: str,
|
|
771
|
+
plugin_name: str,
|
|
772
|
+
module_path: str,
|
|
773
|
+
original_command: str,
|
|
774
|
+
*,
|
|
775
|
+
resolved_command: str | None = None,
|
|
776
|
+
enabled: bool | None = None,
|
|
777
|
+
keep_original_alias: bool | None = None,
|
|
778
|
+
conflict_key: str | None = None,
|
|
779
|
+
resolution_strategy: str | None = None,
|
|
780
|
+
note: str | None = None,
|
|
781
|
+
extra_data: dict | None = None,
|
|
782
|
+
auto_managed: bool | None = None,
|
|
783
|
+
) -> CommandConfig:
|
|
784
|
+
async def _op(session: AsyncSession) -> CommandConfig:
|
|
785
|
+
config = await session.get(CommandConfig, handler_full_name)
|
|
786
|
+
if not config:
|
|
787
|
+
config = self._new_command_config(
|
|
788
|
+
handler_full_name,
|
|
789
|
+
plugin_name,
|
|
790
|
+
module_path,
|
|
791
|
+
original_command,
|
|
792
|
+
resolved_command=resolved_command,
|
|
793
|
+
enabled=enabled,
|
|
794
|
+
keep_original_alias=keep_original_alias,
|
|
795
|
+
conflict_key=conflict_key,
|
|
796
|
+
resolution_strategy=resolution_strategy,
|
|
797
|
+
note=note,
|
|
798
|
+
extra_data=extra_data,
|
|
799
|
+
auto_managed=auto_managed,
|
|
800
|
+
)
|
|
801
|
+
session.add(config)
|
|
802
|
+
else:
|
|
803
|
+
self._apply_updates(
|
|
804
|
+
config,
|
|
805
|
+
plugin_name=plugin_name,
|
|
806
|
+
module_path=module_path,
|
|
807
|
+
original_command=original_command,
|
|
808
|
+
resolved_command=resolved_command,
|
|
809
|
+
enabled=enabled,
|
|
810
|
+
keep_original_alias=keep_original_alias,
|
|
811
|
+
conflict_key=conflict_key,
|
|
812
|
+
resolution_strategy=resolution_strategy,
|
|
813
|
+
note=note,
|
|
814
|
+
extra_data=extra_data,
|
|
815
|
+
auto_managed=auto_managed,
|
|
816
|
+
)
|
|
817
|
+
await session.flush()
|
|
818
|
+
await session.refresh(config)
|
|
819
|
+
return config
|
|
820
|
+
|
|
821
|
+
return await self._run_in_tx(_op)
|
|
822
|
+
|
|
823
|
+
async def delete_command_config(self, handler_full_name: str) -> None:
|
|
824
|
+
await self.delete_command_configs([handler_full_name])
|
|
825
|
+
|
|
826
|
+
async def delete_command_configs(self, handler_full_names: list[str]) -> None:
|
|
827
|
+
if not handler_full_names:
|
|
828
|
+
return
|
|
829
|
+
|
|
830
|
+
async def _op(session: AsyncSession) -> None:
|
|
831
|
+
await session.execute(
|
|
832
|
+
delete(CommandConfig).where(
|
|
833
|
+
col(CommandConfig.handler_full_name).in_(handler_full_names),
|
|
834
|
+
),
|
|
835
|
+
)
|
|
836
|
+
|
|
837
|
+
await self._run_in_tx(_op)
|
|
838
|
+
|
|
839
|
+
async def list_command_conflicts(
|
|
840
|
+
self,
|
|
841
|
+
status: str | None = None,
|
|
842
|
+
) -> list[CommandConflict]:
|
|
843
|
+
async with self.get_db() as session:
|
|
844
|
+
session: AsyncSession
|
|
845
|
+
query = select(CommandConflict)
|
|
846
|
+
if status:
|
|
847
|
+
query = query.where(CommandConflict.status == status)
|
|
848
|
+
result = await session.execute(query)
|
|
849
|
+
return list(result.scalars().all())
|
|
850
|
+
|
|
851
|
+
async def upsert_command_conflict(
|
|
852
|
+
self,
|
|
853
|
+
conflict_key: str,
|
|
854
|
+
handler_full_name: str,
|
|
855
|
+
plugin_name: str,
|
|
856
|
+
*,
|
|
857
|
+
status: str | None = None,
|
|
858
|
+
resolution: str | None = None,
|
|
859
|
+
resolved_command: str | None = None,
|
|
860
|
+
note: str | None = None,
|
|
861
|
+
extra_data: dict | None = None,
|
|
862
|
+
auto_generated: bool | None = None,
|
|
863
|
+
) -> CommandConflict:
|
|
864
|
+
async def _op(session: AsyncSession) -> CommandConflict:
|
|
865
|
+
result = await session.execute(
|
|
866
|
+
select(CommandConflict).where(
|
|
867
|
+
CommandConflict.conflict_key == conflict_key,
|
|
868
|
+
CommandConflict.handler_full_name == handler_full_name,
|
|
869
|
+
),
|
|
870
|
+
)
|
|
871
|
+
record = result.scalar_one_or_none()
|
|
872
|
+
if not record:
|
|
873
|
+
record = self._new_command_conflict(
|
|
874
|
+
conflict_key,
|
|
875
|
+
handler_full_name,
|
|
876
|
+
plugin_name,
|
|
877
|
+
status=status,
|
|
878
|
+
resolution=resolution,
|
|
879
|
+
resolved_command=resolved_command,
|
|
880
|
+
note=note,
|
|
881
|
+
extra_data=extra_data,
|
|
882
|
+
auto_generated=auto_generated,
|
|
883
|
+
)
|
|
884
|
+
session.add(record)
|
|
885
|
+
else:
|
|
886
|
+
self._apply_updates(
|
|
887
|
+
record,
|
|
888
|
+
plugin_name=plugin_name,
|
|
889
|
+
status=status,
|
|
890
|
+
resolution=resolution,
|
|
891
|
+
resolved_command=resolved_command,
|
|
892
|
+
note=note,
|
|
893
|
+
extra_data=extra_data,
|
|
894
|
+
auto_generated=auto_generated,
|
|
895
|
+
)
|
|
896
|
+
await session.flush()
|
|
897
|
+
await session.refresh(record)
|
|
898
|
+
return record
|
|
899
|
+
|
|
900
|
+
return await self._run_in_tx(_op)
|
|
901
|
+
|
|
902
|
+
async def delete_command_conflicts(self, ids: list[int]) -> None:
|
|
903
|
+
if not ids:
|
|
904
|
+
return
|
|
905
|
+
|
|
906
|
+
async def _op(session: AsyncSession) -> None:
|
|
907
|
+
await session.execute(
|
|
908
|
+
delete(CommandConflict).where(col(CommandConflict.id).in_(ids)),
|
|
909
|
+
)
|
|
910
|
+
|
|
911
|
+
await self._run_in_tx(_op)
|
|
912
|
+
|
|
673
913
|
# ====
|
|
674
914
|
# Deprecated Methods
|
|
675
915
|
# ====
|
|
@@ -629,12 +629,11 @@ class Nodes(BaseMessageComponent):
|
|
|
629
629
|
|
|
630
630
|
class Json(BaseMessageComponent):
|
|
631
631
|
type = ComponentType.Json
|
|
632
|
-
data:
|
|
633
|
-
resid: int | None = 0
|
|
632
|
+
data: dict
|
|
634
633
|
|
|
635
|
-
def __init__(self, data, **_):
|
|
636
|
-
if isinstance(data,
|
|
637
|
-
data = json.
|
|
634
|
+
def __init__(self, data: str | dict, **_):
|
|
635
|
+
if isinstance(data, str):
|
|
636
|
+
data = json.loads(data)
|
|
638
637
|
super().__init__(data=data, **_)
|
|
639
638
|
|
|
640
639
|
|
|
@@ -321,7 +321,12 @@ class InternalAgentSubStage(Stage):
|
|
|
321
321
|
elif isinstance(req.tool_calls_result, list):
|
|
322
322
|
for tcr in req.tool_calls_result:
|
|
323
323
|
messages.extend(tcr.to_openai_messages())
|
|
324
|
-
messages.append(
|
|
324
|
+
messages.append(
|
|
325
|
+
{
|
|
326
|
+
"role": "assistant",
|
|
327
|
+
"content": llm_response.completion_text or "*No response*",
|
|
328
|
+
}
|
|
329
|
+
)
|
|
325
330
|
messages = list(filter(lambda item: "_no_save" not in item, messages))
|
|
326
331
|
await self.conv_manager.update_conversation(
|
|
327
332
|
event.unified_msg_origin,
|
|
@@ -119,7 +119,7 @@ class RespondStage(Stage):
|
|
|
119
119
|
|
|
120
120
|
if (result := event.get_result()) is None:
|
|
121
121
|
return False
|
|
122
|
-
if self.only_llm_result and result.is_llm_result():
|
|
122
|
+
if self.only_llm_result and not result.is_llm_result():
|
|
123
123
|
return False
|
|
124
124
|
|
|
125
125
|
if event.get_platform_name() in [
|
|
@@ -200,6 +200,15 @@ class TelegramPlatformEvent(AstrMessageEvent):
|
|
|
200
200
|
if isinstance(chain, MessageChain):
|
|
201
201
|
if chain.type == "break":
|
|
202
202
|
# 分割符
|
|
203
|
+
if message_id:
|
|
204
|
+
try:
|
|
205
|
+
await self.client.edit_message_text(
|
|
206
|
+
text=delta,
|
|
207
|
+
chat_id=payload["chat_id"],
|
|
208
|
+
message_id=message_id,
|
|
209
|
+
)
|
|
210
|
+
except Exception as e:
|
|
211
|
+
logger.warning(f"编辑消息失败(streaming-break): {e!s}")
|
|
203
212
|
message_id = None # 重置消息 ID
|
|
204
213
|
delta = "" # 重置 delta
|
|
205
214
|
continue
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import base64
|
|
2
|
+
import json
|
|
2
3
|
import os
|
|
3
4
|
import shutil
|
|
4
5
|
import uuid
|
|
5
6
|
|
|
6
7
|
from astrbot.api import logger
|
|
7
8
|
from astrbot.api.event import AstrMessageEvent, MessageChain
|
|
8
|
-
from astrbot.api.message_components import File, Image, Plain, Record
|
|
9
|
+
from astrbot.api.message_components import File, Image, Json, Plain, Record
|
|
9
10
|
from astrbot.core.utils.astrbot_path import get_astrbot_data_path
|
|
10
11
|
|
|
11
12
|
from .webchat_queue_mgr import webchat_queue_mgr
|
|
@@ -41,12 +42,20 @@ class WebChatMessageEvent(AstrMessageEvent):
|
|
|
41
42
|
await web_chat_back_queue.put(
|
|
42
43
|
{
|
|
43
44
|
"type": "plain",
|
|
44
|
-
"cid": cid,
|
|
45
45
|
"data": data,
|
|
46
46
|
"streaming": streaming,
|
|
47
47
|
"chain_type": message.type,
|
|
48
48
|
},
|
|
49
49
|
)
|
|
50
|
+
elif isinstance(comp, Json):
|
|
51
|
+
await web_chat_back_queue.put(
|
|
52
|
+
{
|
|
53
|
+
"type": "plain",
|
|
54
|
+
"data": json.dumps(comp.data, ensure_ascii=False),
|
|
55
|
+
"streaming": streaming,
|
|
56
|
+
"chain_type": message.type,
|
|
57
|
+
},
|
|
58
|
+
)
|
|
50
59
|
elif isinstance(comp, Image):
|
|
51
60
|
# save image to local
|
|
52
61
|
filename = f"{str(uuid.uuid4())}.jpg"
|
|
@@ -58,7 +67,6 @@ class WebChatMessageEvent(AstrMessageEvent):
|
|
|
58
67
|
await web_chat_back_queue.put(
|
|
59
68
|
{
|
|
60
69
|
"type": "image",
|
|
61
|
-
"cid": cid,
|
|
62
70
|
"data": data,
|
|
63
71
|
"streaming": streaming,
|
|
64
72
|
},
|
|
@@ -74,7 +82,6 @@ class WebChatMessageEvent(AstrMessageEvent):
|
|
|
74
82
|
await web_chat_back_queue.put(
|
|
75
83
|
{
|
|
76
84
|
"type": "record",
|
|
77
|
-
"cid": cid,
|
|
78
85
|
"data": data,
|
|
79
86
|
"streaming": streaming,
|
|
80
87
|
},
|
|
@@ -91,7 +98,6 @@ class WebChatMessageEvent(AstrMessageEvent):
|
|
|
91
98
|
await web_chat_back_queue.put(
|
|
92
99
|
{
|
|
93
100
|
"type": "file",
|
|
94
|
-
"cid": cid,
|
|
95
101
|
"data": data,
|
|
96
102
|
"streaming": streaming,
|
|
97
103
|
},
|
|
@@ -111,18 +117,17 @@ class WebChatMessageEvent(AstrMessageEvent):
|
|
|
111
117
|
cid = self.session_id.split("!")[-1]
|
|
112
118
|
web_chat_back_queue = webchat_queue_mgr.get_or_create_back_queue(cid)
|
|
113
119
|
async for chain in generator:
|
|
114
|
-
if chain.type == "break" and final_data:
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
continue
|
|
120
|
+
# if chain.type == "break" and final_data:
|
|
121
|
+
# # 分割符
|
|
122
|
+
# await web_chat_back_queue.put(
|
|
123
|
+
# {
|
|
124
|
+
# "type": "break", # break means a segment end
|
|
125
|
+
# "data": final_data,
|
|
126
|
+
# "streaming": True,
|
|
127
|
+
# },
|
|
128
|
+
# )
|
|
129
|
+
# final_data = ""
|
|
130
|
+
# continue
|
|
126
131
|
|
|
127
132
|
r = await WebChatMessageEvent._send(
|
|
128
133
|
chain,
|
|
@@ -142,7 +147,6 @@ class WebChatMessageEvent(AstrMessageEvent):
|
|
|
142
147
|
"data": final_data,
|
|
143
148
|
"reasoning": reasoning_content,
|
|
144
149
|
"streaming": True,
|
|
145
|
-
"cid": cid,
|
|
146
150
|
},
|
|
147
151
|
)
|
|
148
152
|
await super().send_streaming(generator, use_fallback)
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
import base64
|
|
2
4
|
import enum
|
|
3
5
|
import json
|
|
@@ -199,6 +201,38 @@ class ProviderRequest:
|
|
|
199
201
|
return ""
|
|
200
202
|
|
|
201
203
|
|
|
204
|
+
@dataclass
|
|
205
|
+
class TokenUsage:
|
|
206
|
+
input_other: int = 0
|
|
207
|
+
"""The number of input tokens, excluding cached tokens."""
|
|
208
|
+
input_cached: int = 0
|
|
209
|
+
"""The number of input cached tokens."""
|
|
210
|
+
output: int = 0
|
|
211
|
+
"""The number of output tokens."""
|
|
212
|
+
|
|
213
|
+
@property
|
|
214
|
+
def total(self) -> int:
|
|
215
|
+
return self.input_other + self.input_cached + self.output
|
|
216
|
+
|
|
217
|
+
@property
|
|
218
|
+
def input(self) -> int:
|
|
219
|
+
return self.input_other + self.input_cached
|
|
220
|
+
|
|
221
|
+
def __add__(self, other: TokenUsage) -> TokenUsage:
|
|
222
|
+
return TokenUsage(
|
|
223
|
+
input_other=self.input_other + other.input_other,
|
|
224
|
+
input_cached=self.input_cached + other.input_cached,
|
|
225
|
+
output=self.output + other.output,
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
def __sub__(self, other: TokenUsage) -> TokenUsage:
|
|
229
|
+
return TokenUsage(
|
|
230
|
+
input_other=self.input_other - other.input_other,
|
|
231
|
+
input_cached=self.input_cached - other.input_cached,
|
|
232
|
+
output=self.output - other.output,
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
|
|
202
236
|
@dataclass
|
|
203
237
|
class LLMResponse:
|
|
204
238
|
role: str
|
|
@@ -227,6 +261,11 @@ class LLMResponse:
|
|
|
227
261
|
is_chunk: bool = False
|
|
228
262
|
"""Indicates if the response is a chunked response."""
|
|
229
263
|
|
|
264
|
+
id: str | None = None
|
|
265
|
+
"""The ID of the response. For chunked responses, it's the ID of the chunk; for non-chunked responses, it's the ID of the response."""
|
|
266
|
+
usage: TokenUsage | None = None
|
|
267
|
+
"""The usage of the response. For chunked responses, it's the usage of the chunk; for non-chunked responses, it's the usage of the response."""
|
|
268
|
+
|
|
230
269
|
def __init__(
|
|
231
270
|
self,
|
|
232
271
|
role: str,
|
|
@@ -241,6 +280,8 @@ class LLMResponse:
|
|
|
241
280
|
| AnthropicMessage
|
|
242
281
|
| None = None,
|
|
243
282
|
is_chunk: bool = False,
|
|
283
|
+
id: str | None = None,
|
|
284
|
+
usage: TokenUsage | None = None,
|
|
244
285
|
):
|
|
245
286
|
"""初始化 LLMResponse
|
|
246
287
|
|