@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.
Files changed (34) hide show
  1. package/dist/python-src/main.py +2 -1
  2. package/dist/python-src/skill_agent.py +144 -0
  3. package/dist/python-src/src/agents/token_tracker.py +44 -0
  4. package/dist/python-src/src/claw/__init__.py +0 -0
  5. package/dist/python-src/src/claw/bus/__init__.py +3 -0
  6. package/dist/python-src/src/claw/bus/events.py +10 -0
  7. package/dist/python-src/src/claw/bus/queue.py +43 -0
  8. package/dist/python-src/src/claw/channels/__init__.py +3 -0
  9. package/dist/python-src/src/claw/channels/base.py +30 -0
  10. package/dist/python-src/src/claw/channels/feishu.py +89 -0
  11. package/dist/python-src/src/claw/channels/manager.py +47 -0
  12. package/dist/python-src/src/claw/config/schema.py +46 -0
  13. package/dist/python-src/src/managers/manager_agent.py +2 -2
  14. package/dist/python-src/src/managers/manager_instruction.py +7 -7
  15. package/dist/python-src/src/managers/manager_skill.py +121 -0
  16. package/dist/python-src/src/managers/sandbox.py +3 -3
  17. package/dist/python-src/src/middlewares/dynamic_content.py +3 -3
  18. package/dist/python-src/src/middlewares/hitl.py +3 -3
  19. package/dist/python-src/src/middlewares/memory.py +2 -2
  20. package/dist/python-src/src/middlewares/skill.py +27 -0
  21. package/dist/python-src/src/middlewares/subagents.py +2 -2
  22. package/dist/python-src/src/middlewares/summary.py +37 -37
  23. package/dist/python-src/src/stream/formatter.py +19 -19
  24. package/dist/python-src/src/trackers/__init__.py +0 -0
  25. package/dist/python-src/src/trackers/token/__init__.py +0 -0
  26. package/dist/python-src/src/trackers/token/cli.py +45 -0
  27. package/dist/python-src/src/trackers/token/pricing.py +39 -0
  28. package/dist/python-src/src/trackers/token/report.py +114 -0
  29. package/dist/python-src/src/trackers/token/tracker.py +65 -0
  30. package/dist/python-src/src/tui/chatui.py +11 -10
  31. package/dist/python-src/src/tui/components/tlist.py +5 -5
  32. package/dist/python-src/src/tui/components/tscroll_panel.py +14 -14
  33. package/dist/python-src/src/tui/utils/trender.py +23 -22
  34. package/package.json +1 -1
@@ -0,0 +1,121 @@
1
+ _C='managed'
2
+ _B='user'
3
+ _A='project'
4
+ from typing import Optional,Dict,Any,List,Tuple
5
+ from dataclasses import dataclass,field
6
+ from pathlib import Path
7
+ import logging,yaml,re,os
8
+ logger=logging.getLogger(__name__)
9
+ @dataclass
10
+ class Skill:
11
+ name:str;description:str;content:str;path:Path;license:Optional[str]=None;allowed_tools:List[str]=field(default_factory=list);metadata:Dict[str,Any]=field(default_factory=dict);location:str=_A
12
+ @classmethod
13
+ def from_skill_md(C,skill_md_path,location=_A):
14
+ B=skill_md_path
15
+ if not B.exists():return
16
+ F=B.read_text(encoding='utf-8');A,G=C.br(F)
17
+ if not A:return
18
+ D=A.get('name');E=A.get('description')
19
+ if not D or not E:return
20
+ return C(name=D,description=E,content=G.strip(),path=B.parent,license=A.get('license'),allowed_tools=A.get('allowed-tools',[])or[],metadata=A.get('metadata',{})or{},location=location)
21
+ @staticmethod
22
+ def br(content):
23
+ A=content;C='^---\\s*\\n(.*?)\\n---\\s*\\n(.*)$';B=re.match(C,A,re.DOTALL)
24
+ if not B:return{},A
25
+ D=B.group(1);E=B.group(2)
26
+ try:F=yaml.safe_load(D)or{}
27
+ except yaml.YAMLError:return{},A
28
+ return F,E
29
+ def get_prompt(A):B=f"Loading: {A.name} \nBase directory: {A.path}\n \n ";return B+A.content
30
+ def to_xml(A):return f"<skill>\n<name>{A.name}</name>\n<description>{A.description}</description>\n<location>{A.location}</location>\n</skill>"
31
+ def __repr__(A):return f"Skill(name={A.name!r}, location={A.location!r})"
32
+ class SkillManager:
33
+ SKILL_DIRS=['.claude/skills','.autodev/skills'];SKILL_FILE='SKILL.md'
34
+ def __init__(A,project_root=None,home_path=Path.home()):C=home_path;B=project_root;A._skills={};A._skills_by_location={_A:[],_B:[],_C:[]};A.project_root=Path(B)if B else Path.cwd();A.home_dir=Path(C)if C else Path.home()
35
+ def register(B,skill):
36
+ A=skill;C=B._skills.get(A.name)
37
+ if C:
38
+ D={_A:0,_B:1,_C:2};E=D.get(C.location,99);F=D.get(A.location,99)
39
+ if F>=E:return False
40
+ B._skills[A.name]=A;B._skills_by_location[A.location].append(A);return True
41
+ def get(A,name):return A._skills.get(name)
42
+ def exists(A,name):return name in A._skills
43
+ def list_all(A):return list(A._skills.values())
44
+ def list_by_location(A,location):return A._skills_by_location.get(location,[])
45
+ def clear(A):
46
+ A._skills.clear()
47
+ for B in A._skills_by_location:A._skills_by_location[B].clear()
48
+ def generate_skills_prompt(E,char_budget=10000):
49
+ C=E.list_all()
50
+ if not C:return'Execute a skill within the main conversation.\n\nNo skills are currently available. Skills can be added to:\n- .claude/skills/ (project-level)\n- ~/.claude/skills/ (user-level)\n- .minion/skills/ (project-level)\n- ~/.minion/skills/ (user-level) \n '
51
+ A=[];D=0
52
+ for F in C:
53
+ B=F.to_xml()
54
+ if D+len(B)>char_budget:break
55
+ A.append(B);D+=len(B)
56
+ if not A:return'Execute a skill within the main conversation.\n\nNo skills are currently available. Skills can be added to:\n- .claude/skills/ (project-level)\n- ~/.claude/skills/ (user-level)\n- .minion/skills/ (project-level)\n- ~/.minion/skills/ (user-level) \n'
57
+ G='\n'.join(A);return f'''Execute a skill within the main conversation.
58
+
59
+ <skills_instructions>
60
+ When users ask you to perform tasks, check if any of the available skills below can help complete the task more effectively. Skills provide specialized capabilities and domain knowledge.
61
+
62
+ How to use skills:
63
+ - Invoke skills using skill tool with the skill name only (no arguments)
64
+ - When you invoke a skill, you will see <command-message>The "{{name}}" skill is loading</command-message>
65
+ - The skill\'s prompt will expand and provide detailed instructions on how to complete the task
66
+ - Base directory provided in output for resolving bundled resources (references/, scripts/, assets/)
67
+
68
+ Important:
69
+ - Only use skills listed in <available_skills> below
70
+ - Do not invoke a skill that is already running
71
+ </skills_instructions>
72
+
73
+ <available_skills>
74
+ {G}
75
+ </available_skills>'''
76
+ def __len__(A):return len(A._skills)
77
+ def __contains__(A,name):return name in A._skills
78
+ def __iter__(A):return iter(A._skills.values())
79
+ def get_search_paths(A):
80
+ B=[]
81
+ for C in A.SKILL_DIRS:D=A.project_root/C;B.append((D,_A))
82
+ for C in A.SKILL_DIRS:E=A.home_dir/C;B.append((E,_B))
83
+ return B
84
+ def discover_skills(D,skills_dir):
85
+ A=skills_dir
86
+ if not A.exists()or not A.is_dir():return[]
87
+ B=[]
88
+ for C in A.iterdir():
89
+ if C.is_dir():
90
+ E=C/D.SKILL_FILE
91
+ if E.exists():B.append(E)
92
+ else:
93
+ for F in C.iterdir():
94
+ if F.is_dir():
95
+ G=F/D.SKILL_FILE
96
+ if G.exists():B.append(G)
97
+ return B
98
+ def load_skill(D,skill_md_path,location):
99
+ A=skill_md_path
100
+ try:
101
+ B=Skill.from_skill_md(A,location)
102
+ if B:logger.debug(f"Loaded skill: {B.name} from {A}")
103
+ else:logger.warning(f"Failed to parse skill: {A}")
104
+ return B
105
+ except Exception as C:logger.error(f"Error loading skill from {A}: {C}");return
106
+ def load_all(A):
107
+ for(D,C)in A.get_search_paths():
108
+ E=A.discover_skills(D)
109
+ for F in E:
110
+ B=A.load_skill(F,C)
111
+ if B:
112
+ G=A.register(B)
113
+ if G:logger.info(f"Registered skill: {B.name} ({C})")
114
+ else:logger.debug(f"Skipped skill {B.name} - already registered from high priority location")
115
+ return A
116
+ def reload(A):A.clear();return A.load_all()
117
+ def main():
118
+ A=SkillManager(project_root=Path.cwd());A.load_all();C=A.list_all();print(f"Loaded {len(C)} skills");B=A.get('arxiv-search')
119
+ if B:print(B.get_prompt())
120
+ D=A.generate_skills_prompt(char_budget=5000);print(D);A.reload()
121
+ if __name__=='__main__':main()
@@ -21,14 +21,14 @@ sys.path.append(PROJ_PATH)
21
21
  logger=logging.getLogger(__file__)
22
22
  class Container:
23
23
  def __init__(A,user_id=_C,workspace_path=_B,dockerfile_path='.',image_name=_J,container_basename='sandbox'):A.client=docker.from_env();A.container_basename=container_basename;A.workspace_path=workspace_path;A.dockerfile_path=dockerfile_path;A.image_name=image_name;A.sock=_B;A.queue=_B;A.container=_B;A.user_id=user_id;A.images={_G:'ubuntu:22.04',_H:'python:3.12-slim','node':'node:20-bullseye-slim',_C:_J}
24
- def br(A):
24
+ def bs(A):
25
25
  try:
26
26
  B=A.client.images.get(A.image_name)
27
27
  if B:logger.info(f"Image {A.image_name} exists.");return _A
28
28
  else:logger.info(f"Image {A.image_name} not exists.");return _E
29
29
  except docker.errors.ImageNotFound:logger.error(f"Image {A.image_name} not exists.");return _E
30
30
  except Exception as C:logger.error(f"检查镜像时出错: {str(C)}");return _E
31
- def bs(B):
31
+ def bt(B):
32
32
  F='arm64';print('🔧 Building Docker image...')
33
33
  try:
34
34
  C=platform.machine()
@@ -44,7 +44,7 @@ class Container:
44
44
  except Exception as D:logger.error(f"An unexpected error occurred: {D}");raise
45
45
  def get_or_create_container(A,container_name,image_type=_C):
46
46
  E='running';C=image_type;B=container_name;D=_F;F=A.workspace_path;B=f"{A.container_basename}_{A.user_id}"
47
- if C==_C and not A.br():A.bs()
47
+ if C==_C and not A.bs():A.bt()
48
48
  try:
49
49
  A.container=A.client.containers.get(B)
50
50
  if A.container.status==E:logger.info(f"容器 {B} 已存在且运行中,直接复用")
@@ -19,11 +19,11 @@ logger=logging.getLogger(__name__)
19
19
  class DynamicContentState(AgentState):review_contents:NotRequired[Annotated[dict[str,str],PrivateStateAttr]];security_contents:NotRequired[Annotated[dict[str,str],PrivateStateAttr]]
20
20
  class DynamicContentUpdate(TypedDict):review_contents:dict[str,str];security_contents:dict[str,str]
21
21
  REVIEW_SYSTEM_PROMPT=' \n<review_rules>\n {review_rules}\n</review_rules>\n\n<review_guidelines>\n 如果用户要求对项目进行 Review ,请严格依照 review_rules 标签中的规则 对项目进行 Review 。\n</review_guidelines>\n'
22
- SECURITY_SYSTEM_PROMPT=' \n<security_rules>\n {security_rules}\n</security_rules>\n\n<<security_guidelines>\n 如果用户要求对项目进行 安全检查 ,请严格依照 security_rules 标签中的规则 对项目进行安全检查 。\n</security_guidelines>\n'
22
+ SECURITY_SYSTEM_PROMPT=' \n<security_rules>\n {security_rules}\n</security_rules>\n\n<security_guidelines>\n 如果用户要求对项目进行 安全检查 ,请严格依照 security_rules 标签中的规则 对项目进行安全检查 。\n</security_guidelines>\n'
23
23
  class DynamicContentMiddleware(AgentMiddleware[DynamicContentState,ContextT,ResponseT]):
24
24
  state_schema=DynamicContentState
25
25
  def __init__(A,*,home_path=os.path.expanduser('~'),project_path=os.getcwd()):A.home_path=home_path;A.project_path=project_path
26
- def cu(G,prompt,content_type,contents):
26
+ def cv(G,prompt,content_type,contents):
27
27
  E='( No rules loaded)';C=contents;B=content_type;A=prompt
28
28
  if not C:return A.format(**{B:E})
29
29
  D=[f"{A}\n{C[A]}"for A in C]
@@ -58,7 +58,7 @@ class DynamicContentMiddleware(AgentMiddleware[DynamicContentState,ContextT,Resp
58
58
  with open(H,_A,encoding=_B)as B:F[H]=B.read()
59
59
  return DynamicContentUpdate(review_contents=C,security_contents=F)
60
60
  def modify_request(C,request):
61
- B=request;F=B.state.get(_C,{});G=B.state.get(_D,{});D=C.cu(REVIEW_SYSTEM_PROMPT,'review_rules',F);E=C.cu(SECURITY_SYSTEM_PROMPT,'security_rules',G);A=B.system_message
61
+ B=request;F=B.state.get(_C,{});G=B.state.get(_D,{});D=C.cv(REVIEW_SYSTEM_PROMPT,'review_rules',F);E=C.cv(SECURITY_SYSTEM_PROMPT,'security_rules',G);A=B.system_message
62
62
  if D:A=append_to_system_message(A,D)
63
63
  if E:A=append_to_system_message(A,E)
64
64
  return B.override(system_message=A)
@@ -21,12 +21,12 @@ class ReviewConfig(TypedDict):action_name:str;allowed_decisions:list[DecisionTyp
21
21
  class HITLRequest(TypedDict):action_requests:list[ActionRequest];review_configs:list[ReviewConfig]
22
22
  class HumanInTheLoopMiddleware(AgentMiddleware[StateT,ContextT]):
23
23
  def __init__(A,interrupt_on=[],*,description_perfix='Tool execution requires approval'):super().__init__();A.interrupt_on=interrupt_on;A.description_prefix=description_perfix
24
- def cd(D,tool_call):A=tool_call;B=A[_E];C=A[_F];E=A['id'];F=f"{D.description_prefix}\n\nTool: {B}\nArgs: {C}";G=ActionRequest(name=B,id=E,args=C,description=F);H=ReviewConfig(action_name=B,allowed_decisions=[_B,_A,_C]);return G,H
24
+ def ce(D,tool_call):A=tool_call;B=A[_E];C=A[_F];E=A['id'];F=f"{D.description_prefix}\n\nTool: {B}\nArgs: {C}";G=ActionRequest(name=B,id=E,args=C,description=F);H=ReviewConfig(action_name=B,allowed_decisions=[_B,_A,_C]);return G,H
25
25
  async def awrap_tool_call(E,request,handler):
26
26
  C=handler;A=request;F=A.tool_call['id'];D=A.tool_call[_E];R=A.tool_call[_F];G=A.runtime;H=G.context;I=H.tool_mode
27
27
  if I==_A:return await C(A)
28
28
  if len(E.interrupt_on)>0 and not D.strip().lower()in E.interrupt_on:return await C(A)
29
- J,K=E.cd(A.tool_call);L=HITLRequest(action_requests=[J],review_configs=[K]);M=interrupt(L);B=M[_G][0][_D]
29
+ J,K=E.ce(A.tool_call);L=HITLRequest(action_requests=[J],review_configs=[K]);M=interrupt(L);B=M[_G][0][_D]
30
30
  if B==_B:return await C(A)
31
31
  if B==_A:return await C(A)
32
32
  if B==_C:N=f"User rejected the tool call for `{D}` with id {F}";O=ToolMessage(content=N,name=D,tool_call_id=F,status='error');return O
@@ -35,7 +35,7 @@ class HumanInTheLoopMiddleware(AgentMiddleware[StateT,ContextT]):
35
35
  C=handler;A=request;F=A.tool_call['id'];D=A.tool_call[_E];R=A.tool_call[_F];H=A.runtime;I=H.context;J=I.tool_mode
36
36
  if J==_A:return C(A)
37
37
  if len(E.interrupt_on)>0 and not D.strip().lower()in E.interrupt_on:return C(A)
38
- K,L=E.cd(A.tool_call);M=HITLRequest(action_requests=[K],review_configs=[L]);G=interrupt(M);print(f"[HITL] -> Human decision: {G}");B=G[_G][0][_D]
38
+ K,L=E.ce(A.tool_call);M=HITLRequest(action_requests=[K],review_configs=[L]);G=interrupt(M);print(f"[HITL] -> Human decision: {G}");B=G[_G][0][_D]
39
39
  if B==_B:return C(A)
40
40
  if B==_A:return C(A)
41
41
  if B==_C:N=f"User rejected the tool call for `{D}` with id {F}";O=ToolMessage(content=N,name=D,tool_call_id=F,status='error');return O
@@ -14,7 +14,7 @@ MEMORY_SYSTEM_PROMPT=' \n<agent_memory>\n{agent_memory}\n</agent_memory>\n\n<mem
14
14
  class MemoryMiddleware(AgentMiddleware[MemoryState,ContextT,ResponseT]):
15
15
  state_schema=MemoryState
16
16
  def __init__(A,*,home_path=os.path.expanduser('~'),project_path=os.getcwd()):A.home_path=home_path;A.project_path=project_path
17
- def ce(D,contents):
17
+ def cf(D,contents):
18
18
  C='(No memory loaded)';A=contents
19
19
  if not A:return MEMORY_SYSTEM_PROMPT.format(agent_memory=C)
20
20
  B=[f"{B}\n{A[B]}"for B in D.sources if A.get(B)]
@@ -39,6 +39,6 @@ class MemoryMiddleware(AgentMiddleware[MemoryState,ContextT,ResponseT]):
39
39
  H=f"Failed to download {C}: {A.error}";raise ValueError(H)
40
40
  if A.content is not None:E[C]=A.content.decode(_B);logger.debug('Loaded memory from: %s',C)
41
41
  return MemoryStateUpdate(memory_contents=E)
42
- def modify_request(B,request):A=request;C=A.state.get(_A,{});D=B.ce(C);E=append_to_system_message(A.system_message,D);return A.override(system_message=E)
42
+ def modify_request(B,request):A=request;C=A.state.get(_A,{});D=B.cf(C);E=append_to_system_message(A.system_message,D);return A.override(system_message=E)
43
43
  def wrap_model_call(A,request,handler):B=A.modify_request(request);return handler(B)
44
44
  async def awrap_model_call(A,request,handler):B=A.modify_request(request);return await handler(B)
@@ -0,0 +1,27 @@
1
+ from collections.abc import Awaitable,Callable
2
+ from langchain.agents.middleware.types import AgentMiddleware,ModelRequest,ModelResponse
3
+ from langchain.tools import BaseTool,ToolRuntime
4
+ from langchain_core.language_models import BaseChatModel
5
+ from langchain_core.messages import HumanMessage,ToolMessage
6
+ from langchain_core.runnables import Runnable
7
+ from langchain_core.tools import StructuredTool
8
+ from langgraph.types import Command
9
+ from langchain_core.tools import tool
10
+ from src.managers.manager_skill import SkillManager
11
+ def generate_skill_tool(manager):
12
+ B=manager;A=' Execute a skill within the main conversation.\n\nSkills are folders of instructions, scripts, and resources that Claude loads\ndynamically to improve performance on specialized tasks.\n\nUsage:\n- Invoke skills using this tool with the skill name only (no arguments)\n- When you invoke a skill, its prompt will expand and provide detailed instructions\n- Only use skills listed in <available_skills> in the system prompt\n\nImportant:\n- Only use skills that are listed as available\n- Do not use skills that are not listed as available\n'
13
+ @tool(description=A)
14
+ def C(skill):
15
+ D='success';C=skill;A=B.get(C)
16
+ if A is None:E=[A.name for A in B.list_all()];return{D:False,'error':f"Unknown skill: {C}",'available_skills':E[:10],'hint':'Use one of the available skills listed above'}
17
+ F=A.get_prompt();return{D:True,'skill_name':A.name,'skill_description':A.description,'skill_location':A.location,'skill_path':str(A.path),'prompt':F,'message':f'The "{A.name}" skill is loading','allowed_tools':A.allowed_tools}
18
+ return C
19
+ class SkillMiddleware(AgentMiddleware):
20
+ def __init__(A,*,home_path,workspace):B=home_path;super().__init__();A.workspace=workspace;A.home_path=B;A.manager=SkillManager(A.workspace,B);A.manager.load_all();C=generate_skill_tool(A.manager);A.tools=[C]
21
+ def modify_request(D,request):
22
+ A=request;B=D.manager.generate_skills_prompt(5000)
23
+ if A.system_prompt:C=A.system_prompt+'\n\n'+B
24
+ else:C=B
25
+ return A.override(system_prompt=C)
26
+ def wrap_model_call(A,request,handler):B=A.modify_request(request);return handler(B)
27
+ async def awrap_model_call(A,request,handler):B=A.modify_request(request);return await handler(B)
@@ -79,7 +79,7 @@ async def astream_handler(stream,runtime):
79
79
  if isinstance(A,AIMessage)and len(A.tool_calls)==0:C=A
80
80
  if isinstance(A,ToolMessage):G=A.name;N=A.content;H=A.tool_call_id;I={_E:'tool_result',_G:D.tool_call_id,_H:H,_I:'',_C:G,_D:{},_J:N,_K:_O};J(json.dumps(I,indent=2,ensure_ascii=_L))
81
81
  O={_B:[C]if C and isinstance(C,AIMessage)else[]};return O
82
- def bz(*,default_model,default_tools,default_middleware,default_interrupt_on,subagents,general_purpose_agent,task_description=_A):
82
+ def cd(*,default_model,default_tools,default_middleware,default_interrupt_on,subagents,general_purpose_agent,task_description=_A):
83
83
  G='Tool call ID is required for subagent invocation';F='custom';A=task_description;B,H=cc(default_model=default_model,default_tools=default_tools,default_middleware=default_middleware,default_interrupt_on=default_interrupt_on,subagents=subagents,general_purpose_agent=general_purpose_agent);C='\n'.join(H)
84
84
  def D(result,tool_call_id):A=result;B={A:B for(A,B)in A.items()if A not in _EXCLUDED_STATE_KEYS};C=A[_B][-1];D=I(C);return Command(update={**B,_B:[ToolMessage(D,tool_call_id=tool_call_id)]})
85
85
  def I(msg):
@@ -110,7 +110,7 @@ def bz(*,default_model,default_tools,default_middleware,default_interrupt_on,sub
110
110
  return D(L,A.tool_call_id)
111
111
  return StructuredTool.from_function(name='task',func=J,coroutine=K,description=A)
112
112
  class SubAgentMiddleware(AgentMiddleware):
113
- def __init__(A,*,default_model,default_tools=_A,default_middleware=_A,default_interrupt_on=_A,subagents=_A,system_prompt=TASK_SYSTEM_PROMPT,general_purpose_agent=True,task_description=_A):super().__init__();A.system_prompt=system_prompt;B=bz(default_model=default_model,default_tools=default_tools or[],default_middleware=default_middleware,default_interrupt_on=default_interrupt_on,subagents=subagents or[],general_purpose_agent=general_purpose_agent,task_description=task_description);A.tools=[B]
113
+ def __init__(A,*,default_model,default_tools=_A,default_middleware=_A,default_interrupt_on=_A,subagents=_A,system_prompt=TASK_SYSTEM_PROMPT,general_purpose_agent=True,task_description=_A):super().__init__();A.system_prompt=system_prompt;B=cd(default_model=default_model,default_tools=default_tools or[],default_middleware=default_middleware,default_interrupt_on=default_interrupt_on,subagents=subagents or[],general_purpose_agent=general_purpose_agent,task_description=task_description);A.tools=[B]
114
114
  def wrap_model_call(B,request,handler):
115
115
  C=handler;A=request
116
116
  if B.system_prompt is not _A:D=A.system_prompt+'\n\n'+B.system_prompt if A.system_prompt else B.system_prompt;return C(A.override(system_prompt=D))
@@ -27,7 +27,7 @@ ContextFraction=tuple[Literal[_D],float]
27
27
  ContextTokens=tuple[Literal[_E],int]
28
28
  ContextMessages=tuple[Literal[_B],int]
29
29
  ContextSize=ContextFraction|ContextTokens|ContextMessages
30
- def ck(model):
30
+ def cm(model):
31
31
  if model._llm_type=='anthropic-chat':return partial(count_tokens_approximately,chars_per_token=3.3)
32
32
  return count_tokens_approximately
33
33
  class SummaryState(AgentState):compact:NotRequired[bool]=_F
@@ -44,61 +44,61 @@ class SummarizationMiddleware(AgentMiddleware):
44
44
  if isinstance(C,str):C=init_chat_model(C)
45
45
  A.model=C
46
46
  if B is _A:A.trigger=_A;G=[]
47
- elif isinstance(B,list):I=[A.ch(B,N)for B in B];A.trigger=I;G=I
48
- else:J=A.ch(B,N);A.trigger=J;G=[J]
49
- A._trigger_conditions=G;A.keep=A.ch(F,'keep')
50
- if H is count_tokens_approximately:A.token_counter=ck(A.model)
47
+ elif isinstance(B,list):I=[A.cq(B,N)for B in B];A.trigger=I;G=I
48
+ else:J=A.cq(B,N);A.trigger=J;G=[J]
49
+ A._trigger_conditions=G;A.keep=A.cq(F,'keep')
50
+ if H is count_tokens_approximately:A.token_counter=cm(A.model)
51
51
  else:A.token_counter=H
52
52
  A.summary_prompt=summary_prompt;A.trim_tokens_to_summarize=trim_tokens_to_summarize;K=any(A[0]==_D for A in A._trigger_conditions)
53
53
  if A.keep[0]==_D:K=_C
54
- if K and A.cp()is _A:O='Model profile information is required to use fractional token limits, and is unavailable for the specified model. Please use absolute token counts instead, or pass `\n\nChatModel(..., profile={"max_input_tokens": ...})`.\n\nwith a desired integer value of the model\'s maximum input tokens.';raise ValueError(O)
55
- def cl(B,state):A=state.get(_G,_F);return bool(A)
54
+ if K and A.cj()is _A:O='Model profile information is required to use fractional token limits, and is unavailable for the specified model. Please use absolute token counts instead, or pass `\n\nChatModel(..., profile={"max_input_tokens": ...})`.\n\nwith a desired integer value of the model\'s maximum input tokens.';raise ValueError(O)
55
+ def ct(B,state):A=state.get(_G,_F);return bool(A)
56
56
  @override
57
57
  def before_model(self,state,runtime):
58
- C=state;A=self;B=C[_B];A.cq(B);E=A.token_counter(B);F=A.cl(C);G=A.co(B,E)
58
+ C=state;A=self;B=C[_B];A.co(B);E=A.token_counter(B);F=A.ct(C);G=A.ch(B,E)
59
59
  if not G and not F:return
60
- D=A.cn(B)
60
+ D=A.cu(B)
61
61
  if D<=0:return
62
- H,I=A.cg(B,D);J=A.cm(H);K=A.cj(J);return{_B:[RemoveMessage(id=REMOVE_ALL_MESSAGES),*K,*I],_G:_F}
62
+ H,I=A.cn(B,D);J=A.ck(H);K=A.cl(J);return{_B:[RemoveMessage(id=REMOVE_ALL_MESSAGES),*K,*I],_G:_F}
63
63
  @override
64
64
  async def abefore_model(self,state,runtime):
65
- C=state;A=self;B=C[_B];A.cq(B);E=A.token_counter(B);F=A.cl(C);G=A.co(B,E)
65
+ C=state;A=self;B=C[_B];A.co(B);E=A.token_counter(B);F=A.ct(C);G=A.ch(B,E)
66
66
  if not G and not F:return
67
- D=A.cn(B)
67
+ D=A.cu(B)
68
68
  if D<=0:return
69
- H,I=A.cg(B,D);J=await A._acreate_summary(H);K=A.cj(J);return{_B:[RemoveMessage(id=REMOVE_ALL_MESSAGES),*K,*I],_G:_F}
70
- def cs(B,messages,threshold):
69
+ H,I=A.cn(B,D);J=await A._acreate_summary(H);K=A.cl(J);return{_B:[RemoveMessage(id=REMOVE_ALL_MESSAGES),*K,*I],_G:_F}
70
+ def ci(B,messages,threshold):
71
71
  A=next((A for A in reversed(messages)if isinstance(A,AIMessage)),_A)
72
72
  if isinstance(A,AIMessage)and A.usage_metadata is not _A and(C:=A.usage_metadata.get('total_tokens',-1))and C>=threshold and(D:=A.response_metadata.get('model_provider'))and D==B.model._get_ls_params().get('ls_provider'):return _C
73
73
  return _F
74
- def co(A,messages,total_tokens):
74
+ def ch(A,messages,total_tokens):
75
75
  F=total_tokens;E=messages
76
76
  if not A._trigger_conditions:return _F
77
77
  for(B,C)in A._trigger_conditions:
78
78
  if B==_B and len(E)>=C:return _C
79
79
  if B==_E and F>=C:return _C
80
- if B==_E and A.cs(E,C):return _C
80
+ if B==_E and A.ci(E,C):return _C
81
81
  if B==_D:
82
- G=A.cp()
82
+ G=A.cj()
83
83
  if G is _A:continue
84
84
  D=int(G*C)
85
85
  if D<=0:D=1
86
86
  if F>=D:return _C
87
- if A.cs(E,D):return _C
87
+ if A.ci(E,D):return _C
88
88
  return _F
89
- def cn(A,messages):
89
+ def cu(A,messages):
90
90
  B=messages;D,E=A.keep
91
91
  if D in{_E,_D}:
92
- C=A.cf(B)
92
+ C=A.cs(B)
93
93
  if C is not _A:return C
94
- return A.cr(B,_DEFAULT_MESSAGES_TO_KEEP)
95
- return A.cr(B,cast('int',E))
96
- def cf(C,messages):
94
+ return A.cp(B,_DEFAULT_MESSAGES_TO_KEEP)
95
+ return A.cp(B,cast('int',E))
96
+ def cs(C,messages):
97
97
  A=messages
98
98
  if not A:return 0
99
99
  H,I=C.keep
100
100
  if H==_D:
101
- J=C.cp()
101
+ J=C.cj()
102
102
  if J is _A:return
103
103
  F=int(J*I)
104
104
  elif H==_E:F=int(I)
@@ -114,8 +114,8 @@ class SummarizationMiddleware(AgentMiddleware):
114
114
  if B>=len(A):
115
115
  if len(A)==1:return 0
116
116
  B=len(A)-1
117
- return C.ct(A,B)
118
- def cp(C):
117
+ return C.cr(A,B)
118
+ def cj(C):
119
119
  try:A=C.model.profile
120
120
  except AttributeError:return
121
121
  if not isinstance(A,Mapping):return
@@ -123,7 +123,7 @@ class SummarizationMiddleware(AgentMiddleware):
123
123
  if not isinstance(B,int):return
124
124
  return B
125
125
  @staticmethod
126
- def ch(context,parameter_name):
126
+ def cq(context,parameter_name):
127
127
  E=context;C=parameter_name;D,B=E
128
128
  if D==_D:
129
129
  if not 0<B<=1:A=f"Fractional {C} values must be between 0 and 1, got {B}.";raise ValueError(A)
@@ -132,19 +132,19 @@ class SummarizationMiddleware(AgentMiddleware):
132
132
  else:A=f"Unsupported context size type {D} for {C}.";raise ValueError(A)
133
133
  return E
134
134
  @staticmethod
135
- def cj(summary):return[HumanMessage(content=f"Here is a summary of the conversation to date:\n\n{summary}",additional_kwargs={'lc_source':'summarization'})]
135
+ def cl(summary):return[HumanMessage(content=f"Here is a summary of the conversation to date:\n\n{summary}",additional_kwargs={'lc_source':'summarization'})]
136
136
  @staticmethod
137
- def cq(messages):
137
+ def co(messages):
138
138
  for A in messages:
139
139
  if A.id is _A:A.id=str(uuid.uuid4())
140
140
  @staticmethod
141
- def cg(conversation_messages,cutoff_index):B=cutoff_index;A=conversation_messages;C=A[:B];D=A[B:];return C,D
142
- def cr(C,messages,messages_to_keep):
141
+ def cn(conversation_messages,cutoff_index):B=cutoff_index;A=conversation_messages;C=A[:B];D=A[B:];return C,D
142
+ def cp(C,messages,messages_to_keep):
143
143
  B=messages_to_keep;A=messages
144
144
  if len(A)<=B:return 0
145
- D=len(A)-B;return C.ct(A,D)
145
+ D=len(A)-B;return C.cr(A,D)
146
146
  @staticmethod
147
- def ct(messages,cutoff_index):
147
+ def cr(messages,cutoff_index):
148
148
  B=cutoff_index;A=messages
149
149
  if B>=len(A)or not isinstance(A[B],ToolMessage):return B
150
150
  E=set();C=B
@@ -158,10 +158,10 @@ class SummarizationMiddleware(AgentMiddleware):
158
158
  H={A.get('id')for A in D.tool_calls if A.get('id')}
159
159
  if E&H:return G
160
160
  return C
161
- def cm(A,messages_to_sumarize):
161
+ def ck(A,messages_to_sumarize):
162
162
  B=messages_to_sumarize
163
163
  if not B:return _H
164
- C=A.ci(B)
164
+ C=A.cg(B)
165
165
  if not C:return'Previous conversation was too long to summarize'
166
166
  D=get_buffer_string(C)
167
167
  try:E=A.model.invoke(A.summary_prompt.format(messages=D));return E.text.strip()
@@ -169,12 +169,12 @@ class SummarizationMiddleware(AgentMiddleware):
169
169
  async def _acreate_summary(A,messages_to_summarize):
170
170
  B=messages_to_summarize
171
171
  if not B:return _H
172
- C=A.ci(B)
172
+ C=A.cg(B)
173
173
  if not C:return'Previous conversation was too long to summarize.'
174
174
  D=get_buffer_string(C)
175
175
  try:E=await A.model.ainvoke(A.summary_prompt.format(messages=D));return E.text.strip()
176
176
  except Exception as F:return f"Error generating sumamry: {F!s}"
177
- def ci(A,messages):
177
+ def cg(A,messages):
178
178
  B=messages
179
179
  try:
180
180
  if A.trim_tokens_to_summarize is _A:return B
@@ -16,33 +16,33 @@ class ToolResultFormatter:
16
16
  def detect_type(B,content):
17
17
  A=content;A=A.strip()
18
18
  if A.statswith(SUCCESS_PREFIX):
19
- C=B.bi(A)
20
- if B.bd(C):return ContentType.JSON
19
+ C=B.bh(A)
20
+ if B.bi(C):return ContentType.JSON
21
21
  return ContentType.SUCCESS
22
22
  if A.startswith(FAILURE_PREFIX):return ContentType.ERROR
23
- if B.bd(A):return ContentType.JSON
24
- if B.bg(A):return ContentType.ERROR
25
- if B.bc(A):return ContentType.MARKDOWN
23
+ if B.bi(A):return ContentType.JSON
24
+ if B.bd(A):return ContentType.ERROR
25
+ if B.bg(A):return ContentType.MARKDOWN
26
26
  return ContentType.TEXT
27
27
  def is_success(A,content):return _is_success(content)
28
- def format(A,name,content,max_length=800):B=content;C=A.detect_type(B);D=A.is_success(B);E={ContentType.SUCCESS:A.bf,ContentType.ERROR:A.bh,ContentType.JSON:A.bj,ContentType.MARKDOWN:A.bb,ContentType.TEXT:A.be};F=E.get(C,A.be);G=F(name,B,max_length);return FormattedResult(content_type=C,elements=G,success=D)
29
- def bi(B,content):A=content.split('\n',2);return A[2].strip()if len(A)>2 else''
30
- def bd(B,content):
28
+ def format(A,name,content,max_length=800):B=content;C=A.detect_type(B);D=A.is_success(B);E={ContentType.SUCCESS:A.be,ContentType.ERROR:A.bc,ContentType.JSON:A.ba,ContentType.MARKDOWN:A.bj,ContentType.TEXT:A.bb};F=E.get(C,A.bb);G=F(name,B,max_length);return FormattedResult(content_type=C,elements=G,success=D)
29
+ def bh(B,content):A=content.split('\n',2);return A[2].strip()if len(A)>2 else''
30
+ def bi(B,content):
31
31
  A=content;A=A.strip()
32
32
  if not A:return _A
33
33
  if A.startswith('{')and A.endswith('}')or A.startswith('[')and A.endswith(']'):
34
34
  try:json.loads(A);return True
35
35
  except(json.JSONDecodeError,ValueError):pass
36
36
  return _A
37
- def bg(B,content):A=['Traceback (most recent call last)','Exception:','Error:'];return any(A in content for A in A)
38
- def bc(C,content):A=content;B=['```','**','##','- **'];return A.startswith('#')or any(B in A for B in B)
39
- def bf(B,name,content,max_length):A='green';C=B.ba(content,max_length);return[Panel(Text(C,style=A),title=f"📤 {name} ✓",border_style=A)]
40
- def bh(B,name,content,max_length):A='red';C=B.ba(content,max_length);return[Panel(Text(C,style=A),title=f"📤 {name} ✗",border_style=A)]
41
- def bj(B,name,content,max_length):
37
+ def bd(B,content):A=['Traceback (most recent call last)','Exception:','Error:'];return any(A in content for A in A)
38
+ def bg(C,content):A=content;B=['```','**','##','- **'];return A.startswith('#')or any(B in A for B in B)
39
+ def be(B,name,content,max_length):A='green';C=B.bf(content,max_length);return[Panel(Text(C,style=A),title=f"📤 {name} ✓",border_style=A)]
40
+ def bc(B,name,content,max_length):A='red';C=B.bf(content,max_length);return[Panel(Text(C,style=A),title=f"📤 {name} ✗",border_style=A)]
41
+ def ba(B,name,content,max_length):
42
42
  D=max_length;A=content;E=A
43
- if A.startswith(SUCCESS_PREFIX):E=B.bi(A)
44
- try:F=json.loads(E);C=json.dumps(F,indent=2,ensure_ascii=_A);C=B.ba(C,D);return[Text(f"📤 {name} ✓",style=_B),Syntax(C,'json',theme='monokai',line_numbers=_A)]
45
- except(json.JSONDecodeError,ValueError):return B.be(name,A,D)
46
- def bb(A,name,content,max_length):B=A.ba(content,max_length);return[Panel(Markdown(B),title=f"📤 {name}",border_style='cyan dim')]
47
- def be(A,name,content,max_length):B=A.ba(content,max_length);return[Text(f"📤 {name}:",style=_B),Text(f" {B}",style='dim')]
48
- def ba(A,content,max_length):return truncate(content,max_length)
43
+ if A.startswith(SUCCESS_PREFIX):E=B.bh(A)
44
+ try:F=json.loads(E);C=json.dumps(F,indent=2,ensure_ascii=_A);C=B.bf(C,D);return[Text(f"📤 {name} ✓",style=_B),Syntax(C,'json',theme='monokai',line_numbers=_A)]
45
+ except(json.JSONDecodeError,ValueError):return B.bb(name,A,D)
46
+ def bj(A,name,content,max_length):B=A.bf(content,max_length);return[Panel(Markdown(B),title=f"📤 {name}",border_style='cyan dim')]
47
+ def bb(A,name,content,max_length):B=A.bf(content,max_length);return[Text(f"📤 {name}:",style=_B),Text(f" {B}",style='dim')]
48
+ def bf(A,content,max_length):return truncate(content,max_length)
File without changes
File without changes
@@ -0,0 +1,45 @@
1
+ from __future__ import annotations
2
+ _A=None
3
+ import argparse,json,sys
4
+ from datetime import datetime,timedelta,timezone
5
+ from pathlib import Path
6
+ from.pricing import ModelPricing
7
+ from.tracker import CostTracker,UsageRecord,BudgetGuard
8
+ from.report import CostReport
9
+ def parse_since(s):
10
+ A=datetime.now(timezone.utc);s=s.strip().lower()
11
+ if s.endswith('d'):return A-timedelta(days=int(s[:-1]))
12
+ if s.endswith('w'):return A-timedelta(weeks=int(s[:-1]))
13
+ if s.endswith('m'):return A-timedelta(days=30*int(s[:-1]))
14
+ if s.endswith('h'):return A-timedelta(hours=int(s[:-1]))
15
+ return datetime.fromisoformat(s)
16
+ def cmd_add(args,tracker):
17
+ B=tracker;A=args;D=B.add_call(model=A.model,input_tokens=A.input,output_tokens=A.output,tag=A.tag);E=ModelPricing()
18
+ try:F=E.comp(A.model,A.input,A.output);C=f"${F:.6f}"
19
+ except KeyError:C='unknown (model not in pricing DB)'
20
+ print(f"✓ Logged: {A.model} {A.input:,} in / {A.output:,} out cost={C} tag={A.tag}");print(f" record_id={D.record_id} db={B.db_path}")
21
+ def cmd_report(args,tracker):
22
+ A=args;B=parse_since(A.since)if A.since else _A;C=CostReport(tracker)
23
+ if A.format=='json':print(C.to_json(since=B,period=A.period))
24
+ else:print(C.to_text(since=B,period=A.period))
25
+ def cmd_models(args):
26
+ C=ModelPricing();B=C.list_models(provider=args.provider)
27
+ if not B:print('No models found.');return
28
+ print(f"\n{'Model':<45} {'Provider':<12} {'Input $/1M':>12} {'Output $/1M':>12}");print('─'*85)
29
+ for A in B:print(f" {A.model:<43} {A.provider:<12} ${A.input_per_1m:>10.3f} ${A.output_per_1m:>10.3f}")
30
+ print()
31
+ def cmd_budget(args,tracker):
32
+ C=tracker;A=args
33
+ if A.budget_cmd=='set':D=BudgetGuard(tag=A.tag,limit_usd=A.limit);C.set_budget(D);print(f"✓ Budget set: tag={A.tag} limit=${A.limit:.2f}");B=C.total_cost(tag=A.tag);E=B/A.limit*100 if A.limit>0 else 0;print(f" Current spend: ${B:.4f} ({E:.1f}% of budget)")
34
+ elif A.budget_cmd=='check':B=C.total_cost(tag=A.tag);print(f" Tag '{A.tag}': ${B:.4f} spent")
35
+ else:print('Usage: llm-cost budget set --tag TAG --limit AMOUNT')
36
+ def main():
37
+ P='budget';O='models';N='daily';M='text';L='report';K='add';H='--tag';B=True;C=argparse.ArgumentParser(prog='llm-cost',description='LLM cost tracker — log API calls, report spending, guard budgets.');C.add_argument('--db',type=Path,default=_A,help='Path to usage DB (default: ~/.config/llm-cost-tracker/usage.jsonl)');E=C.add_subparsers(dest='command');D=E.add_parser(K,help='Log an LLM API call');D.add_argument('--model',required=B,help='Model name, e.g. gpt-4o');D.add_argument('--input',type=int,required=B,help='Input token count');D.add_argument('--output',type=int,required=B,help='Output token count');D.add_argument(H,default='default',help='Project/tag label');D.add_argument('--timestamp',default=_A,help='ISO timestamp (default: now)');F=E.add_parser(L,help='Show cost report');F.add_argument('--since',default='30d',help='e.g. 7d, 1w, 1m, or ISO date');F.add_argument('--format',choices=[M,'json'],default=M);F.add_argument('--period',choices=[N,'weekly','monthly'],default=N);Q=E.add_parser(O,help='List pricing database');Q.add_argument('--provider',default=_A,help='Filter by provider');R=E.add_parser(P,help='Budget guard commands');I=R.add_subparsers(dest='budget_cmd');J=I.add_parser('set',help='Set a budget for a tag');J.add_argument(H,required=B);J.add_argument('--limit',type=float,required=B,help='Budget in USD');S=I.add_parser('check',help='Check spending for a tag');S.add_argument(H,required=B);A=C.parse_args()
38
+ if not A.command:C.print_help();sys.exit(0)
39
+ G=CostTracker(db_path=A.db)
40
+ if A.command==K:cmd_add(A,G)
41
+ elif A.command==L:cmd_report(A,G)
42
+ elif A.command==O:cmd_models(A)
43
+ elif A.command==P:cmd_budget(A,G)
44
+ else:C.print_help()
45
+ if __name__=='__main__':main()
@@ -0,0 +1,39 @@
1
+ _I='qwen'
2
+ _H='mistral'
3
+ _G='avg hosted price'
4
+ _F='meta'
5
+ _E='cohere'
6
+ _D=None
7
+ _C='google'
8
+ _B='anthropic'
9
+ _A='openai'
10
+ from dataclasses import dataclass
11
+ from typing import Optional
12
+ @dataclass(frozen=True)
13
+ class ModelRate:provider:str;model:str;input_per_1m:float;output_per_1m:float;notes:str=''
14
+ _PRICING={}
15
+ def cy(*C):
16
+ for A in C:
17
+ _PRICING[A.model]=A;B=A.model.split('/')[-1]if'/'in A.model else _D
18
+ if B and B not in _PRICING:_PRICING[B]=A
19
+ cy(ModelRate(_A,'gpt-4o',2.5,1e1),ModelRate(_A,'gpt-4o-mini',.15,.6),ModelRate(_A,'gpt-4o-2024-11-20',2.5,1e1),ModelRate(_A,'gpt-4-turbo',1e1,3e1),ModelRate(_A,'gpt-4',3e1,6e1),ModelRate(_A,'gpt-3.5-turbo',.5,1.5),ModelRate(_A,'o1',15.,6e1),ModelRate(_A,'o1-mini',3.,12.),ModelRate(_A,'o3-mini',1.1,4.4),ModelRate(_A,'o3',1e1,4e1),ModelRate(_B,'claude-3-5-sonnet-20241022',3.,15.),ModelRate(_B,'claude-3-5-haiku-20241022',.8,4.),ModelRate(_B,'claude-3-opus-20240229',15.,75.),ModelRate(_B,'claude-3-sonnet-20240229',3.,15.),ModelRate(_B,'claude-3-haiku-20240307',.25,1.25),ModelRate(_B,'claude-sonnet-4-5',3.,15.),ModelRate(_B,'claude-sonnet-4-6',3.,15.),ModelRate(_B,'claude-opus-4',15.,75.),ModelRate(_C,'gemini-1.5-pro',3.5,10.5,'up to 128k ctx'),ModelRate(_C,'gemini-1.5-flash',.075,.3),ModelRate(_C,'gemini-2.0-flash',.1,.4),ModelRate(_C,'gemini-2.0-flash-lite',.075,.3),ModelRate(_C,'gemini-2.0-pro',3.5,10.5),ModelRate(_E,'command-r-plus',2.5,1e1),ModelRate(_E,'command-r',.15,.6),ModelRate(_E,'command-r7b',.0375,.15),ModelRate(_F,'llama-3.3-70b-instruct',.23,.4,_G),ModelRate(_F,'llama-3.1-405b-instruct',3.,3.,_G),ModelRate(_F,'llama-3.1-70b-instruct',.2,.2,_G),ModelRate(_H,'mistral-large-2411',2.,6.),ModelRate(_H,'mistral-small-2501',.1,.3),ModelRate(_H,'codestral-2501',.1,.3),ModelRate(_I,'qwen-plus',2.,6.),ModelRate(_I,'qwen-max-3',2.,6.),ModelRate(_I,'qwen-max-3.5',2.,6.))
20
+ class ModelPricing:
21
+ def __init__(A,custom_rates=_D):
22
+ B=custom_rates;A._db=dict(_PRICING)
23
+ if B:A._db.update(B)
24
+ def get_rate(A,model):
25
+ C=model;B=C.lower().strip()
26
+ if B in A._db:return A._db[B]
27
+ for(D,E)in A._db.items():
28
+ if B in D or D in B:return E
29
+ raise KeyError(f"Unknown model: {C!r}. Known models: {sorted(A._db)}")
30
+ def compute_cost(B,model,input_tokens,output_tokens):A=B.get_rate(model);return A.input_per_1m*input_tokens/1000000+A.output_per_1m*output_tokens/1000000
31
+ def list_models(F,provider=_D):
32
+ B=provider;C=set();D=[]
33
+ for A in F._db.values():
34
+ E=A.provider,A.model
35
+ if E in C:continue
36
+ C.add(E)
37
+ if B is _D or A.provider.lower()==B.lower():D.append(A)
38
+ return sorted(D,key=lambda r:(r.provider,r.model))
39
+ def add_custom(A,rate):A._db[rate.model]=rate