@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
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
_O='output'
|
|
3
|
+
_N='by_session_id'
|
|
4
|
+
_M='by_user_id'
|
|
5
|
+
_L='by_tag'
|
|
6
|
+
_K='by_model'
|
|
7
|
+
_J='total_tokens'
|
|
8
|
+
_I='total_calls'
|
|
9
|
+
_H='total_cost_usd'
|
|
10
|
+
_G='period'
|
|
11
|
+
_F='by_period'
|
|
12
|
+
_E='until'
|
|
13
|
+
_D='since'
|
|
14
|
+
_C='calls'
|
|
15
|
+
_B='cost_usd'
|
|
16
|
+
_A=None
|
|
17
|
+
import json
|
|
18
|
+
from collections import defaultdict
|
|
19
|
+
from datetime import datetime
|
|
20
|
+
from typing import Optional
|
|
21
|
+
from.tracker import CostTracker
|
|
22
|
+
def dc(since,until,tag=_A,user_id=_A,session_id=_A):
|
|
23
|
+
F=session_id;E=user_id;D=until;C=since;A=[];B=[]
|
|
24
|
+
if C:A.append('timestamp >= ?');B.append(C.isoformat())
|
|
25
|
+
if D:A.append('timestamp <= ?');B.append(D.isoformat())
|
|
26
|
+
if tag:A.append('tag = ?');B.append(tag)
|
|
27
|
+
if E:A.append('user_id = ?');B.append(E)
|
|
28
|
+
if F:A.append('session_id = ?');B.append(F)
|
|
29
|
+
G=' AND '.join(A)if A else'1=1';return G,B
|
|
30
|
+
def dd(period):
|
|
31
|
+
A=period
|
|
32
|
+
if A=='daily':return"strftime('%Y-%m-%d', timestamp)"
|
|
33
|
+
elif A=='weekly':return"strftime('%Y-%W', timestamp)"
|
|
34
|
+
elif A=='monthly':return"strftime('%Y-%m', timestamp)"
|
|
35
|
+
else:raise ValueError(f"Unknown period: {A}")
|
|
36
|
+
class CostReport:
|
|
37
|
+
def __init__(A,tracker):A.tracker=tracker
|
|
38
|
+
def build(L,since=_A,until=_A,period='daily',tag=_A,user_id=_A,session_id=_A):I=period;H=until;G=since;F='output_tokens';E='input_tokens';B,C=dc(G,H,tag,user_id,session_id);A=L.tracker._conn.cursor();A.execute(f"""
|
|
39
|
+
SELECT
|
|
40
|
+
COUNT(*),
|
|
41
|
+
SUM(cost),
|
|
42
|
+
SUM(input_tokens),
|
|
43
|
+
SUM(output_tokens)
|
|
44
|
+
FROM usage
|
|
45
|
+
WHERE {B}
|
|
46
|
+
""",C);D=A.fetchone();M=D[0]or 0;N=D[1]or .0;J=D[2]or 0;K=D[3]or 0;A.execute(f"""
|
|
47
|
+
SELECT
|
|
48
|
+
model,
|
|
49
|
+
COUNT(*),
|
|
50
|
+
SUM(cost),
|
|
51
|
+
SUM(input_tokens),
|
|
52
|
+
SUM(output_tokens)
|
|
53
|
+
FROM usage
|
|
54
|
+
WHERE {B}
|
|
55
|
+
GROUP BY model
|
|
56
|
+
ORDER BY SUM(cost) DESC
|
|
57
|
+
""",C);O={A:{_B:round(C or 0,6),_C:B,E:D or 0,F:G or 0}for(A,B,C,D,G)in A.fetchall()};A.execute(f"""
|
|
58
|
+
SELECT
|
|
59
|
+
tag,
|
|
60
|
+
COUNT(*),
|
|
61
|
+
SUM(cost),
|
|
62
|
+
SUM(input_tokens),
|
|
63
|
+
SUM(output_tokens)
|
|
64
|
+
FROM usage
|
|
65
|
+
WHERE {B}
|
|
66
|
+
GROUP BY tag
|
|
67
|
+
ORDER BY SUM(cost) DESC
|
|
68
|
+
""",C);P={A:{_B:round(C or 0,6),_C:B,E:D or 0,F:G or 0}for(A,B,C,D,G)in A.fetchall()};A.execute(f"""
|
|
69
|
+
SELECT
|
|
70
|
+
user_id,
|
|
71
|
+
COUNT(*),
|
|
72
|
+
SUM(cost),
|
|
73
|
+
SUM(input_tokens),
|
|
74
|
+
SUM(output_tokens)
|
|
75
|
+
FROM usage
|
|
76
|
+
WHERE {B}
|
|
77
|
+
GROUP BY user_id
|
|
78
|
+
ORDER BY SUM(cost) DESC
|
|
79
|
+
""",C);Q={A:{_B:round(C or 0,6),_C:B,E:D or 0,F:G or 0}for(A,B,C,D,G)in A.fetchall()};A.execute(f"""
|
|
80
|
+
SELECT
|
|
81
|
+
session_id,
|
|
82
|
+
COUNT(*),
|
|
83
|
+
SUM(cost),
|
|
84
|
+
SUM(input_tokens),
|
|
85
|
+
SUM(output_tokens)
|
|
86
|
+
FROM usage
|
|
87
|
+
WHERE {B}
|
|
88
|
+
GROUP BY session_id
|
|
89
|
+
ORDER BY SUM(cost) DESC
|
|
90
|
+
""",C);R={A:{_B:round(C or 0,6),_C:B,E:D or 0,F:G or 0}for(A,B,C,D,G)in A.fetchall()};S=dd(I);A.execute(f"""
|
|
91
|
+
SELECT
|
|
92
|
+
{S} as bucket,
|
|
93
|
+
COUNT(*),
|
|
94
|
+
SUM(cost)
|
|
95
|
+
FROM usage
|
|
96
|
+
WHERE {B}
|
|
97
|
+
GROUP BY bucket
|
|
98
|
+
ORDER BY bucket
|
|
99
|
+
""",C);T={A:{_B:round(C or 0,6),_C:B}for(A,B,C)in A.fetchall()};return{_G:I,_D:G.isoformat()if G else _A,_E:H.isoformat()if H else _A,_H:round(N,6),_I:M,_J:{'input':J,_O:K,'total':J+K},_K:O,_L:P,_M:Q,_N:R,_F:T}
|
|
100
|
+
def to_json(A,**B):return json.dumps(A.build(**B),indent=2)
|
|
101
|
+
def to_text(G,**H):
|
|
102
|
+
E='─';B=G.build(**H);A=[];A.append(E*60);A.append(' LLM Cost Report');A.append(E*60)
|
|
103
|
+
if B[_D]:A.append(f" Since: {B[_D]}")
|
|
104
|
+
if B[_E]:A.append(f" Until: {B[_E]}")
|
|
105
|
+
A.append('');A.append(f" Total cost : ${B[_H]:.4f}");A.append(f" Total calls : {B[_I]}");D=B[_J];A.append(f" Tokens : {D['input']:,} in / {D[_O]:,} out ({D['total']:,})")
|
|
106
|
+
def C(title,data):
|
|
107
|
+
if not data:return
|
|
108
|
+
A.append(f"\n {title}:")
|
|
109
|
+
for(C,B)in data.items():A.append(f" {C:<30} ${B[_B]:.4f} ({B[_C]} calls)")
|
|
110
|
+
C('By Model',B[_K]);C('By Tag',B[_L]);C('By User',B[_M]);C('By Session',B[_N])
|
|
111
|
+
if B[_F]:
|
|
112
|
+
A.append(f"\n {B[_G].capitalize()} Breakdown:")
|
|
113
|
+
for(I,F)in B[_F].items():A.append(f" {I:<15} ${F[_B]:.4f} ({F[_C]} calls)")
|
|
114
|
+
A.append('\n'+E*60);return'\n'.join(A)
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
_D=True
|
|
2
|
+
_C='anonymous'
|
|
3
|
+
_B='default'
|
|
4
|
+
_A=None
|
|
5
|
+
import asyncio,json,os,sqlite3,uuid
|
|
6
|
+
from dataclasses import dataclass,asdict
|
|
7
|
+
from datetime import datetime,timezone
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Optional,List,Dict
|
|
10
|
+
@dataclass
|
|
11
|
+
class UsageRecord:
|
|
12
|
+
model:str;input_tokens:int;output_tokens:int;tag:str=_B;user_id:str=_C;session_id:str=_B;timestamp:str=_A;record_id:str=_A;cost_usd:float=.0
|
|
13
|
+
def __post_init__(A):
|
|
14
|
+
if not A.timestamp:A.timestamp=datetime.now(timezone.utc).isoformat()
|
|
15
|
+
if not A.record_id:A.record_id=str(uuid.uuid4())[:8]
|
|
16
|
+
def to_tuple(A):return A.record_id,A.model,A.input_tokens,A.output_tokens,A.cost_usd,A.tag,A.user_id,A.session_id,A.timestamp
|
|
17
|
+
class BudgetExceeded(Exception):0
|
|
18
|
+
@dataclass
|
|
19
|
+
class BudgetGuard:tag:Optional[str]=_A;user_id:Optional[str]=_A;session_id:Optional[str]=_A;limit_usd:float;alert_at:float=.8;raise_on_exceed:bool=_D
|
|
20
|
+
class CostTracker:
|
|
21
|
+
DEFAULT_DB=Path.home()/'.config'/'llm-cost-tracker'/'usage.db'
|
|
22
|
+
def __init__(A,db_path=_A,pricing=_A,budgets=_A,batch_size=50,flush_interval=1.):A.db_path=Path(db_path or os.environ.get('LLM_COST_DB',A.DEFAULT_DB));A.db_path.parent.mkdir(parents=_D,exist_ok=_D);A.pricing=pricing;A.budgets={A.tag or f"{A.user_id or _C}_{A.session_id or _B}":A for A in budgets or[]};A.batch_size=batch_size;A.flush_interval=flush_interval;A._queue=asyncio.Queue();A._task=_A;A._conn=_A
|
|
23
|
+
async def start(A):A._conn=sqlite3.connect(A.db_path,isolation_level=_A,check_same_thread=False);A._conn.execute('PRAGMA journal_mode=WAL;');A._conn.execute('PRAGMA synchronous=NORMAL;');A.db();A._task=asyncio.create_task(A._writer_loop())
|
|
24
|
+
async def stop(A):
|
|
25
|
+
await A._queue.put(_A)
|
|
26
|
+
if A._task:await A._task
|
|
27
|
+
if A._conn:A._conn.close()
|
|
28
|
+
def db(A):A._conn.execute(' \n CREATE TABLE IF NOT EXISTS usage (\n id TEXT PRIMARY KEY,\n model TEXT,\n input_tokens INTEGER,\n output_tokens INTEGER,\n cost REAL,\n \n tag TEXT,\n user_id TEXT,\n session_id TEXT,\n \n timestamp TEXT\n )\n ');A._conn.execute('CREATE INDEX IF NOT EXISTS idx_tag ON usage(tag)');A._conn.execute('CREATE INDEX IF NOT EXISTS idx_user_id ON usage(user_id)');A._conn.execute('CREATE INDEX IF NOT EXISTS idx_session_id ON usage(session_id)');A._conn.execute('CREATE INDEX IF NOT EXISTS idx_user_session ON usage(user_id, session_id)')
|
|
29
|
+
async def add_call(A,model,input_tokens,output_tokens,tag=_B,user_id=_C,session_id=_C):
|
|
30
|
+
D=output_tokens;C=input_tokens;B=model;E=.0
|
|
31
|
+
if A.pricing:
|
|
32
|
+
try:E=A.pricing.compute_cost(B,C,D)
|
|
33
|
+
except Exception:pass
|
|
34
|
+
F=UsageRecord(model=B,input_tokens=C,output_tokens=D,tag=tag,user_id=user_id,session_id=session_id,cost_usd=E);await A._queue.put(F)
|
|
35
|
+
async def _write_loop(B):
|
|
36
|
+
A=[]
|
|
37
|
+
while _D:
|
|
38
|
+
try:C=await asyncio.wait_for(B._queue.get(),timeout=B.flush_interval)
|
|
39
|
+
except asyncio.TimeoutError:C=_A
|
|
40
|
+
if C is _A:
|
|
41
|
+
if A:B.da(A);A.clear()
|
|
42
|
+
if C is _A and B._queue.empty():break
|
|
43
|
+
continue
|
|
44
|
+
A.append(C)
|
|
45
|
+
if len(A)>=B.batch_size:B.da(A);A.clear()
|
|
46
|
+
def da(D,records):
|
|
47
|
+
F=records;A=D._conn.cursor()
|
|
48
|
+
try:
|
|
49
|
+
A.execute('BEGIN');A.executemany(' \n INSERT INTO usage\n (id, model, input_tokens, output_tokens, cost, tag, user_id, session_id, timestamp)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)\n ',[A.to_tuple()for A in F]);H={A.tag for A in F}
|
|
50
|
+
for B in H:
|
|
51
|
+
if B not in D.budgets:continue
|
|
52
|
+
C=D.budgets[B];A.execute('SELECT SUM(cost) from usage WHERE tag=?',(B,));G=A.fetchone()[0]or .0;E=G/C.limit_usd if C.limit_usd else 0
|
|
53
|
+
if E>=1.:
|
|
54
|
+
if C.raise_on_exceed:raise BudgetExceeded(f"{B} exceeded: {G:.4f}/{C.limit_usd:.4f}")
|
|
55
|
+
elif E>=C.alert_at:print(f"[WARN] {B} budget {E:.0%}")
|
|
56
|
+
A.execute('COMMIT')
|
|
57
|
+
except Exception:A.execute('ROLLBACK');raise
|
|
58
|
+
def cz(A):
|
|
59
|
+
B=[];C=[]
|
|
60
|
+
if A.tag:B.append('tag=?');C.append(A.tag)
|
|
61
|
+
if A.user_id:B.append('user_id=?');C.append(A.user_id)
|
|
62
|
+
if A.session_id:B.append('session_id=?');C.append(A.session_id)
|
|
63
|
+
D=' AND '.join(B)if B else'1=1';return D,C
|
|
64
|
+
def total_cost(A,tag=_A,user_id=_A,session_id=_A):B=A._conn.cursor();C=BudgetGuard(tag,user_id,session_id);D,E=A.cz(C);B.execute(f"SELECT SUM(cost) FROM usage WHERE {D}",E);return B.fetchone()[0]or .0
|
|
65
|
+
def total_tokens(C,tag=_A,user_id=_A,session_id=_A):E=BudgetGuard(tag,user_id,session_id);D=C._conn.cursor();F,G=C.cz(E);D.execute(f"SELECT SUM(input_tokens), SUM(output_tokens) FROM usage WHERE {F}",G);A,B=D.fetchone();A=A or 0;B=B or 0;return{'input':A,'output':B,'total':A+B}
|
|
@@ -26,6 +26,7 @@ from prompt_toolkit.styles import Style
|
|
|
26
26
|
from prompt_toolkit import Application
|
|
27
27
|
from rich.markdown import Markdown
|
|
28
28
|
from rich.columns import Columns
|
|
29
|
+
from rich.console import Group
|
|
29
30
|
from rich.markup import render
|
|
30
31
|
from rich.panel import Panel
|
|
31
32
|
from rich.text import Text
|
|
@@ -41,11 +42,11 @@ from src.tui.utils.trender import display_tool_call,display_tool_result,display_
|
|
|
41
42
|
from langchain_core.messages import HumanMessage
|
|
42
43
|
from langgraph.types import Command
|
|
43
44
|
from dotenv import find_dotenv,load_dotenv
|
|
44
|
-
|
|
45
|
+
a=load_dotenv(find_dotenv())
|
|
45
46
|
class LiveChatUI:
|
|
46
|
-
def __init__(A,agent=_A,saver=_A,workspace=_A,**C):B='#afafff';A.agent=agent;A.saver=saver;A.kwargs=C;A.workspace=re.sub('^(\\/Users\\/[^/]+|\\/home\\/[^/]+)','~',workspace);A.thread_id=1;A.token_count=0;A.mode=_E;A.context=C.get('context',_A);A.instruction_manager=C.get('instruction_manager',_A);A.spinner=Spinner('block');A.max_input_lines=10;A.cancel_event=_A;A.logo_label=Label(LOGO.format(A.workspace),style='class:logo');A.begin_items=[A.logo_label];A.begin_area=HSplit([*A.begin_items],padding=1);A.log_control=ScrollableFormattedLogControl();A.output_area=Window(content=A.log_control,wrap_lines=_B,always_hide_cursor=_C,height=D(weight=1));A.status_label=FormattedTextControl(text=[(_H,' 状态: 等待输入 | Tokens: 0 (⌥ + ⏎ 换行 Esc 中断 ctrl + c 退出)')]);F=FormattedTextControl(text=[('class:spinner',f"{A.spinner.current_frame()}")],show_cursor=_C);G=FormattedTextControl(text=[(_F,f"mode: {A.mode}")]);A.status_bar=VSplit([Window(F,width=D(weight=5),dont_extend_width=_B,dont_extend_height=_B,height=D(weight=1)),Window(A.status_label,width=D(weight=85),height=D(weight=1)),Window(G,width=D(weight=10),dont_extend_width=_B,dont_extend_height=_B,height=D(weight=1))],width=D(weight=100));A.input_box=TextArea(height=1,prompt='> ',multiline=_B,wrap_lines=_B,scrollbar=_B,style='class:input_box');A.input_box.buffer.on_text_changed+=lambda _:A.update_input_area_height(
|
|
47
|
-
def
|
|
48
|
-
def
|
|
47
|
+
def __init__(A,agent=_A,saver=_A,workspace=_A,**C):B='#afafff';A.agent=agent;A.saver=saver;A.kwargs=C;A.workspace=re.sub('^(\\/Users\\/[^/]+|\\/home\\/[^/]+)','~',workspace);A.thread_id=1;A.token_count=0;A.mode=_E;A.context=C.get('context',_A);A.instruction_manager=C.get('instruction_manager',_A);A.spinner=Spinner('block');A.max_input_lines=10;A.cancel_event=_A;A.logo_label=Label(LOGO.format(A.workspace),style='class:logo');A.begin_items=[A.logo_label];A.begin_area=HSplit([*A.begin_items],padding=1);A.log_control=ScrollableFormattedLogControl();A.output_area=Window(content=A.log_control,wrap_lines=_B,always_hide_cursor=_C,height=D(weight=1));A.status_label=FormattedTextControl(text=[(_H,' 状态: 等待输入 | Tokens: 0 (⌥ + ⏎ 换行 Esc 中断 ctrl + c 退出)')]);F=FormattedTextControl(text=[('class:spinner',f"{A.spinner.current_frame()}")],show_cursor=_C);G=FormattedTextControl(text=[(_F,f"mode: {A.mode}")]);A.status_bar=VSplit([Window(F,width=D(weight=5),dont_extend_width=_B,dont_extend_height=_B,height=D(weight=1)),Window(A.status_label,width=D(weight=85),height=D(weight=1)),Window(G,width=D(weight=10),dont_extend_width=_B,dont_extend_height=_B,height=D(weight=1))],width=D(weight=100));A.input_box=TextArea(height=1,prompt='> ',multiline=_B,wrap_lines=_B,scrollbar=_B,style='class:input_box');A.input_box.buffer.on_text_changed+=lambda _:A.update_input_area_height(a);A.kb=KeyBindings();A.e();H=Frame(body=A.input_box,style='class:frame');A.input_items=[A.status_bar,H];A.input_area=HSplit([*A.input_items],padding=0);A.interact_items=[];A.interact_area=HSplit([*A.interact_items],padding=1);E=_F;A.footer=VSplit([Window(FormattedTextControl([(E,f"{A.workspace} (main) ")]),width=D(weight=50)),Window(FormattedTextControl([(E,f"MCP: (0/0) ")]),width=D(weight=20)),Window(FormattedTextControl([(E,'Env: (local) ')]),width=D(weight=20)),Window(FormattedTextControl([(E,f"Model: kimi-k2-0711-preview ")]),wrap_lines=_B,dont_extend_width=_C,always_hide_cursor=_B,width=D(weight=10))],width=D(weight=100),height=1);A.suggest_items=[A.footer];A.suggest_area=HSplit([*A.suggest_items],padding=0);A.logo_area=DynamicContainer(lambda:A.begin_area);A.display_container=DynamicContainer(lambda:A.output_area);A.input_container=DynamicContainer(lambda:A.input_area);A.status_area=DynamicContainer(lambda:A.suggest_area);A.layout=Layout(HSplit([A.logo_area,A.output_area,A.interact_area,A.input_container,A.status_area],padding=0),focused_element=A.input_box);A.style=Style.from_dict({'logo':B,'output':B,'input_box':B,'status':B,'frame.border':B,'suggestions':B,'footer':B,'suggestion.label':B,'suggestion.desc':'#5f5f5f','spinner':B,'suggestion.selected':'bold #00afff'});A.app=Application(layout=A.layout,key_bindings=A.kb,style=A.style,full_screen=_B,mouse_support=_B);A.app.input_area=A.input_box;A.app.kb=A.kb;A.interrupt_tools=C.get('interrupt_tools',[]);A.toolcall_mode='manul'
|
|
48
|
+
def d(A,role='user',spinner='●',status='',tokens=0):A.status_label.text=[(_H,f" 状态: {status} | ({role}) | Tokens: {tokens} (esc + ⏎ 换行 按两次 esc 中断 ctrl + c 退出)")];A.app.invalidate()
|
|
49
|
+
def b(A,workspace=_A,mcp_status=_A,sandbox_status=_A,model_status=_A):
|
|
49
50
|
F=model_status;E=sandbox_status;D=mcp_status;C=workspace;B=_F
|
|
50
51
|
if C is not _A:A._footer_workspace.text=[(B,f"{C}(main) ")]
|
|
51
52
|
if D is not _A:A._footer_context.text=[(B,f"{D} ")]
|
|
@@ -58,7 +59,7 @@ class LiveChatUI:
|
|
|
58
59
|
def clear(A):A.log_control.clear();A.input_box.text='';B=render_info(LOGO.format(A.workspace),style='light_stell_blue',markdown=_C);A.log_control.append_text(B);A.app.invalidate();A.app.layout.focus(A.input_box)
|
|
59
60
|
async def updater(A):await A.spinner.run(A.app)
|
|
60
61
|
async def run_async(A):await asyncio.gather(A.app.run_async(),A.updater())
|
|
61
|
-
def
|
|
62
|
+
def e(A):
|
|
62
63
|
D='enter';C='escape'
|
|
63
64
|
@A.kb.add(D)
|
|
64
65
|
def B(event):
|
|
@@ -77,19 +78,19 @@ class LiveChatUI:
|
|
|
77
78
|
if A.logo_label in A.begin_items:A.begin_items.remove(A.logo_label);A.begin_area.children=list(A.begin_items);J=render_info(LOGO.format(A.workspace),style=D,markdown=_C);A.log_control.append_text(J);A.app.invalidate()
|
|
78
79
|
if B.strip()in['quit','exit','q']:get_app().exit();return
|
|
79
80
|
if B.strip()in['/clear','clear']:A.clear();return
|
|
80
|
-
A.spinner.start();A.
|
|
81
|
+
A.spinner.start();A.c('● user',f"● {B}",style='light_salmon3');await asyncio.sleep(.05)
|
|
81
82
|
if B.strip()in['/commands']and A.instruction_manager:
|
|
82
83
|
E=[]
|
|
83
84
|
for F in A.instruction_manager.list_instructions():E.append(f"/{F.name}: - {F.settings[_D]}")
|
|
84
|
-
K='\n'.join(E);A.
|
|
85
|
+
K='\n'.join(E);A.c('● bot',K,style=D,markdown=_B);return
|
|
85
86
|
if A.instruction_manager:G=A.instruction_manager.parse(B);H,I=G['executed_instruction'],G['message'];B=f"""
|
|
86
87
|
[注意]: 执行用户请求必须严格遵循如下准则:
|
|
87
88
|
{H}
|
|
88
89
|
|
|
89
90
|
用户请求:
|
|
90
91
|
{I}"""if H else I
|
|
91
|
-
C=A.context if C is _A else C;L=await A._handle_stream('○ bot',A._stream_generate(B,C),style=D,markdown=_B,context=C);A.spinner.stop();A.
|
|
92
|
-
def
|
|
92
|
+
C=A.context if C is _A else C;L=await A._handle_stream('○ bot',A._stream_generate(B,C),style=D,markdown=_B,context=C);A.spinner.stop();A.d(spinner='',status='等待输入',tokens=A.token_count);A.app.layout.focus(A.input_box);return L
|
|
93
|
+
def c(A,sender,message,style='green',markdown=_C):D=markdown;C=style;B=message;E=Markdown(B)if D else Text(B,style=C);F=render_panel(sender,E,C,D);A.log_control.append_text(F);A.app.invalidate()
|
|
93
94
|
async def _stream_generate(A,prompt,context=_A):
|
|
94
95
|
B=A.agent.astream({_G:[HumanMessage(content=prompt)]},config={_I:{_J:A.thread_id}},stream_mode=[_G,_K,_L],context=context);A.cancel_event=asyncio.Event()
|
|
95
96
|
async for C in astream_handler(B,interrupt_tools=A.interrupt_tools,tool_mode=A.toolcall_mode):
|
|
@@ -142,7 +143,7 @@ class LiveChatUI:
|
|
|
142
143
|
D.spinner.stop();j=[];p=B['interrupt_id']
|
|
143
144
|
for d in B[m][m]['action_requests']:w=d[P];x=d[Q];y=d[_D];q=await D._handle_human_interrupt(message=f" 允许执行当前函数么? ",options=[{h:'是的,允许当前函数执行',_D:''},{h:'是的,总是允许执行,当前对话过程中不再提示',_D:''},{h:'不, 不允许当前函数执行',_D:''}]);k=['approve',_E,'reject'][q];D.toolcall_mode=_E if k==_E else'manual';j.append({I:k})
|
|
144
145
|
D.spinner.start();await D._handle_stream(a,D._resume_generate(p,j,i),style=b,markdown=c,items=A,context=i);break
|
|
145
|
-
r=
|
|
146
|
+
r=Group(*[A[0]for A in A]);D.log_control.update_last(render_panel(a,r,b,c));D.app.invalidate();await asyncio.sleep(.03);s='';D.d(spinner=s,status='正在生成 ...',tokens=D.token_count)
|
|
146
147
|
return R
|
|
147
148
|
async def _handle_human_interrupt(A,message,options):
|
|
148
149
|
E=asyncio.get_event_loop();C=E.create_future();D=A.app.key_bindings
|
|
@@ -19,20 +19,20 @@ from prompt_toolkit.layout import Layout,HSplit
|
|
|
19
19
|
from prompt_toolkit.styles import Style
|
|
20
20
|
from prompt_toolkit import Application
|
|
21
21
|
class InterruptSelector:
|
|
22
|
-
def __init__(A,description,options,callback):B=description;A.options=options;A.description=B;A.selected_index=0;A.callback=callback;A.rows=A.p();A.list_container=HSplit(A.rows,padding=0);A.markdown=A.r(B);C=Window(content=A.markdown.content,height=A.markdown.height,dont_extend_height=_B,style='class:desc');D=Frame(body=C);A.container=HSplit([C,A.list_container]);A.kb=KeyBindings();A.
|
|
22
|
+
def __init__(A,description,options,callback):B=description;A.options=options;A.description=B;A.selected_index=0;A.callback=callback;A.rows=A.p();A.list_container=HSplit(A.rows,padding=0);A.markdown=A.r(B);C=Window(content=A.markdown.content,height=A.markdown.height,dont_extend_height=_B,style='class:desc');D=Frame(body=C);A.container=HSplit([C,A.list_container]);A.kb=KeyBindings();A.v()
|
|
23
23
|
def r(C,content):A=StringIO();B=Console(file=A,width=80,force_terminal=_B,color_system='truecolor');B.print(Align.left(Markdown(content)),justify='left');return Window(content=FormattedTextControl(ANSI(A.getvalue())),height=D(min=1))
|
|
24
24
|
def p(A):
|
|
25
25
|
E='class:suggestion.selected';B=[]
|
|
26
26
|
for(F,C)in enumerate(A.options):D=F==A.selected_index;G='> 'if D else' ';H=E if D else'class:suggestion.label';I='class:suggestion.desc';J=VSplit([Window(FormattedTextControl([(E,G)]),width=2),Window(FormattedTextControl([(H,C[_A])]),width=60),Window(FormattedTextControl([(I,C[_C])]),wrap_lines=_B,dont_extend_width=False,always_hide_cursor=_B)],height=1);B.append(J)
|
|
27
27
|
return B
|
|
28
|
-
def
|
|
29
|
-
def
|
|
28
|
+
def s(A):A.rows=A.p();A.list_container.children=A.rows;get_app().invalidate()
|
|
29
|
+
def v(A):
|
|
30
30
|
@A.kb.add('up')
|
|
31
31
|
def B(event):
|
|
32
|
-
if A.selected_index>0:A.selected_index-=1;A.
|
|
32
|
+
if A.selected_index>0:A.selected_index-=1;A.s()
|
|
33
33
|
@A.kb.add('down')
|
|
34
34
|
def C(event):
|
|
35
|
-
if A.selected_index<len(A.options)-1:A.selected_index+=1;A.
|
|
35
|
+
if A.selected_index<len(A.options)-1:A.selected_index+=1;A.s()
|
|
36
36
|
@A.kb.add('enter')
|
|
37
37
|
def D(event):A.callback(A.selected_index)
|
|
38
38
|
async def demo():
|
|
@@ -6,44 +6,44 @@ from prompt_toolkit.layout.controls import UIControl,UIContent
|
|
|
6
6
|
from prompt_toolkit.data_structures import Point
|
|
7
7
|
from prompt_toolkit.formatted_text import ANSI
|
|
8
8
|
class ScrollableFormattedLogControl(FormattedTextControl):
|
|
9
|
-
def __init__(A):A.lines=[];A.scroll_offset=0;A._height=0;A.last_count=0;super().__init__(A.
|
|
9
|
+
def __init__(A):A.lines=[];A.scroll_offset=0;A._height=0;A.last_count=0;super().__init__(A.z,focusable=True,show_cursor=False)
|
|
10
10
|
def clear(A):A.lines=[];A.scroll_offset=0;A.last_count=0;A._height=0
|
|
11
|
-
def append_text(A,ansi_text):B=ansi_text.splitlines();A.lines.extend(B);A.last_count=len(B);A.
|
|
11
|
+
def append_text(A,ansi_text):B=ansi_text.splitlines();A.lines.extend(B);A.last_count=len(B);A.x()
|
|
12
12
|
def update_last(A,ansi_text):
|
|
13
13
|
B=ansi_text.splitlines()
|
|
14
14
|
if A.lines:A.lines=A.lines[:-A.last_count]+B
|
|
15
15
|
else:A.lines=B
|
|
16
|
-
A.last_count=len(B);A.
|
|
16
|
+
A.last_count=len(B);A.x()
|
|
17
17
|
def refresh_scroll(A):
|
|
18
18
|
if A._height:A.scroll_offset=max(0,len(A.lines)-A._height)
|
|
19
|
-
def
|
|
19
|
+
def x(A):
|
|
20
20
|
if A._height:A.scroll_offset=max(0,len(A.lines)-A._height)
|
|
21
|
-
def
|
|
21
|
+
def y(A,amount):
|
|
22
22
|
if A._height:B=max(0,len(A.lines)-A._height);A.scroll_offset=max(0,min(A.scroll_offset+amount,B))
|
|
23
23
|
def is_focusable(A):return True
|
|
24
|
-
def
|
|
24
|
+
def z(A):
|
|
25
25
|
C=A._height or 100;D=A.lines[A.scroll_offset:A.scroll_offset+C];B=[]
|
|
26
26
|
for E in D:F=sanitize_ansi_text(E);B.extend(ANSI(F).__pt_formatted_text__());B.append(('','\n'))
|
|
27
27
|
return B
|
|
28
28
|
def create_content(B,width,height):A=height;B._height=A or 100;return super().create_content(width,A)
|
|
29
29
|
def mouse_handler(A,mouse_event):
|
|
30
30
|
B=mouse_event;C=5
|
|
31
|
-
if B.event_type==MouseEventType.SCROLL_UP:A.
|
|
32
|
-
elif B.event_type==MouseEventType.SCROLL_DOWN:A.
|
|
31
|
+
if B.event_type==MouseEventType.SCROLL_UP:A.y(-C);return
|
|
32
|
+
elif B.event_type==MouseEventType.SCROLL_DOWN:A.y(C);return
|
|
33
33
|
return NotImplemented
|
|
34
34
|
class ScrollableLogControl(UIControl):
|
|
35
35
|
def __init__(A):A.lines=[];A.scroll_offset=0;A.visible_lines=[];A._height=0;A.last_count=0
|
|
36
36
|
def clear(A):A.visible_lines=[];A.lines=[];A.scroll_offset=0;A.last_count=0;A._height=0
|
|
37
|
-
def append_text(A,ansi_text):B=ansi_text.splitlines();A.lines.extend(B);A.last_count=len(B);A.
|
|
37
|
+
def append_text(A,ansi_text):B=ansi_text.splitlines();A.lines.extend(B);A.last_count=len(B);A.x()
|
|
38
38
|
def update_last(A,ansi_text):
|
|
39
39
|
B=ansi_text.splitlines()
|
|
40
40
|
if A.lines:A.lines=A.lines[:-A.last_count]+B
|
|
41
41
|
else:A.lines=B
|
|
42
|
-
A.last_count=len(B);A.
|
|
42
|
+
A.last_count=len(B);A.x()
|
|
43
43
|
def refresh_scroll(A):
|
|
44
44
|
if A._height:A.scroll_offset=max(0,len(A.lines)-A._height)
|
|
45
|
-
def
|
|
46
|
-
def
|
|
45
|
+
def x(A):A.scroll_offset=max(0,len(A.lines)-A._height)
|
|
46
|
+
def y(A,amount):A.scroll_offset=max(0,min(A.scroll_offset+amount,max(0,len(A.lines)-A._height)))
|
|
47
47
|
def is_focusable(A):return True
|
|
48
48
|
def create_content(A,width,height):B=height;A._height=B;A.visible_lines=A.lines[A.scroll_offset:A.scroll_offset+B];return UIContent(get_line=A.get_line,line_count=len(A.visible_lines),cursor_position=Point(0,len(A.visible_lines)-1))
|
|
49
49
|
def get_line(B,lineno):
|
|
@@ -52,7 +52,7 @@ class ScrollableLogControl(UIControl):
|
|
|
52
52
|
C=sanitize_ansi_text(B.visible_lines[A]);return ANSI(C).__pt_formatted_text__()
|
|
53
53
|
def mouse_handler(A,mouse_event):
|
|
54
54
|
B=mouse_event
|
|
55
|
-
if B.event_type==MouseEventType.SCROLL_UP:A.
|
|
56
|
-
elif B.event_type==MouseEventType.SCROLL_DOWN:A.
|
|
55
|
+
if B.event_type==MouseEventType.SCROLL_UP:A.y(-1);return
|
|
56
|
+
elif B.event_type==MouseEventType.SCROLL_DOWN:A.y(1);return
|
|
57
57
|
return NotImplemented
|
|
58
58
|
def sanitize_ansi_text(text):return re.sub('[¹²³⁰⁴⁵⁶⁷⁸⁹₀₁₂₃₄₅₆₇₈₉]',' ',text)
|
|
@@ -19,6 +19,7 @@ _A='\n'
|
|
|
19
19
|
from pathlib import Path,PurePath
|
|
20
20
|
import json,os
|
|
21
21
|
from rich.columns import Columns
|
|
22
|
+
from rich.console import Group
|
|
22
23
|
from rich.panel import Panel
|
|
23
24
|
from rich.text import Text
|
|
24
25
|
from rich.tree import Tree
|
|
@@ -27,7 +28,7 @@ from src.tui.components.tdiff import render_diff_as_markdown
|
|
|
27
28
|
from src.tui.components.tdisplay import render_content_with_line_limit
|
|
28
29
|
from src.tui.utils.render import markdown_to_wrapped_text
|
|
29
30
|
def display_tool_call(tool_name,args,events=[]):
|
|
30
|
-
C=tool_name;A=events;E=format_tool_compact(C,args);D=
|
|
31
|
+
C=tool_name;A=events;E=format_tool_compact(C,args);D=l(C,args);B=Tree(Text(_I,style=_J)+Text(f"{E}",style=_C),guide_style=_D)
|
|
31
32
|
if A and len(A)>0:F=display_sub_panel(D,sub_events=A);B.add(F)
|
|
32
33
|
else:B.add(D)
|
|
33
34
|
return B
|
|
@@ -36,14 +37,14 @@ def display_tool_result(tool_name,args=_E,result=_E,events=[]):
|
|
|
36
37
|
if A is _E:A={}
|
|
37
38
|
H=format_tool_compact(G,A);C=G.lower()
|
|
38
39
|
try:
|
|
39
|
-
if C==_K and isinstance(A,dict):B=
|
|
40
|
-
elif C==_L and isinstance(A,dict):B=
|
|
41
|
-
elif C in[_M]and isinstance(A,dict):B=
|
|
42
|
-
elif C==_H and isinstance(A,dict):B=
|
|
43
|
-
elif C==_N and isinstance(A,dict):B=
|
|
44
|
-
elif C==_O and isinstance(A,dict):B=
|
|
45
|
-
elif C==_P and isinstance(A,dict):B=
|
|
46
|
-
else:B=
|
|
40
|
+
if C==_K and isinstance(A,dict):B=o(A)
|
|
41
|
+
elif C==_L and isinstance(A,dict):B=o(A)
|
|
42
|
+
elif C in[_M]and isinstance(A,dict):B=o(A)
|
|
43
|
+
elif C==_H and isinstance(A,dict):B=f(A,D)
|
|
44
|
+
elif C==_N and isinstance(A,dict):B=g(A,D)
|
|
45
|
+
elif C==_O and isinstance(A,dict):B=h(A,D)
|
|
46
|
+
elif C==_P and isinstance(A,dict):B=i(A,D)
|
|
47
|
+
else:B=j(A,D)
|
|
47
48
|
except Exception as I:import traceback as J;B=Text(f"Error: {str(I)}\n{J.format_exc()}",style='red')
|
|
48
49
|
F=Tree(Text(_I,style=_J)+Text(f"{H}",style=_C),guide_style=_D)
|
|
49
50
|
if E and len(E)>0:K=display_sub_panel(B,sub_events=E);F.add(K)
|
|
@@ -101,12 +102,12 @@ def format_tool_compact(name,args):
|
|
|
101
102
|
I=', '.join(M)
|
|
102
103
|
if len(I)>50:I=I[:47]+_B
|
|
103
104
|
return f"{J}({I})"
|
|
104
|
-
def
|
|
105
|
+
def l(tool_name,arguments):
|
|
105
106
|
D=' Will search for pattern';C=arguments;B='bold grey';A=tool_name.lower()
|
|
106
107
|
if A==_H:return Text(f"running ...",style=B)
|
|
107
108
|
if A==_M:return Text(f" Executing ...",style=B)
|
|
108
|
-
elif A==_L:return
|
|
109
|
-
elif A==_K:return
|
|
109
|
+
elif A==_L:return m(C)
|
|
110
|
+
elif A==_K:return k(C)
|
|
110
111
|
elif A in[_N,_O]:return Text(D,style=B)
|
|
111
112
|
elif A==_P:return Text(D,stype=B)
|
|
112
113
|
else:return Text(' Executing ...',style=B)
|
|
@@ -121,13 +122,13 @@ def display_sub_panel(result=_E,sub_events=[]):
|
|
|
121
122
|
if isinstance(C,str):A.append(markdown_to_wrapped_text(f"{C[:200]}",prefix=_E))
|
|
122
123
|
else:A.append(C)
|
|
123
124
|
else:A.append(markdown_to_wrapped_text(f"calling ...",prefix=_E))
|
|
124
|
-
J=
|
|
125
|
-
def
|
|
125
|
+
J=Group(*[A for A in A]);K=Box(' \n \n \n \n \n \n \n ');L=Panel(J,title_align='left',box=K);return L
|
|
126
|
+
def k(arguments):
|
|
126
127
|
A=arguments;B=A.get(_F,'');C=A.get('old_string','');D=A.get('new_string','');E=''
|
|
127
128
|
if os.path.exists(B):
|
|
128
129
|
with open(B,'r',encoding='utf-8')as F:G=F.read()
|
|
129
130
|
H=D if C==''else G.replace(C,D,1);I=render_diff_as_markdown(E,H);return I
|
|
130
|
-
def
|
|
131
|
+
def m(arguments):
|
|
131
132
|
D=arguments;A=D.get(_F,'');B=D.get(_R,'')
|
|
132
133
|
if os.path.exists(A):C=open(A,'r').read()
|
|
133
134
|
else:C=''
|
|
@@ -136,7 +137,7 @@ def g(arguments):
|
|
|
136
137
|
E=recognize_language(A)
|
|
137
138
|
if E:G=f"```\n{E}\n{B}\n```";return render_content_with_line_limit(G,'',100)
|
|
138
139
|
else:return render_content_with_line_limit(B,'',100)
|
|
139
|
-
def
|
|
140
|
+
def o(arguments):
|
|
140
141
|
B=arguments.get(_F,'');C=''
|
|
141
142
|
if os.path.exists(B):C=open(B,'r').read()
|
|
142
143
|
E=C.split(_A);F=len(E);A=50
|
|
@@ -145,7 +146,7 @@ def f(arguments):
|
|
|
145
146
|
H=recognize_language(B)
|
|
146
147
|
if H:J=f"```{H}\n{D}\n```";return render_content_with_line_limit(J,'',-1)
|
|
147
148
|
else:return render_content_with_line_limit(D,'',-1)
|
|
148
|
-
def
|
|
149
|
+
def n(arguments,result):
|
|
149
150
|
C=result.strip().split(_A);F=len(C);B=[];D=10;G=C[:D]
|
|
150
151
|
for(H,A)in enumerate(G):
|
|
151
152
|
I=''if H==0 else''
|
|
@@ -154,7 +155,7 @@ def i(arguments,result):
|
|
|
154
155
|
E=F-D
|
|
155
156
|
if E>0:B.append(f" ... and {E} more lines")
|
|
156
157
|
return Text(_A.join(B),style=_G)
|
|
157
|
-
def
|
|
158
|
+
def f(arguments,result):
|
|
158
159
|
C=result.strip().split(_A);F=len(C);B=[];D=10;G=C[:D]
|
|
159
160
|
for(H,A)in enumerate(G):
|
|
160
161
|
I=''if H==0 else''
|
|
@@ -163,7 +164,7 @@ def o(arguments,result):
|
|
|
163
164
|
E=F-D
|
|
164
165
|
if E>0:B.append(f" ... and {E} more lines")
|
|
165
166
|
return Text(_A.join(B),style=_G)
|
|
166
|
-
def
|
|
167
|
+
def g(arguments,result):
|
|
167
168
|
C=result.strip().split(_A);F=len(C);B=[];D=10;G=C[:D]
|
|
168
169
|
for(H,A)in enumerate(G):
|
|
169
170
|
I=''if H==0 else''
|
|
@@ -172,7 +173,7 @@ def m(arguments,result):
|
|
|
172
173
|
E=F-D
|
|
173
174
|
if E>0:B.append(f" ... and {E} more lines")
|
|
174
175
|
return Text(_A.join(B),style=_G)
|
|
175
|
-
def
|
|
176
|
+
def h(arguments,result):
|
|
176
177
|
C=result.strip().split(_A);F=len(C);B=[];D=10;G=C[:D]
|
|
177
178
|
for(H,A)in enumerate(G):
|
|
178
179
|
I=''if H==0 else''
|
|
@@ -181,7 +182,7 @@ def n(arguments,result):
|
|
|
181
182
|
E=F-D
|
|
182
183
|
if E>0:B.append(f" ... and {E} more lines")
|
|
183
184
|
return Text(_A.join(B),style=_G)
|
|
184
|
-
def
|
|
185
|
+
def i(arguments,result):
|
|
185
186
|
C=result.strip().split(_A);F=len(C);B=[];D=10;G=C[:D]
|
|
186
187
|
for(H,A)in enumerate(G):
|
|
187
188
|
I=''if H==0 else''
|
|
@@ -190,7 +191,7 @@ def l(arguments,result):
|
|
|
190
191
|
E=F-D
|
|
191
192
|
if E>0:B.append(f" ... and {E} more lines")
|
|
192
193
|
return Text(_A.join(B),style=_G)
|
|
193
|
-
def
|
|
194
|
+
def j(arguments,result):
|
|
194
195
|
B=result;A=str(B)if B else'tool completed successfully'
|
|
195
196
|
if len(A)>500:C=A[:500]+'\n ... (truncated)'
|
|
196
197
|
else:C=A
|