@brandon_9527/tcode 1.0.6 → 1.0.7
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.
- package/dist/python-src/main.py +2 -1
- package/dist/python-src/skill_agent.py +144 -0
- package/dist/python-src/src/agents/token_tracker.py +44 -0
- package/dist/python-src/src/claw/__init__.py +0 -0
- package/dist/python-src/src/claw/bus/__init__.py +3 -0
- package/dist/python-src/src/claw/bus/events.py +10 -0
- package/dist/python-src/src/claw/bus/queue.py +43 -0
- package/dist/python-src/src/claw/channels/__init__.py +3 -0
- package/dist/python-src/src/claw/channels/base.py +30 -0
- package/dist/python-src/src/claw/channels/feishu.py +89 -0
- package/dist/python-src/src/claw/channels/manager.py +47 -0
- package/dist/python-src/src/claw/config/schema.py +46 -0
- package/dist/python-src/src/managers/manager_agent.py +2 -2
- package/dist/python-src/src/managers/manager_instruction.py +7 -7
- package/dist/python-src/src/managers/manager_skill.py +121 -0
- package/dist/python-src/src/managers/sandbox.py +3 -3
- package/dist/python-src/src/middlewares/dynamic_content.py +3 -3
- package/dist/python-src/src/middlewares/hitl.py +3 -3
- package/dist/python-src/src/middlewares/memory.py +2 -2
- package/dist/python-src/src/middlewares/skill.py +27 -0
- package/dist/python-src/src/middlewares/subagents.py +2 -2
- package/dist/python-src/src/middlewares/summary.py +37 -37
- package/dist/python-src/src/stream/formatter.py +19 -19
- package/dist/python-src/src/trackers/__init__.py +0 -0
- package/dist/python-src/src/trackers/token/__init__.py +0 -0
- package/dist/python-src/src/trackers/token/cli.py +45 -0
- package/dist/python-src/src/trackers/token/pricing.py +39 -0
- package/dist/python-src/src/trackers/token/report.py +114 -0
- package/dist/python-src/src/trackers/token/tracker.py +65 -0
- package/dist/python-src/src/tui/chatui.py +11 -10
- package/dist/python-src/src/tui/components/tlist.py +5 -5
- package/dist/python-src/src/tui/components/tscroll_panel.py +14 -14
- package/dist/python-src/src/tui/utils/trender.py +23 -22
- package/package.json +1 -1
package/dist/python-src/main.py
CHANGED
|
@@ -328,7 +328,8 @@ async def team_main():
|
|
|
328
328
|
|
|
329
329
|
def _build_team(agents_conf: Dict[str, Any], domain: str, llm: ChatOpenAI, toolkits: Dict[str, List[BaseTool]], workspace: str, saver=None, store=None, recursion_limit=1000):
|
|
330
330
|
""" """
|
|
331
|
-
|
|
331
|
+
|
|
332
|
+
team_conf = agents_conf.setdefault(domain, {"members":{}})
|
|
332
333
|
|
|
333
334
|
# for mname, mconf in team_conf['members'].items():
|
|
334
335
|
# print(f"member_name: {mname}, member: {mconf}")
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import uuid
|
|
2
|
+
from langchain.tools import tool
|
|
3
|
+
from langchain.agents import create_agent
|
|
4
|
+
|
|
5
|
+
from langgraph.checkpoint.memory import InMemorySaver
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
from langchain.messages import AIMessage, AIMessageChunk, HumanMessage, AnyMessage, ToolMessage
|
|
9
|
+
from langchain_openai import ChatOpenAI
|
|
10
|
+
from dotenv import find_dotenv, load_dotenv
|
|
11
|
+
import os
|
|
12
|
+
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
import sys
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
from src.stream.handler import ainput, anormal_handler
|
|
18
|
+
from src.middlewares.skill import SkillMiddleware
|
|
19
|
+
|
|
20
|
+
from src.tools.tools import (
|
|
21
|
+
SkillAgentContext,
|
|
22
|
+
shell,
|
|
23
|
+
bash,
|
|
24
|
+
read_file,
|
|
25
|
+
write_file,
|
|
26
|
+
glob,
|
|
27
|
+
grep,
|
|
28
|
+
edit,
|
|
29
|
+
list_dir,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
from src.tools.web import (
|
|
33
|
+
web_search,
|
|
34
|
+
web_fetch
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
toolkits = {
|
|
38
|
+
"filetools": [ bash, read_file, write_file, glob, grep, edit, list_dir],
|
|
39
|
+
"web": [web_search, web_fetch]
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
_ = load_dotenv(find_dotenv())
|
|
44
|
+
|
|
45
|
+
def get_default_model(streaming: bool = False) -> ChatOpenAI:
|
|
46
|
+
"""
|
|
47
|
+
修复:兼容所有 OpenAI 格式厂商
|
|
48
|
+
- 流式:开启 stream_options 获取 usage
|
|
49
|
+
- 非流式:不传 stream_options,避免报错
|
|
50
|
+
"""
|
|
51
|
+
# 基础参数
|
|
52
|
+
model_kwargs = {}
|
|
53
|
+
#model_kwargs['extra_body'] = {"chat_template_kwargs":{'enable_thinking': False}}
|
|
54
|
+
extra_body = {"chat_template_kwargs":{'enable_thinking': False}}
|
|
55
|
+
# 只有流式模式才加 stream_options(非流式不加,避免400错误)
|
|
56
|
+
# 非流式模式默认输出 usage 信息
|
|
57
|
+
if streaming:
|
|
58
|
+
model_kwargs["stream_options"] = {"include_usage": True}
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
return ChatOpenAI(
|
|
63
|
+
model_name=os.getenv("DEFAULT_MODEL"),
|
|
64
|
+
base_url=os.getenv("OPENAI_API_BASE"),
|
|
65
|
+
api_key=os.getenv("OPENAI_API_KEY"),
|
|
66
|
+
streaming=streaming,
|
|
67
|
+
model_kwargs=model_kwargs, # 动态传入
|
|
68
|
+
extra_body=extra_body
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _render_message_chunk(token: AIMessageChunk) -> None:
|
|
73
|
+
if token.text:
|
|
74
|
+
print(token.text, end="|")
|
|
75
|
+
if token.tool_call_chunks:
|
|
76
|
+
print(token.tool_call_chunks)
|
|
77
|
+
|
|
78
|
+
def _render_completed_message(message: AnyMessage) -> None:
|
|
79
|
+
if isinstance(message, AIMessage) and message.tool_calls:
|
|
80
|
+
print(f"Tool calls: {message.tool_calls}")
|
|
81
|
+
if isinstance(message, ToolMessage):
|
|
82
|
+
print(f"Tool response: {message.content_blocks}")
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
async def amain():
|
|
86
|
+
model = get_default_model(streaming=True)
|
|
87
|
+
|
|
88
|
+
BASE_AGENT_PROMPT = """
|
|
89
|
+
你是一个专业的助手,你的任务是回答用户的问题。
|
|
90
|
+
你可以使用工具来获取信息,也可以直接回答用户的问题。
|
|
91
|
+
"""
|
|
92
|
+
|
|
93
|
+
agent = create_agent(
|
|
94
|
+
model=model,
|
|
95
|
+
system_prompt=BASE_AGENT_PROMPT,
|
|
96
|
+
tools=[bash, read_file, write_file, glob, grep, edit, list_dir, web_search, web_fetch],
|
|
97
|
+
middleware=[SkillMiddleware(workspace=Path.cwd())],
|
|
98
|
+
checkpoint=InMemorySaver(),
|
|
99
|
+
verbose=True,
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
context = SkillAgentContext(
|
|
103
|
+
working_directory=Path.cwd(),
|
|
104
|
+
# skill_loader=None,
|
|
105
|
+
sandbox=None
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
while True:
|
|
110
|
+
|
|
111
|
+
user_input = ainput("User> ")
|
|
112
|
+
if user_input == "exit":
|
|
113
|
+
break
|
|
114
|
+
|
|
115
|
+
steam = agent.astream(
|
|
116
|
+
{
|
|
117
|
+
"messages": [HumanMessage(content=user_input) ]
|
|
118
|
+
},
|
|
119
|
+
config = {"configurable": {"thread_id": 1}},
|
|
120
|
+
stream_mode=["updates", "custom"],
|
|
121
|
+
context=context
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
async for chunk in steam:
|
|
125
|
+
if chunk["type"] == "messages":
|
|
126
|
+
token, metadate = chunk["data"]
|
|
127
|
+
if isinstance(token, AIMessageChunk):
|
|
128
|
+
#print(token)
|
|
129
|
+
_render_message_chunk(token)
|
|
130
|
+
elif chunk["type"] == "updates":
|
|
131
|
+
for source, update in chunk["data"].items():
|
|
132
|
+
if source in ("model", "tools"):
|
|
133
|
+
_render_completed_message(update["messages"][-1])
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
if __name__ == "__main__":
|
|
137
|
+
import asyncio
|
|
138
|
+
asyncio.run(amain())
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
_H='content'
|
|
2
|
+
_G=' \n 你是一个专业的助手,你的任务是回答用户的问题。\n 你可以使用工具来获取信息,也可以直接回答用户的问题。\n '
|
|
3
|
+
_F='---------------------------'
|
|
4
|
+
_E='total_tokens'
|
|
5
|
+
_D='output_tokens'
|
|
6
|
+
_C='input_tokens'
|
|
7
|
+
_B=False
|
|
8
|
+
_A='messages'
|
|
9
|
+
from langchain_openai import ChatOpenAI
|
|
10
|
+
from dotenv import find_dotenv,load_dotenv
|
|
11
|
+
import os
|
|
12
|
+
from langchain.messages import AIMessage,AIMessageChunk,AnyMessage,ToolMessage
|
|
13
|
+
from langchain.agents.middleware import AgentMiddleware,ModelRequest,ModelResponse
|
|
14
|
+
from langchain_core.messages import AIMessage
|
|
15
|
+
from typing import Callable,Dict
|
|
16
|
+
_=load_dotenv(find_dotenv())
|
|
17
|
+
def get_default_model(streaming=_B):
|
|
18
|
+
A=streaming;B={};C={'chat_template_kwargs':{'enable_thinking':_B}}
|
|
19
|
+
if A:B['stream_options']={'include_usage':True}
|
|
20
|
+
return ChatOpenAI(model_name=os.getenv('DEFAULT_MODEL'),base_url=os.getenv('OPENAI_API_BASE'),api_key=os.getenv('OPENAI_API_KEY'),streaming=A,model_kwargs=B,extra_body=C)
|
|
21
|
+
class TokenUsageMiddleware(AgentMiddleware):
|
|
22
|
+
def wrap_model_call(H,request,handler):C=request;print(f"[REQUEST]: {C}");A=handler(C);print(f"[RESPONSE]: {A}");D=A.result[-1];B=D.usage_metadata or{};E=B.get(_C,0);F=B.get(_D,0);G=B.get(_E,0);print(f"--- 模型调用 Token 统计 ---");print(f"输入 Tokens: {E}");print(f"输出 Tokens: {F}");print(f"总计 Tokens: {G}");print(_F);return A
|
|
23
|
+
async def awrap_model_call(G,request,handler):B=await handler(request);C=B.result[-1];A=C.usage_metadata or{};D=A.get(_C,0);E=A.get(_D,0);F=A.get(_E,0);print(f"\n--- 模型调用 Token 统计 ---");print(f"输入 Tokens: {D}");print(f"输出 Tokens: {E}");print(f"总计 Tokens: {F}");print(_F);return B
|
|
24
|
+
from langchain.agents import create_agent
|
|
25
|
+
from langchain.chat_models import init_chat_model
|
|
26
|
+
def main():B=get_default_model(streaming=_B);C=[];D=_G;E=create_agent(model=B,tools=C,system_prompt=D,middleware=[TokenUsageMiddleware()]);A='请介绍一下LangChain v1的新特性';A='你好';E.invoke({_A:[{'role':'user',_H:A}]})
|
|
27
|
+
def cx(token):
|
|
28
|
+
A=token
|
|
29
|
+
if A.text:print(A.text,end='|')
|
|
30
|
+
if A.tool_call_chunks:print(A.tool_call_chunks)
|
|
31
|
+
def cw(message):
|
|
32
|
+
A=message
|
|
33
|
+
if isinstance(A,AIMessage)and A.tool_calls:print(f"Tool calls: {A.tool_calls}")
|
|
34
|
+
if isinstance(A,ToolMessage):print(f"Tool response: {A.content_blocks}")
|
|
35
|
+
async def amain():
|
|
36
|
+
E='data';D='type';C='updates';F=get_default_model(streaming=True);G=[];H=_G;I=create_agent(model=F,tools=G,system_prompt=H,middleware=[TokenUsageMiddleware()]);J={'role':'user',_H:'你好'}
|
|
37
|
+
async for A in I.astream({_A:[J]},stream_mode=[_A,C],version='v2'):
|
|
38
|
+
if A[D]==_A:
|
|
39
|
+
B,M=A[E]
|
|
40
|
+
if isinstance(B,AIMessageChunk):cx(B)
|
|
41
|
+
elif A[D]==C:
|
|
42
|
+
for(K,L)in A[E].items():
|
|
43
|
+
if K in('model','tools'):cw(L[_A][-1])
|
|
44
|
+
if __name__=='__main__':import asyncio;asyncio.run(amain())
|
|
File without changes
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
from dataclasses import dataclass,field
|
|
2
|
+
from datetime import datetime
|
|
3
|
+
from typing import Any
|
|
4
|
+
@dataclass
|
|
5
|
+
class InboundMessage:
|
|
6
|
+
channel:str;sender_id:str;chat_id:str;content:str;timestamp:datetime=field(default_factory=datetime.now);media:list[str]=field(default_factory=list);metadata:dict[str,Any]=field(default_factory=dict)
|
|
7
|
+
@property
|
|
8
|
+
def session_key(self):return f"{self.channel}:{self.chat_id}"
|
|
9
|
+
@dataclass
|
|
10
|
+
class OutboundMessage:channel:str;chat_id:str;content:str;reply_to:str|None=None;media:list[str]=field(default_factory=list);metadata:dict[str,Any]=field(default_factory=dict)
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
_A=False
|
|
2
|
+
import asyncio
|
|
3
|
+
from typing import Callable,Awaitable
|
|
4
|
+
from logging import getLogger
|
|
5
|
+
from src.nano.bus.events import InboundMessage,OutboundMessage
|
|
6
|
+
logger=getLogger(__name__)
|
|
7
|
+
class MessageBus:
|
|
8
|
+
def __init__(A):A.inbound=asyncio.Queue();A.outbound=asyncio.Queue();A._outbound_subscribers={};A._inbound_subscribers={};A._in_running=_A;A._out_running=_A
|
|
9
|
+
async def publish_inbound(A,msg):await A.inbound.put(msg)
|
|
10
|
+
async def consume_inbound(A):return await A.inbound.get()
|
|
11
|
+
async def publish_outbound(A,msg):await A.outbound.put(msg)
|
|
12
|
+
async def consume_outbound(A):return await A.outbound.get()
|
|
13
|
+
def subscribe_inbound(A,channel,callback):
|
|
14
|
+
B=channel
|
|
15
|
+
if B not in A._inbound_subscribers:A._inbound_subscribers[B]=[]
|
|
16
|
+
A._inbound_subscribers[B].append(callback)
|
|
17
|
+
def subscribe_outbound(A,channel,callback):
|
|
18
|
+
B=channel
|
|
19
|
+
if B not in A._outbound_subscribers:A._outbound_subscribers[B]=[]
|
|
20
|
+
A._outbound_subscribers[B].append(callback)
|
|
21
|
+
async def dispatch_inbound(A):
|
|
22
|
+
A._in_running=True
|
|
23
|
+
while A._in_running:
|
|
24
|
+
try:
|
|
25
|
+
B=await asyncio.wait_for(A.inbound.get(),timeout=1.);C=A._inbound_subscribers.get(B.channel,[])
|
|
26
|
+
for D in C:
|
|
27
|
+
try:await D(B)
|
|
28
|
+
except Exception as E:logger.error(f"Error dispatching to {B.channel}: {E}")
|
|
29
|
+
except asyncio.TimeoutError:continue
|
|
30
|
+
async def dispatch_outbound(A):
|
|
31
|
+
A._out_running=True
|
|
32
|
+
while A._out_running:
|
|
33
|
+
try:
|
|
34
|
+
B=await asyncio.wait_for(A.outbound.get(),timeout=1.);C=A._outbound_subscribers.get(B.channel,[])
|
|
35
|
+
for D in C:
|
|
36
|
+
try:await D(B)
|
|
37
|
+
except Exception as E:logger.error(f"Error dispatching to {B.channel}: {E}")
|
|
38
|
+
except asyncio.TimeoutError:continue
|
|
39
|
+
def stop(A):A._in_running=_A;A._out_running=_A
|
|
40
|
+
@property
|
|
41
|
+
def inbound_size(self):return self.inbound.qsize()
|
|
42
|
+
@property
|
|
43
|
+
def outbound_size(self):return self.outbound.qsize()
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
from abc import ABC,abstractmethod
|
|
2
|
+
from typing import Any
|
|
3
|
+
from logging import getLogger
|
|
4
|
+
from src.nano.bus.events import InboundMessage,OutboundMessage
|
|
5
|
+
from src.nano.bus.queue import MessageBus
|
|
6
|
+
logger=getLogger(__name__)
|
|
7
|
+
class BaseChannel(ABC):
|
|
8
|
+
name:str='base'
|
|
9
|
+
def __init__(A,config,bus):A.config=config;A.bus=bus;A._running=False
|
|
10
|
+
@abstractmethod
|
|
11
|
+
async def start(self):0
|
|
12
|
+
@abstractmethod
|
|
13
|
+
async def stop(self):0
|
|
14
|
+
@abstractmethod
|
|
15
|
+
async def send(self,msg):0
|
|
16
|
+
def is_allowed(E,sender_id):
|
|
17
|
+
C=True;A=getattr(E.config,'allow_from',[])
|
|
18
|
+
if not A:return C
|
|
19
|
+
B=str(sender_id)
|
|
20
|
+
if B in A:return C
|
|
21
|
+
if'|'in B:
|
|
22
|
+
for D in B.split('|'):
|
|
23
|
+
if D and D in A:return C
|
|
24
|
+
return False
|
|
25
|
+
async def _handle_message(A,sender_id,chat_id,content,media=None,metadata=None):
|
|
26
|
+
B=sender_id
|
|
27
|
+
if not A.is_allowed(B):logger.warning(f"Access denied for sender {B} on channel {A.name}.Add them to allowFrom list in config to grant access.");return
|
|
28
|
+
C=InboundMessage(channel=A.name,sender_id=str(B),chat_id=str(chat_id),content=content,media=media or[],metadata=metadata or{});await A.bus.publish_inbound(C)
|
|
29
|
+
@property
|
|
30
|
+
def is_running(self):return self._running
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
_E='THUMBSUP'
|
|
2
|
+
_D=False
|
|
3
|
+
_C=True
|
|
4
|
+
_B='tag'
|
|
5
|
+
_A=None
|
|
6
|
+
import asyncio,json,re,threading
|
|
7
|
+
from collections import OrderedDict
|
|
8
|
+
from typing import Any
|
|
9
|
+
from logging import getLogger
|
|
10
|
+
logger=getLogger(__name__)
|
|
11
|
+
from src.nano.bus.events import InboundMessage,OutboundMessage
|
|
12
|
+
from src.nano.bus.queue import MessageBus
|
|
13
|
+
from src.nano.channels.base import BaseChannel
|
|
14
|
+
from src.nano.config.schema import FeishuConfig
|
|
15
|
+
try:import lark_oapi as lark;from lark_oapi.api.im.v1 import CreateMessageRequest,CreateMessageRequestBody,CreateMessageReactionRequest,CreateMessageReactionRequestBody,Emoji,P2ImMessageReceiveV1;FEISHU_AVAILABLE=_C
|
|
16
|
+
except ImportError:FEISHU_AVAILABLE=_D;lark=_A;Emoji=_A
|
|
17
|
+
MSG_TYPE_MAP={'image':'[image]','audio':'[audio]','file':'[file]','sticker':'[sticker]'}
|
|
18
|
+
class FeishuChannel(BaseChannel):
|
|
19
|
+
name='feishu'
|
|
20
|
+
def __init__(A,config,bus):B=config;super().__init__(B,bus);A.config=B;A._client=_A;A._ws_client=_A;A._ws_thread=_A;A._processed_message_ids=OrderedDict();A._loop=_A
|
|
21
|
+
async def start(A):
|
|
22
|
+
if not FEISHU_AVAILABLE:logger.error('Feishu SDK not installed. Run: pip install lark-oapi');return
|
|
23
|
+
if not A.config.app_id or not A.config.app_secret:logger.error('Feishu app_id and app_secret not configured');return
|
|
24
|
+
A._running=_C;A._loop=asyncio.get_running_loop();A._client=lark.Client.builder().app_id(A.config.app_id).app_secret(A.config.app_secret).log_level(lark.LogLevel.INFO).build();B=lark.EventDispatcherHandler.builder(A.config.encrypt_key or'',A.config.verification_token or'').register_p2_im_message_receive_v1(A.dg).build();A._ws_client=lark.ws.Client(A.config.app_id,A.config.app_secret,event_handler=B,log_level=lark.LogLevel.INFO)
|
|
25
|
+
def C():
|
|
26
|
+
try:A._ws_client.start()
|
|
27
|
+
except Exception as B:logger.error(f"Feishu WebSocket error: {B}")
|
|
28
|
+
A._ws_thread=threading.Thread(target=C,daemon=_C);A._ws_thread.start();logger.info('Feishu bot started with WebSocket long connection');logger.info('No public IP required - using WebSocket to reveive events')
|
|
29
|
+
while A._running:await asyncio.sleep(1)
|
|
30
|
+
async def stop(A):
|
|
31
|
+
A._running=_D
|
|
32
|
+
if A._ws_client:
|
|
33
|
+
try:A._ws_client.stop()
|
|
34
|
+
except Exception as B:logger.warning(f"Error stoppping WebSocket client: {B}")
|
|
35
|
+
logger.info('Feishu bot stopped')
|
|
36
|
+
def dh(D,message_id,emoji_type):
|
|
37
|
+
C=emoji_type;B=message_id
|
|
38
|
+
try:
|
|
39
|
+
E=CreateMessageReactionRequest.builder().message_id(B).request_body(CreateMessageReactionRequestBody.builder().reaction_type(Emoji.builder().emoji_type(C).build()).build()).build();A=D._client.im.v1.message_reaction.create(E)
|
|
40
|
+
if not A.success():logger.warning(f"Failed to add reaction: code={A.code}, msg={A.msg}")
|
|
41
|
+
else:logger.debug(f"Added {C} reaction to message {B}")
|
|
42
|
+
except Exception as F:logger.warning(f"Error adding reaction: {F}")
|
|
43
|
+
async def _add_reaction(A,message_id,emoji_type=_E):
|
|
44
|
+
if not A._client or not Emoji:return
|
|
45
|
+
B=asyncio.get_running_loop();await B.run_in_executor(_A,A.dh,message_id,emoji_type)
|
|
46
|
+
_TABLE_RE=re.compile('((?:^[ \\t]*\\|.+\\|[ \\t]*\\n)(?:^[ \\t]*\\|[-:\\s|]+\\|[ \\t]*\\n)(?:^[ \\t]*\\|.+\\|[ \\t]*\\n?)+)',re.MULTILINE)
|
|
47
|
+
@staticmethod
|
|
48
|
+
def dj(table_text):
|
|
49
|
+
A=[A.strip()for A in table_text.strip().split('\n')if A.strip()]
|
|
50
|
+
if len(A)<3:return
|
|
51
|
+
B=lambda l:[A.strip()for A in l.strip('|').split('|')];C=B(A[0]);D=[B(A)for A in A[2:]];E=[{_B:'column','name':f"c{A}",'display_name':B,'width':'auto'}for(A,B)in enumerate(C)];return{_B:'table','page_size':len(D)+1,'columns':E,'rows':[{f"c{A}":B[A]if A<len(B)else''for A in range(len(C))}for B in D]}
|
|
52
|
+
def di(G,content):
|
|
53
|
+
E='markdown';D='content';A=content;B,F=[],0
|
|
54
|
+
for C in G._TABLE_RE.finditer(A):
|
|
55
|
+
H=A[F:C.start()].strip()
|
|
56
|
+
if H:B.append({_B:E,D:H})
|
|
57
|
+
B.append(G.dj(C.group(1))or{_B:E,D:C.group(1)});F=C.end()
|
|
58
|
+
I=A[F:].strip()
|
|
59
|
+
if I:B.append({_B:E,D:I})
|
|
60
|
+
return B or[{_B:E,D:A}]
|
|
61
|
+
async def send(C,msg):
|
|
62
|
+
A=msg
|
|
63
|
+
if not C._client:logger.warning('Feishu client not initialized');return
|
|
64
|
+
try:
|
|
65
|
+
if A.chat_id.startswith('oc_'):D='chat_id'
|
|
66
|
+
else:D='open_id'
|
|
67
|
+
E=C.di(A.content);F={'config':{'wide_screen_mode':_C},'elements':E};G=json.dumps(F,ensure_ascii=_D);H=CreateMessageRequest.builder().receive_id_type(D).request_body(CreateMessageRequestBody.builder().receive_id(A.chat_id).msg_type('interactive').content(G).build()).build();B=C._client.im.v1.message.create(H)
|
|
68
|
+
if not B.success():logger.error(f"Failed to send Feishu message: code={B.code},msg={B.msg}, log_id={B.get_log_id()}")
|
|
69
|
+
else:logger.debug(f"Feishu message sent to {A.chat_id}")
|
|
70
|
+
except Exception as I:logger.error(f"Error sending Feishu message: {I}")
|
|
71
|
+
def dg(A,data):
|
|
72
|
+
if A._loop and A._loop.is_running():asyncio.run_coroutine_threadsafe(A._on_message(data),A._loop)
|
|
73
|
+
async def _on_message(A,data):
|
|
74
|
+
J='text'
|
|
75
|
+
try:
|
|
76
|
+
G=data.event;B=G.message;F=G.sender;C=B.message_id
|
|
77
|
+
if C in A._processed_message_ids:return
|
|
78
|
+
A._processed_message_ids[C]=_A
|
|
79
|
+
while len(A._processed_message_ids)>1000:A._processed_message_ids.popitem(last=_D)
|
|
80
|
+
K=F.sender_type
|
|
81
|
+
if K=='bot':return
|
|
82
|
+
H=F.sender_id.open_id if F.sender_id else'unknown';L=B.chat_id;I=B.chat_type;D=B.message_type;await A._add_reaction(C,_E)
|
|
83
|
+
if D==J:
|
|
84
|
+
try:E=json.loads(B.content).get(J,'')
|
|
85
|
+
except json.JSONDecodeError:E=B.content or''
|
|
86
|
+
else:E=MSG_TYPE_MAP.get(D,f"[{D}]")
|
|
87
|
+
if not E:return
|
|
88
|
+
M=L if I=='group'else H;await A._handle_message(sender_id=H,chat_id=M,content=E,metadata={'message_id':C,'chat_type':I,'msg_type':D})
|
|
89
|
+
except Exception as N:logger.error(f"Error processing Feishu message: {N}")
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
import asyncio
|
|
3
|
+
from typing import Any,TYPE_CHECKING
|
|
4
|
+
from logging import getLogger
|
|
5
|
+
from src.claw.bus.events import OutboundMessage
|
|
6
|
+
from src.claw.bus.queue import MessageBus
|
|
7
|
+
from src.claw.channels.base import BaseChannel
|
|
8
|
+
from src.claw.config.schema import Config
|
|
9
|
+
logger=getLogger(__name__)
|
|
10
|
+
class ChannelManager:
|
|
11
|
+
def __init__(A,config,bus):A.config=config;A.bus=bus;A.channels={};A._dispatch_task=None;A.dk()
|
|
12
|
+
def dk(A):
|
|
13
|
+
if A.config.channels.feishu.enabled:
|
|
14
|
+
try:from src.claw.channels.feishu import FeishuChannel as B;A.channels['feishu']=B(A.config.channels.feishu,A.bus);logger.info('Feishu channel enabled')
|
|
15
|
+
except ImportError as C:logger.warning(f"Feishu channel not available: {C}")
|
|
16
|
+
async def _start_channel(B,name,channel):
|
|
17
|
+
try:await channel.start()
|
|
18
|
+
except Exception as A:logger.error(f"Failed to start channel {name}: {A}")
|
|
19
|
+
async def start_all(A):
|
|
20
|
+
if not A.channels:logger.warning('No channels enabled');return
|
|
21
|
+
A._dispatch_task=asyncio.create_task(A._dispatch_outbound());B=[]
|
|
22
|
+
for(C,D)in A.channels.items():logger.info(f"Starting {C} channel ...");B.append(asyncio.create_task(A._start_channel(C,D)))
|
|
23
|
+
await asyncio.gather(*B,return_exceptions=True)
|
|
24
|
+
async def stop_all(A):
|
|
25
|
+
logger.info('Stopping all channels ...')
|
|
26
|
+
if A._dispatch_task:
|
|
27
|
+
A._dispatch_task.cancel()
|
|
28
|
+
try:await A._dispatch_task
|
|
29
|
+
except asyncio.CancelledError:pass
|
|
30
|
+
for(B,C)in A.channels.items():
|
|
31
|
+
try:await C.stop();logger.info(f"Stopped {B} channel")
|
|
32
|
+
except Exception as D:logger.error(f"Error stopping {B}: {D}")
|
|
33
|
+
async def _dispatch_outbound(B):
|
|
34
|
+
logger.info('Outbound dispatcher started')
|
|
35
|
+
while True:
|
|
36
|
+
try:
|
|
37
|
+
A=await asyncio.wait_for(B.bus.consume_outbound(),timeout=1.);C=B.channels.get(A.channel)
|
|
38
|
+
if C:
|
|
39
|
+
try:await C.send(A)
|
|
40
|
+
except Exception as D:logger.error(f"Error sending to {A.channel}: {D}")
|
|
41
|
+
else:logger.warning(f"Unknown channel: {A.channel}")
|
|
42
|
+
except asyncio.TimeoutError:continue
|
|
43
|
+
except asyncio.CancelledError:break
|
|
44
|
+
def get_channel(A,name):return A.channels.get(name)
|
|
45
|
+
def get_status(A):return{A:{'enabled':True,'running':B.is_running}for(A,B)in A.channels.items()}
|
|
46
|
+
@property
|
|
47
|
+
def enabled_channels(self):return list(self.channels.keys())
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
_C=True
|
|
2
|
+
_B=False
|
|
3
|
+
_A=None
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from pydantic import BaseModel,Field
|
|
6
|
+
from pydantic_settings import BaseSettings
|
|
7
|
+
class FeishuConfig(BaseModel):enabled:bool=_B;app_id:str='';app_secret:str='';encrypt_key:str='';verification_token:str='';allow_from:list[str]=Field(default_factory=list)
|
|
8
|
+
class DingTalkConfig(BaseModel):enabled:bool=_B;client_id:str='';client_secret:str='';allow_from:list[str]=Field(default_factory=list)
|
|
9
|
+
class EmailConfig(BaseModel):enabled:bool=_B;consent_granted:bool=_B;imap_host:str='';imap_port:int=993;imap_username:str='';imap_password:str='';imap_mailbox:str='INBOX';imap_use_ssl:bool=_C;smtp_host:str='';smtp_port:int=587;smtp_username:str='';smtp_password:str='';smtp_use_tls:bool=_C;smtp_use_ssl:bool=_B;from_address:str='';auto_reply_enabled:bool=_C;poll_interval_seconds:int=30;mark_seen:bool=_C;max_body_chars:int=12000;subject_prefix:str='Re: ';allow_from:list[str]=Field(default_factory=list)
|
|
10
|
+
class MochatMentionConfig(BaseModel):require_in_groups:bool=_B
|
|
11
|
+
class MochatGroupRule(BaseModel):require_mention:bool=_B
|
|
12
|
+
class MochatConfig(BaseModel):enabled:bool=_B;base_url:str='https://mochat.io';socket_url:str='';socket_path:str='/socket.io';socket_disable_msgpack:bool=_B;socket_reconnect_delay_ms:int=1000;socket_max_reconnect_delay_ms:int=10000;socket_connect_timeout_ms:int=10000;refresh_interval_ms:int=30000;watch_timeout_ms:int=25000;watch_limit:int=100;retry_delay_ms:int=500;max_retry_attempts:int=0;claw_token:str='';agent_user_id:str='';sessions:list[str]=Field(default_factory=list);panels:list[str]=Field(default_factory=list);allow_from:list[str]=Field(default_factory=list);mention:MochatMentionConfig=Field(default_factory=MochatMentionConfig);groups:dict[str,MochatGroupRule]=Field(default_factory=dict);reply_delay_mode:str='non-mention';reply_delay_ms:int=120000
|
|
13
|
+
class QQConfig(BaseModel):enabled:bool=_B;app_id:str='';secret:str='';allow_from:list[str]=Field(default_factory=list)
|
|
14
|
+
class ChannelsConfig(BaseModel):feishu:FeishuConfig=Field(default_factory=FeishuConfig);mochat:MochatConfig=Field(default_factory=MochatConfig);dingtalk:DingTalkConfig=Field(default_factory=DingTalkConfig);email:EmailConfig=Field(default_factory=EmailConfig);qq:QQConfig=Field(default_factory=QQConfig)
|
|
15
|
+
class AgentDefaults(BaseModel):workspace:str='~/.nanobot/workspace';model:str='qwen-plus';max_tokens:int=8192;temperature:float=.7;max_tool_iterations:int=200
|
|
16
|
+
class AgentsConfig(BaseModel):defaults:AgentDefaults=Field(default_factory=AgentDefaults)
|
|
17
|
+
class ProviderConfig(BaseModel):api_key:str='';api_base:str|_A=_A;extra_headers:dict[str,str]|_A=_A
|
|
18
|
+
class ProvidersConfig(BaseModel):anthropic:ProviderConfig=Field(default_factory=ProviderConfig);openai:ProviderConfig=Field(default_factory=ProviderConfig);openrouter:ProviderConfig=Field(default_factory=ProviderConfig);deepseek:ProviderConfig=Field(default_factory=ProviderConfig);groq:ProviderConfig=Field(default_factory=ProviderConfig);zhipu:ProviderConfig=Field(default_factory=ProviderConfig);dashscope:ProviderConfig=Field(default_factory=ProviderConfig);vllm:ProviderConfig=Field(default_factory=ProviderConfig);gemini:ProviderConfig=Field(default_factory=ProviderConfig);moonshot:ProviderConfig=Field(default_factory=ProviderConfig);minimax:ProviderConfig=Field(default_factory=ProviderConfig);aihubmix:ProviderConfig=Field(default_factory=ProviderConfig)
|
|
19
|
+
class GatewayConfig(BaseModel):host:str='0.0.0.0';port:int=18790
|
|
20
|
+
class WebSearchConfig(BaseModel):api_key:str='';max_results:int=5
|
|
21
|
+
class WebToolsConfig(BaseModel):search:WebSearchConfig=Field(default_factory=WebSearchConfig)
|
|
22
|
+
class ExecToolConfig(BaseModel):timeout:int=60
|
|
23
|
+
class ToolsConfig(BaseModel):web:WebToolsConfig=Field(default_factory=WebToolsConfig);exec:ExecToolConfig=Field(default_factory=ExecToolConfig);restrict_to_workspace:bool=_B
|
|
24
|
+
class Config(BaseSettings):
|
|
25
|
+
agents:AgentsConfig=Field(default_factory=AgentsConfig);channels:ChannelsConfig=Field(default_factory=ChannelsConfig);providers:ProvidersConfig=Field(default_factory=ProvidersConfig);gateway:GatewayConfig=Field(default_factory=GatewayConfig);tools:ToolsConfig=Field(default_factory=ToolsConfig)
|
|
26
|
+
@property
|
|
27
|
+
def workspace_path(self):return Path(self.agents.defaults.workspace).expanduser()
|
|
28
|
+
def df(C,model=_A):
|
|
29
|
+
from nanobot.providers.registry import PROVIDERS as D;E=(model or C.agents.defaults.model).lower()
|
|
30
|
+
for B in D:
|
|
31
|
+
A=getattr(C.providers,B.name,_A)
|
|
32
|
+
if A and any(A in E for A in B.keywords)and A.api_key:return A,B.name
|
|
33
|
+
for B in D:
|
|
34
|
+
A=getattr(C.providers,B.name,_A)
|
|
35
|
+
if A and A.api_key:return A,B.name
|
|
36
|
+
return _A,_A
|
|
37
|
+
def get_provider(A,model=_A):B,C=A.df(model);return B
|
|
38
|
+
def get_provider_name(A,model=_A):C,B=A.df(model);return B
|
|
39
|
+
def get_api_key(B,model=_A):A=B.get_provider(model);return A.api_key if A else _A
|
|
40
|
+
def get_api_base(D,model=_A):
|
|
41
|
+
from nanobot.providers.registry import find_by_name as E;B,C=D.df(model)
|
|
42
|
+
if B and B.api_base:return B.api_base
|
|
43
|
+
if C:
|
|
44
|
+
A=E(C)
|
|
45
|
+
if A and A.is_gateway and A.default_api_base:return A.default_api_base
|
|
46
|
+
class Config:env_prefix='NANOBOT_';env_nested_delimiter='__'
|
|
@@ -15,8 +15,8 @@ from pathlib import Path
|
|
|
15
15
|
import yaml,json,sys,re,os
|
|
16
16
|
sys.path.append('/Users/brandon/workspace/coder/autodev')
|
|
17
17
|
class AgentInfo:
|
|
18
|
-
def __init__(A,name,content,src_):B=content;A.name=name;A.original_content=B;A.src_=src_;A.meta,A.body=A.
|
|
19
|
-
def
|
|
18
|
+
def __init__(A,name,content,src_):B=content;A.name=name;A.original_content=B;A.src_=src_;A.meta,A.body=A.bw(B)
|
|
19
|
+
def bw(F,content):
|
|
20
20
|
B=content;C='^---\\n(.*?)\\n---\\n(.*)$';A=re.match(C,B,re.S)
|
|
21
21
|
if A:D=yaml.safe_load(A.group(1));E=A.group(2).strip();return D,E
|
|
22
22
|
else:return{},B.strip()
|
|
@@ -39,7 +39,7 @@ class InstructionManager:
|
|
|
39
39
|
if A.project_commands_dir and not os.path.exists(A.project_commands_dir):os.makedirs(A.project_commands_dir)
|
|
40
40
|
if not os.path.exists(A.user_dir):os.makedirs(A.user_dir)
|
|
41
41
|
A.reload_instructions()
|
|
42
|
-
def
|
|
42
|
+
def bx(O,dir_path,source):
|
|
43
43
|
F=source;C=dir_path;A={}
|
|
44
44
|
if not os.path.exists(C):return A
|
|
45
45
|
for(G,P,I)in os.walk(C):
|
|
@@ -54,9 +54,9 @@ class InstructionManager:
|
|
|
54
54
|
except Exception as N:print(f"从 {F} 加载命令 {B}/{E} 失败: {str(N)}")
|
|
55
55
|
return A
|
|
56
56
|
def reload_instructions(A):
|
|
57
|
-
A.instructions[_B].clear();A.instructions[_C].clear();A.instructions[_C]=A.
|
|
58
|
-
if A.project_commands_dir:A.instructions[_B]=A.
|
|
59
|
-
def
|
|
57
|
+
A.instructions[_B].clear();A.instructions[_C].clear();A.instructions[_C]=A.bx(A.user_dir,_C)
|
|
58
|
+
if A.project_commands_dir:A.instructions[_B]=A.bx(A.project_commands_dir,_B)
|
|
59
|
+
def by(A,source,domain,name):
|
|
60
60
|
B=source
|
|
61
61
|
if B==_B:
|
|
62
62
|
if not A.project_commands_dir:raise ValueError('项目目录未初始化,无法操作project来源的指令')
|
|
@@ -79,7 +79,7 @@ class InstructionManager:
|
|
|
79
79
|
def add_instruction(B,domain,name,settings,content,source=_B):
|
|
80
80
|
E=source;D=name;C=domain
|
|
81
81
|
if B.exists(C,D,E):raise ValueError(f"指令 '{E}/{C}/{D}' 已存在")
|
|
82
|
-
G=B.
|
|
82
|
+
G=B.by(E,C,D);os.makedirs(os.path.dirname(G),exist_ok=_G)
|
|
83
83
|
with open(G,'w',encoding=_I)as A:
|
|
84
84
|
A.write('---\n')
|
|
85
85
|
for(H,F)in settings.items():
|
|
@@ -98,7 +98,7 @@ class InstructionManager:
|
|
|
98
98
|
D=C.instructions[G][A][B]
|
|
99
99
|
if I is not _A:D.settings=I
|
|
100
100
|
if J is not _A:D.original_content=J
|
|
101
|
-
K=C.
|
|
101
|
+
K=C.by(G,A,B);os.makedirs(os.path.dirname(K),exist_ok=_G)
|
|
102
102
|
with open(K,'w',encoding=_I)as E:
|
|
103
103
|
E.write('---\n')
|
|
104
104
|
for(L,H)in D.settings.items():
|
|
@@ -114,7 +114,7 @@ class InstructionManager:
|
|
|
114
114
|
else:
|
|
115
115
|
G,E=C.get_instruction_with_source(A,B)
|
|
116
116
|
if not E:raise ValueError(f"指令 '{A}/{B}' 不存在于任何来源")
|
|
117
|
-
F=C.
|
|
117
|
+
F=C.by(E,A,B)
|
|
118
118
|
if os.path.exists(F):os.remove(F)
|
|
119
119
|
C.reload_instructions()
|
|
120
120
|
def load_all_instructions(A,domain=_D,source=_A):
|