@brandon_9527/tcode 1.0.3 → 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/entry.py +59 -17
- package/dist/python-src/main.py +71 -12
- package/dist/python-src/pyproject.toml +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/core/deepagents.py +1 -1
- package/dist/python-src/src/managers/manager_agent.py +15 -15
- package/dist/python-src/src/managers/manager_context.py +1 -1
- package/dist/python-src/src/managers/manager_instruction.py +16 -16
- package/dist/python-src/src/managers/manager_skill.py +121 -0
- package/dist/python-src/src/managers/sandbox.py +2 -2
- package/dist/python-src/src/middlewares/dynamic_content.py +15 -12
- package/dist/python-src/src/middlewares/hitl.py +3 -3
- package/dist/python-src/src/middlewares/inject_content.py +0 -0
- 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 +4 -4
- package/dist/python-src/src/middlewares/summary.py +33 -33
- package/dist/python-src/src/prompts/prompts.py +1 -1
- package/dist/python-src/src/stream/formatter.py +16 -16
- package/dist/python-src/src/stream/handler.py +44 -45
- package/dist/python-src/src/stream/handler_with_tracker.py +7 -7
- package/dist/python-src/src/tools/tools.py +2 -2
- 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 +38 -41
- package/dist/python-src/src/tui/components/tlist.py +7 -7
- package/dist/python-src/src/tui/components/tscroll_panel.py +12 -12
- package/dist/python-src/src/tui/demo.py +22 -0
- package/dist/python-src/src/tui/utils/trender.py +22 -21
- package/dist/python-src/uv.lock +1974 -1939
- package/package.json +1 -1
package/dist/python-src/entry.py
CHANGED
|
@@ -6,37 +6,79 @@
|
|
|
6
6
|
# main()
|
|
7
7
|
|
|
8
8
|
|
|
9
|
-
# ===================== 强制修复 Git Bash 报错:NoConsoleScreenBufferError =====================
|
|
10
|
-
import os
|
|
11
|
-
import sys
|
|
9
|
+
# # ===================== 强制修复 Git Bash 报错:NoConsoleScreenBufferError =====================
|
|
10
|
+
# import os
|
|
11
|
+
# import sys
|
|
12
12
|
|
|
13
|
-
# 🔥 核心:强制让 prompt_toolkit 使用兼容模式,彻底关闭 Windows 控制台检测
|
|
14
|
-
os.environ["PROMPT_TOOLKIT_NO_WIN32"] = "1"
|
|
15
|
-
os.environ["PROMPT_TOOLKIT_BASIC_OUTPUT"] = "1"
|
|
16
|
-
os.environ["PROMPT_TOOLKIT_FORCE_VT100"] = "1"
|
|
13
|
+
# # 🔥 核心:强制让 prompt_toolkit 使用兼容模式,彻底关闭 Windows 控制台检测
|
|
14
|
+
# os.environ["PROMPT_TOOLKIT_NO_WIN32"] = "1"
|
|
15
|
+
# os.environ["PROMPT_TOOLKIT_BASIC_OUTPUT"] = "1"
|
|
16
|
+
# os.environ["PROMPT_TOOLKIT_FORCE_VT100"] = "1"
|
|
17
17
|
|
|
18
|
-
# 🔥 强制设置终端类型,让程序认为是 Linux 终端
|
|
19
|
-
if sys.platform == "win32":
|
|
20
|
-
|
|
18
|
+
# # 🔥 强制设置终端类型,让程序认为是 Linux 终端
|
|
19
|
+
# if sys.platform == "win32":
|
|
20
|
+
# os.environ["TERM"] = "xterm-256color"
|
|
21
21
|
|
|
22
|
-
# ===================== 下面才是你原来的代码 =====================
|
|
22
|
+
# # ===================== 下面才是你原来的代码 =====================
|
|
23
23
|
|
|
24
24
|
|
|
25
25
|
import warnings
|
|
26
26
|
warnings.filterwarnings("ignore", category=DeprecationWarning)
|
|
27
27
|
warnings.filterwarnings("ignore", message="Pydantic serializer warnings")
|
|
28
28
|
|
|
29
|
+
|
|
30
|
+
import typer
|
|
31
|
+
import asyncio
|
|
32
|
+
|
|
33
|
+
|
|
29
34
|
from main import (
|
|
30
35
|
agent_ui,
|
|
31
36
|
asingle_agent,
|
|
32
37
|
team_main,
|
|
38
|
+
run_once,
|
|
39
|
+
teminal_chat
|
|
33
40
|
)
|
|
34
41
|
|
|
42
|
+
app = typer.Typer()
|
|
43
|
+
interact_app = typer.Typer()
|
|
44
|
+
app.add_typer(interact_app)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@app.callback(invoke_without_command=True, no_args_is_help=False)
|
|
48
|
+
def root(
|
|
49
|
+
ctx: typer.Context,
|
|
50
|
+
prompt: str = typer.Option(None, "-p", "--prompt", help="用户输入请求内容"),
|
|
51
|
+
verbose: bool = typer.Option(False, "-v", "--verbose", help="显示agent执行详细过程"),
|
|
52
|
+
mode: str = typer.Option("team", "-m", "--mode", help="运行模式,single或team"),
|
|
53
|
+
):
|
|
54
|
+
if ctx.invoked_subcommand is None:
|
|
55
|
+
if not prompt:
|
|
56
|
+
if mode == "single":
|
|
57
|
+
asyncio.run(asingle_agent())
|
|
58
|
+
elif mode == "team":
|
|
59
|
+
asyncio.run(team_main())
|
|
60
|
+
|
|
61
|
+
# 同步环境执行异步任务
|
|
62
|
+
asyncio.run(run_once(prompt, verbose))
|
|
63
|
+
# typer.echo(result)
|
|
64
|
+
# raise typer.Exit()
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@interact_app.command("interact")
|
|
68
|
+
def interact_mode(
|
|
69
|
+
verbose: bool = typer.Option(False, "-v", "--verbose", help="显示agent执行详细过程"),
|
|
70
|
+
):
|
|
71
|
+
# 执行交互模式
|
|
72
|
+
asyncio.run(teminal_chat(verbose))
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
|
|
35
76
|
if __name__ == "__main__":
|
|
36
|
-
|
|
77
|
+
app()
|
|
78
|
+
# import asyncio
|
|
37
79
|
|
|
38
|
-
try:
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
except Exception as e:
|
|
42
|
-
|
|
80
|
+
# try:
|
|
81
|
+
# # asyncio.run(asingle_agent())
|
|
82
|
+
# asyncio.run(team_main())
|
|
83
|
+
# except Exception as e:
|
|
84
|
+
# pass
|
package/dist/python-src/main.py
CHANGED
|
@@ -328,14 +328,15 @@ 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}")
|
|
335
336
|
|
|
336
337
|
agents = []
|
|
337
338
|
for member_name, member in team_conf["members"].items():
|
|
338
|
-
print(f"member_name: {member_name}, member: {member}")
|
|
339
|
+
# print(f"member_name: {member_name}, member: {member}")
|
|
339
340
|
|
|
340
341
|
tools = []
|
|
341
342
|
for tool_name in member["tools"]:
|
|
@@ -404,8 +405,7 @@ async def team_main():
|
|
|
404
405
|
await ui.run_async()
|
|
405
406
|
|
|
406
407
|
|
|
407
|
-
|
|
408
|
-
""" """
|
|
408
|
+
def build_team():
|
|
409
409
|
from dataclasses import dataclass, field
|
|
410
410
|
from pathlib import Path
|
|
411
411
|
|
|
@@ -414,8 +414,6 @@ async def teminal_chat():
|
|
|
414
414
|
from src.utils.prompt import apply_template, apply_prompt
|
|
415
415
|
from src.managers.sandbox import Container
|
|
416
416
|
from src.prompts.prompts import leader
|
|
417
|
-
from src.stream.handler import ainput, anormal_handler
|
|
418
|
-
|
|
419
417
|
from src.tools.tools import (
|
|
420
418
|
SkillAgentContext,
|
|
421
419
|
shell,
|
|
@@ -431,13 +429,15 @@ async def teminal_chat():
|
|
|
431
429
|
from src.tools.web import (
|
|
432
430
|
web_search,
|
|
433
431
|
web_fetch
|
|
434
|
-
)
|
|
435
|
-
|
|
432
|
+
)
|
|
433
|
+
from src.middlewares.dynamic_content import DynamicContentMiddleware
|
|
434
|
+
|
|
436
435
|
from langgraph.checkpoint.memory import MemorySaver
|
|
437
|
-
from langchain_core.messages import HumanMessage
|
|
438
436
|
from langchain_core.tools import BaseTool
|
|
439
437
|
|
|
440
438
|
|
|
439
|
+
|
|
440
|
+
|
|
441
441
|
def _prepare(workspace, run_mode, session_id):
|
|
442
442
|
saver = MemorySaver()
|
|
443
443
|
|
|
@@ -493,14 +493,14 @@ async def teminal_chat():
|
|
|
493
493
|
|
|
494
494
|
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):
|
|
495
495
|
""" """
|
|
496
|
-
team_conf = agents_conf
|
|
496
|
+
team_conf = agents_conf.setdefault(domain, {"members":{}})
|
|
497
497
|
|
|
498
498
|
# for mname, mconf in team_conf['members'].items():
|
|
499
499
|
# print(f"member_name: {mname}, member: {mconf}")
|
|
500
500
|
|
|
501
501
|
agents = []
|
|
502
502
|
for member_name, member in team_conf["members"].items():
|
|
503
|
-
print(f"member_name: {member_name}, member: {member}")
|
|
503
|
+
# print(f"member_name: {member_name}, member: {member}")
|
|
504
504
|
|
|
505
505
|
tools = []
|
|
506
506
|
for tool_name in member["tools"]:
|
|
@@ -540,6 +540,9 @@ async def teminal_chat():
|
|
|
540
540
|
# prompt_name="leader_",
|
|
541
541
|
# WORKSPACE=workspace
|
|
542
542
|
# ),
|
|
543
|
+
middleware=[
|
|
544
|
+
DynamicContentMiddleware(),
|
|
545
|
+
],
|
|
543
546
|
system_prompt=apply_prompt(leader, WORKSPACE=workspace),
|
|
544
547
|
checkpointer=saver
|
|
545
548
|
).with_config({"recursion_limit": recursion_limit})
|
|
@@ -564,6 +567,15 @@ async def teminal_chat():
|
|
|
564
567
|
recursion_limit=1000
|
|
565
568
|
)
|
|
566
569
|
|
|
570
|
+
return agent, context, instruction_manager
|
|
571
|
+
|
|
572
|
+
|
|
573
|
+
async def teminal_chat(verbose=True):
|
|
574
|
+
""" """
|
|
575
|
+
from langchain_core.messages import HumanMessage
|
|
576
|
+
from src.stream.handler import ainput, anormal_handler
|
|
577
|
+
|
|
578
|
+
agent, context, instruction_manager = build_team()
|
|
567
579
|
|
|
568
580
|
try:
|
|
569
581
|
while True:
|
|
@@ -580,6 +592,9 @@ async def teminal_chat():
|
|
|
580
592
|
|
|
581
593
|
new_query = f"\n [注意]: 执行用户请求必须严格遵循如下准则:\n{executed_instruction}\n\n 用户请求:\n{message}" if executed_instruction else message
|
|
582
594
|
|
|
595
|
+
|
|
596
|
+
print(f"【new_query】:\n {new_query}")
|
|
597
|
+
|
|
583
598
|
stream = agent.astream(
|
|
584
599
|
{
|
|
585
600
|
"messages":[HumanMessage(content=new_query)]
|
|
@@ -589,7 +604,7 @@ async def teminal_chat():
|
|
|
589
604
|
context=context
|
|
590
605
|
) # .with_config({"recursion_limit": 1000})
|
|
591
606
|
|
|
592
|
-
await anormal_handler(stream, detail=
|
|
607
|
+
await anormal_handler(stream, detail=verbose)
|
|
593
608
|
|
|
594
609
|
except Exception as e:
|
|
595
610
|
import traceback
|
|
@@ -597,6 +612,50 @@ async def teminal_chat():
|
|
|
597
612
|
finally:
|
|
598
613
|
pass
|
|
599
614
|
|
|
615
|
+
|
|
616
|
+
async def run_once(prompt:str = None, verbose: bool=True):
|
|
617
|
+
from langchain_core.messages import HumanMessage
|
|
618
|
+
from src.stream.handler import ainput, anormal_handler
|
|
619
|
+
|
|
620
|
+
agent, context, instruction_manager = build_team()
|
|
621
|
+
|
|
622
|
+
query = prompt
|
|
623
|
+
|
|
624
|
+
try:
|
|
625
|
+
if query == "/comands":
|
|
626
|
+
for instruction in instruction_manager.list_instructions():
|
|
627
|
+
# print(instruction)
|
|
628
|
+
print(f"command: {instruction.name}, description: {instruction.description}")
|
|
629
|
+
|
|
630
|
+
instruction_result = instruction_manager.parse(query)
|
|
631
|
+
executed_instruction, message = instruction_result["executed_instruction"], instruction_result['message']
|
|
632
|
+
|
|
633
|
+
new_query = f"\n [注意]: 执行用户请求必须严格遵循如下准则:\n{executed_instruction}\n\n 用户请求:\n{message}" if executed_instruction else message
|
|
634
|
+
|
|
635
|
+
|
|
636
|
+
if verbose:
|
|
637
|
+
print(f"user> {new_query}")
|
|
638
|
+
|
|
639
|
+
stream = agent.astream(
|
|
640
|
+
{
|
|
641
|
+
"messages":[HumanMessage(content=new_query)]
|
|
642
|
+
},
|
|
643
|
+
config = {"configurable": {"thread_id": 1}},
|
|
644
|
+
stream_mode=["updates", "custom"],
|
|
645
|
+
context=context
|
|
646
|
+
) # .with_config({"recursion_limit": 1000})
|
|
647
|
+
|
|
648
|
+
await anormal_handler(stream, detail=verbose)
|
|
649
|
+
|
|
650
|
+
except Exception as e:
|
|
651
|
+
import traceback
|
|
652
|
+
traceback.print_exc()
|
|
653
|
+
|
|
654
|
+
finally:
|
|
655
|
+
pass
|
|
656
|
+
|
|
657
|
+
|
|
658
|
+
|
|
600
659
|
|
|
601
660
|
|
|
602
661
|
if __name__ == "__main__":
|
|
@@ -32,6 +32,7 @@ dependencies = [
|
|
|
32
32
|
"langchain-core==1.2.13",
|
|
33
33
|
"python-minifier>=3.2.0",
|
|
34
34
|
"ddgs>=9.11.4",
|
|
35
|
+
"typer>=0.24.1",
|
|
35
36
|
]
|
|
36
37
|
|
|
37
38
|
#[project.scripts]
|
|
@@ -56,7 +57,7 @@ skip = [
|
|
|
56
57
|
"resources",
|
|
57
58
|
"_workspace",
|
|
58
59
|
".autodev",
|
|
59
|
-
"tools"
|
|
60
|
+
"tools",
|
|
60
61
|
]
|
|
61
62
|
|
|
62
63
|
exclude = [
|
|
@@ -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}")
|