@brandon_9527/tcode 1.0.2 → 1.0.3
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/.autodev/skills/teams/scripts/README.md +29 -0
- package/dist/python-src/.autodev/skills/teams/scripts/cursor_dispatch.sh +88 -0
- package/dist/python-src/.autodev/skills/teams/scripts/dispatch.sh +112 -0
- package/dist/python-src/.autodev/skills/teams/scripts/label.sh +62 -0
- package/dist/python-src/.autodev/skills/teams/scripts/layout.sh +181 -0
- package/dist/python-src/.autodev/skills/teams/scripts/old_dispatch.sh +42 -0
- package/dist/python-src/.autodev/skills/teams/scripts/pipe.sh +19 -0
- package/dist/python-src/.autodev/skills/teams/scripts/pipe_dispatch.sh +59 -0
- package/dist/python-src/.autodev/skills/teams/scripts/run.sh +18 -0
- package/dist/python-src/.autodev/skills/teams/scripts/stop.sh +26 -0
- package/dist/python-src/.autodev/skills/teams/scripts/tmux-layout.sh +43 -0
- package/dist/python-src/entry.py +28 -1
- package/dist/python-src/main.py +382 -3
- package/dist/python-src/pyproject.toml +1 -1
- package/dist/python-src/src/ai_tcode.egg-info/PKG-INFO +30 -0
- package/dist/python-src/src/ai_tcode.egg-info/SOURCES.txt +48 -0
- package/dist/python-src/src/ai_tcode.egg-info/dependency_links.txt +1 -0
- package/dist/python-src/src/ai_tcode.egg-info/requires.txt +26 -0
- package/dist/python-src/src/ai_tcode.egg-info/top_level.txt +9 -0
- package/dist/python-src/src/managers/manager_agent.py +200 -0
- package/dist/python-src/src/managers/manager_context.py +49 -0
- package/dist/python-src/src/managers/manager_instruction.py +192 -0
- package/dist/python-src/src/managers/sandbox.py +3 -3
- package/dist/python-src/src/middlewares/dynamic_content.py +63 -0
- package/dist/python-src/src/middlewares/hitl.py +3 -3
- package/dist/python-src/src/middlewares/memory.py +44 -0
- package/dist/python-src/src/middlewares/subagents.py +25 -25
- package/dist/python-src/src/middlewares/summary.py +35 -35
- package/dist/python-src/src/middlewares/utils.py +5 -0
- package/dist/python-src/src/prompts/prompts.py +1 -0
- package/dist/python-src/src/stream/formatter.py +17 -17
- package/dist/python-src/src/stream/handler.py +109 -81
- package/dist/python-src/src/stream/handler_with_tracker.py +10 -10
- package/dist/python-src/src/tools/web.py +10 -9
- package/dist/python-src/src/tui/chatui.py +67 -51
- package/dist/python-src/src/tui/components/tlist.py +6 -6
- package/dist/python-src/src/tui/components/tscroll_panel.py +8 -8
- package/dist/python-src/src/tui/utils/trender.py +27 -27
- package/dist/python-src/src/utils/prompt.py +15 -4
- package/dist/python-src/uv.lock +1980 -2094
- package/package.json +1 -1
- package/dist/python-src/src/__pycache__/__init__.cpython-311.pyc +0 -0
- package/dist/python-src/src/managers/__pycache__/__init__.cpython-311.pyc +0 -0
- package/dist/python-src/src/managers/__pycache__/sandbox.cpython-311.pyc +0 -0
- package/dist/python-src/src/middlewares/__pycache__/__init__.cpython-311.pyc +0 -0
- package/dist/python-src/src/middlewares/__pycache__/hitl.cpython-311.pyc +0 -0
- package/dist/python-src/src/middlewares/dynamic_prompt.py +0 -15
- package/dist/python-src/src/stream/__pycache__/__init__.cpython-311.pyc +0 -0
- package/dist/python-src/src/stream/__pycache__/emitter.cpython-311.pyc +0 -0
- package/dist/python-src/src/stream/__pycache__/file_write_parser.cpython-311.pyc +0 -0
- package/dist/python-src/src/stream/__pycache__/formatter.cpython-311.pyc +0 -0
- package/dist/python-src/src/stream/__pycache__/handler.cpython-311.pyc +0 -0
- package/dist/python-src/src/stream/__pycache__/tracker.cpython-311.pyc +0 -0
- package/dist/python-src/src/stream/__pycache__/utils.cpython-311.pyc +0 -0
- package/dist/python-src/src/tools/__pycache__/__init__.cpython-311.pyc +0 -0
- package/dist/python-src/src/tools/__pycache__/skill_loader.cpython-311.pyc +0 -0
- package/dist/python-src/src/tools/__pycache__/tools.cpython-311.pyc +0 -0
- package/dist/python-src/src/tools/__pycache__/web.cpython-311.pyc +0 -0
- package/dist/python-src/src/tui/__pycache__/chatui.cpython-311.pyc +0 -0
- package/dist/python-src/src/tui/__pycache__/config.cpython-311.pyc +0 -0
- package/dist/python-src/src/tui/components/__pycache__/__init__.cpython-311.pyc +0 -0
- package/dist/python-src/src/tui/components/__pycache__/live_spinner.cpython-311.pyc +0 -0
- package/dist/python-src/src/tui/components/__pycache__/tdiff.cpython-311.pyc +0 -0
- package/dist/python-src/src/tui/components/__pycache__/tdisplay.cpython-311.pyc +0 -0
- package/dist/python-src/src/tui/components/__pycache__/tlist.cpython-311.pyc +0 -0
- package/dist/python-src/src/tui/components/__pycache__/tscroll_panel.cpython-311.pyc +0 -0
- package/dist/python-src/src/tui/utils/__pycache__/__init__.cpython-311.pyc +0 -0
- package/dist/python-src/src/tui/utils/__pycache__/render.cpython-311.pyc +0 -0
- package/dist/python-src/src/tui/utils/__pycache__/trender.cpython-311.pyc +0 -0
- package/dist/python-src/src/utils/__pycache__/__init__.cpython-311.pyc +0 -0
- package/dist/python-src/src/utils/__pycache__/utils.cpython-311.pyc +0 -0
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
_L='domain'
|
|
2
|
+
_K='default'
|
|
3
|
+
_J='source'
|
|
4
|
+
_I='utf-8'
|
|
5
|
+
_H='llm'
|
|
6
|
+
_G='tools'
|
|
7
|
+
_F='name'
|
|
8
|
+
_E=True
|
|
9
|
+
_D='description'
|
|
10
|
+
_C='user'
|
|
11
|
+
_B='project'
|
|
12
|
+
_A=None
|
|
13
|
+
from typing import Dict,Tuple,Any,List,Optional,Union
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
import yaml,json,sys,re,os
|
|
16
|
+
sys.path.append('/Users/brandon/workspace/coder/autodev')
|
|
17
|
+
class AgentInfo:
|
|
18
|
+
def __init__(A,name,content,src_):B=content;A.name=name;A.original_content=B;A.src_=src_;A.meta,A.body=A.bt(B)
|
|
19
|
+
def bt(F,content):
|
|
20
|
+
B=content;C='^---\\n(.*?)\\n---\\n(.*)$';A=re.match(C,B,re.S)
|
|
21
|
+
if A:D=yaml.safe_load(A.group(1));E=A.group(2).strip();return D,E
|
|
22
|
+
else:return{},B.strip()
|
|
23
|
+
def info(A):B=A.body;return{_F:A.meta.get(_F,A.name),_D:A.meta.get(_D,''),_G:A.meta.get(_G,[]),_H:A.meta.get(_H,_A),'prompt':B,_J:A.src_}
|
|
24
|
+
class AgentManager:
|
|
25
|
+
def __init__(A,project_dir=_A):
|
|
26
|
+
C='agents';B='.autodev';A.project_dir=project_dir;A.user_agents_dir=os.path.join(str(Path.home()),B,C)
|
|
27
|
+
if A.project_dir:A.project_agents_dir=os.path.abspath(os.path.join(A.project_dir,B,C))
|
|
28
|
+
else:A.project_agents_dir=_A
|
|
29
|
+
A.agents={_B:{},_C:{}}
|
|
30
|
+
if A.project_agents_dir and not os.path.exists(A.project_agents_dir):os.makedirs(A.project_agents_dir)
|
|
31
|
+
if not os.path.exists(A.user_agents_dir):os.makedirs(A.user_agents_dir)
|
|
32
|
+
A.reload_agents()
|
|
33
|
+
def bv(O,dir_path,source):
|
|
34
|
+
E=source;D=dir_path;B={}
|
|
35
|
+
if not os.path.exists(D):return B
|
|
36
|
+
for(G,P,I)in os.walk(D):
|
|
37
|
+
H=os.path.relpath(G,D)
|
|
38
|
+
if H=='.':A=_K
|
|
39
|
+
else:A=H.replace(os.sep,'/')
|
|
40
|
+
if A not in B:B[A]={}
|
|
41
|
+
for F in I:
|
|
42
|
+
if F.endswith('.md'):
|
|
43
|
+
J=os.path.join(G,F);C=os.path.splitext(F)[0];print(f"加载 {E} 来源 agent: {A}/{C}")
|
|
44
|
+
try:
|
|
45
|
+
with open(J,'r',encoding=_I)as K:L=K.read()
|
|
46
|
+
M=AgentInfo(C,L,E);B[A][C]=M
|
|
47
|
+
except Exception as N:print(f"加载 {E} 来源 agent {A}/{C} 失败: {str(N)}")
|
|
48
|
+
return B
|
|
49
|
+
def reload_agents(A):
|
|
50
|
+
A.agents[_B].clear();A.agents[_C].clear();A.agents[_C]=A.bv(A.user_agents_dir,_C)
|
|
51
|
+
if A.project_agents_dir:A.agents[_B]=A.bv(A.project_agents_dir,_B)
|
|
52
|
+
def bu(A,source,domain,name):
|
|
53
|
+
B=source
|
|
54
|
+
if B==_B:
|
|
55
|
+
if not A.project_agents_dir:raise ValueError('项目目录未初始化,无法操作project来源的agent')
|
|
56
|
+
C=A.project_agents_dir
|
|
57
|
+
elif B==_C:C=A.user_agents_dir
|
|
58
|
+
else:raise ValueError(f"无效的来源: {B},仅支持 'project' 或 'user'")
|
|
59
|
+
D=os.path.join(C,domain.replace('/',os.sep));return os.path.join(D,f"{name}.md")
|
|
60
|
+
def exists(A,domain,name,source=_A):
|
|
61
|
+
C=source;B=domain
|
|
62
|
+
if C:return name in A.agents.get(C,{}).get(B,{})
|
|
63
|
+
else:
|
|
64
|
+
for D in[_B,_C]:
|
|
65
|
+
if name in A.agents.get(D,{}).get(B,{}):return _E
|
|
66
|
+
return False
|
|
67
|
+
def get_agent_with_source(A,domain,name):
|
|
68
|
+
C=name;B=domain
|
|
69
|
+
if C in A.agents.get(_B,{}).get(B,{}):return A.agents[_B][B][C],_B
|
|
70
|
+
elif C in A.agents.get(_C,{}).get(B,{}):return A.agents[_C][B][C],_C
|
|
71
|
+
else:return _A,_A
|
|
72
|
+
def create_agent(A,domain,name,meta,body,source=_B):
|
|
73
|
+
D=source;C=name;B=domain
|
|
74
|
+
if A.exists(B,C,D):raise ValueError(f"Agent '{D}/{B}/{C}' 已存在")
|
|
75
|
+
F=f"---\n{yaml.safe_dump(meta,allow_unicode=_E)}---\n{body.strip()}\n";E=A.bu(D,B,C);os.makedirs(os.path.dirname(E),exist_ok=_E)
|
|
76
|
+
with open(E,'w',encoding=_I)as G:G.write(F)
|
|
77
|
+
A.reload_agents()
|
|
78
|
+
def update_agent(C,domain,name,meta=_A,body=_A,source=_A):
|
|
79
|
+
D=source;B=name;A=domain
|
|
80
|
+
if D:
|
|
81
|
+
if not C.exists(A,B,D):raise ValueError(f"Agent '{D}/{A}/{B}' 不存在")
|
|
82
|
+
E=D
|
|
83
|
+
else:
|
|
84
|
+
L,E=C.get_agent_with_source(A,B)
|
|
85
|
+
if not E:raise ValueError(f"Agent '{A}/{B}' 不存在于任何来源")
|
|
86
|
+
F=C.agents[E][A][B];G=meta if meta is not _A else F.meta;H=body if body is not _A else F.body;I=f"---\n{yaml.safe_dump(G,allow_unicode=_E)}---\n{H.strip()}\n";J=C.bu(E,A,B)
|
|
87
|
+
with open(J,'w',encoding=_I)as K:K.write(I)
|
|
88
|
+
C.reload_agents()
|
|
89
|
+
def delete_agent(C,domain,name,source=_A):
|
|
90
|
+
D=source;B=name;A=domain
|
|
91
|
+
if D:
|
|
92
|
+
if not C.exists(A,B,D):raise ValueError(f"Agent '{D}/{A}/{B}' 不存在")
|
|
93
|
+
E=D
|
|
94
|
+
else:
|
|
95
|
+
G,E=C.get_agent_with_source(A,B)
|
|
96
|
+
if not E:raise ValueError(f"Agent '{A}/{B}' 不存在于任何来源")
|
|
97
|
+
F=C.bu(E,A,B)
|
|
98
|
+
if os.path.exists(F):os.remove(F)
|
|
99
|
+
C.reload_agents()
|
|
100
|
+
def get_agent(A,domain,name,source=_A):
|
|
101
|
+
D=source;C=name;B=domain
|
|
102
|
+
if D:return A.agents.get(D,{}).get(B,{}).get(C)
|
|
103
|
+
else:
|
|
104
|
+
E=A.agents.get(_B,{}).get(B,{}).get(C)
|
|
105
|
+
if E:return E
|
|
106
|
+
return A.agents.get(_C,{}).get(B,{}).get(C)
|
|
107
|
+
def list_domains(A,source=_A):
|
|
108
|
+
B=source;C=set()
|
|
109
|
+
if B:
|
|
110
|
+
if B in A.agents:C.update(A.agents[B].keys())
|
|
111
|
+
else:
|
|
112
|
+
for D in[_B,_C]:C.update(A.agents[D].keys())
|
|
113
|
+
return list(C)
|
|
114
|
+
def list_agents(G,domain=_A,source=_A):
|
|
115
|
+
I=source;B=domain;A={};J=[I]if I else[_B,_C]
|
|
116
|
+
for H in J:
|
|
117
|
+
if H not in G.agents:continue
|
|
118
|
+
if B:
|
|
119
|
+
if B in G.agents[H]:
|
|
120
|
+
if B not in A:A[B]=[]
|
|
121
|
+
A[B].extend(list(G.agents[H][B].keys()))
|
|
122
|
+
else:
|
|
123
|
+
for(D,K)in G.agents[H].items():
|
|
124
|
+
if D not in A:A[D]=[]
|
|
125
|
+
A[D].extend(list(K.keys()))
|
|
126
|
+
if B:
|
|
127
|
+
E=set();F=[]
|
|
128
|
+
for C in A.get(B,[]):
|
|
129
|
+
if C not in E:E.add(C);F.append(C)
|
|
130
|
+
return F
|
|
131
|
+
for D in A:
|
|
132
|
+
E=set();F=[]
|
|
133
|
+
for C in A[D]:
|
|
134
|
+
if C not in E:E.add(C);F.append(C)
|
|
135
|
+
A[D]=F
|
|
136
|
+
return A
|
|
137
|
+
def agent_info(D,domain,name,source=_A):
|
|
138
|
+
C=source;A=domain;B=D.get_agent(A,name,C)
|
|
139
|
+
if not B:raise ValueError(f"Agent '{A}/{name}' 不存在(来源: {C or'all'})")
|
|
140
|
+
E=B.info();return{_L:A,_J:B.src_,**E}
|
|
141
|
+
def get_conf(B,domain,source=_A):
|
|
142
|
+
E='members';A=domain;C={_L:A,E:{}};F=B.list_agents(domain=A,source=source)
|
|
143
|
+
for D in F:
|
|
144
|
+
try:G=B.agent_info(A,D);C[E][D]=G
|
|
145
|
+
except ValueError:continue
|
|
146
|
+
return C
|
|
147
|
+
def get_all_conf(A,source=_A):
|
|
148
|
+
B=source;C={};E=A.list_domains(source=B)
|
|
149
|
+
for D in E:C[D]=A.get_conf(D,source=B)
|
|
150
|
+
return C
|
|
151
|
+
def descriptions_(B,user=False,source=_A):
|
|
152
|
+
C=source;D={};H=[C]if C else[_B,_C]
|
|
153
|
+
for A in H:
|
|
154
|
+
if A not in B.agents:continue
|
|
155
|
+
for(E,I)in B.agents[A].items():
|
|
156
|
+
for(F,J)in I.items():
|
|
157
|
+
if user:G=f"@{E}/{F}[user_{A}]"
|
|
158
|
+
else:G=f"@{E}/{F}[{A}]"
|
|
159
|
+
D[G]=J.meta.get(_D,'')
|
|
160
|
+
return D
|
|
161
|
+
def parse_agent_file(file_path):
|
|
162
|
+
H="'";G='"'
|
|
163
|
+
with open(file_path,'r',encoding=_I)as M:J=M.read()
|
|
164
|
+
N='^---\\s*(.*?)\\s*---\\s*(.*)$';I=re.search(N,J,re.DOTALL|re.MULTILINE)
|
|
165
|
+
if not I:return{},J.strip()
|
|
166
|
+
K,O=I.group(1).strip(),I.group(2).strip();D={}
|
|
167
|
+
if K:
|
|
168
|
+
for E in K.splitlines():
|
|
169
|
+
E=E.strip()
|
|
170
|
+
if not E:continue
|
|
171
|
+
if': 'not in E:continue
|
|
172
|
+
B,A=E.split(': ',1);B=B.strip();A=A.strip()
|
|
173
|
+
if A.startswith('[')and A.endswith(']'):
|
|
174
|
+
L=A[1:-1].strip()
|
|
175
|
+
if L=='':F=[]
|
|
176
|
+
else:
|
|
177
|
+
P=[A.strip()for A in L.split(',')];F=[]
|
|
178
|
+
for C in P:
|
|
179
|
+
if C.startswith(G)and C.endswith(G)or C.startswith(H)and C.endswith(H):F.append(C[1:-1])
|
|
180
|
+
else:F.append(C)
|
|
181
|
+
D[B]=F
|
|
182
|
+
elif A.startswith(G)and A.endswith(G)or A.startswith(H)and A.endswith(H):D[B]=A[1:-1]
|
|
183
|
+
else:
|
|
184
|
+
try:Q=json.loads(A);D[B]=Q
|
|
185
|
+
except Exception:D[B]=A
|
|
186
|
+
return D,O
|
|
187
|
+
def curd():
|
|
188
|
+
D='user_helper';C='new_helper';B='coding';H=os.path.expanduser('~');A=AgentManager(H);print('=== 新增project来源的agent ===');A.create_agent(B,C,meta={_F:C,_D:'a test agent (project)',_G:['tool1','tool2'],_H:'gpt-4'},body='你是一个新的项目Agent',source=_B);print('\n=== 新增user来源的agent ===');A.create_agent(B,D,meta={_F:D,_D:'a test agent (user)',_G:['tool3'],_H:'gpt-3.5'},body='你是一个新的用户Agent',source=_C);print('\n=== 查询所有coding域的agents ===');E=A.list_agents(domain=B);print(f"coding域agents: {E}");print('\n=== 查询agent详细信息 ===')
|
|
189
|
+
try:F=A.agent_info(B,C,source=_B);print(f"project/new_helper: {F}")
|
|
190
|
+
except ValueError as G:print(G)
|
|
191
|
+
try:F=A.agent_info(B,D,source=_C);print(f"user/user_helper: {F}")
|
|
192
|
+
except ValueError as G:print(G)
|
|
193
|
+
print('\n=== 更新agent ===');A.update_agent(B,C,body='更新后的项目Agent prompt',source=_B);print('\n=== 删除agent ===');A.delete_agent(B,C,source=_B);A.delete_agent(B,D,source=_C);print('\n=== 删除后查询 ===');E=A.list_agents(domain=B);print(f"coding域剩余agents: {E}")
|
|
194
|
+
def main():
|
|
195
|
+
B=os.path.expanduser('~');A=AgentManager(B);print('=== 列出所有域 ===');print(A.list_domains());print('\n=== 列出default域的所有agents ===');print(A.list_agents(domain=_K));print('\n=== 列出所有域的agents ===');C=A.list_agents()
|
|
196
|
+
for(D,E)in C.items():print(f"{D}: {E}")
|
|
197
|
+
print('\n=== 所有agent描述(包含来源)===');F=A.descriptions_()
|
|
198
|
+
for(G,H)in F.items():print(f"{G}: {H}")
|
|
199
|
+
print('\n=== 获取所有配置 ===');I=A.get_all_conf();print(yaml.safe_dump(I,allow_unicode=_E,indent=2))
|
|
200
|
+
if __name__=='__main__':curd()
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
_C='sandbox'
|
|
2
|
+
_B='default'
|
|
3
|
+
_A=None
|
|
4
|
+
from typing import Dict
|
|
5
|
+
import sys,os
|
|
6
|
+
sys.path.insert(0,os.path.dirname(os.path.dirname(os.path.dirname(__file__))))
|
|
7
|
+
from src.managers.manager_instruction import InstructionManager
|
|
8
|
+
from src.managers.manager_agent import AgentManager
|
|
9
|
+
from src.managers.sandbox import Container
|
|
10
|
+
DOCKERFILE_PATH=os.path.join('../../resources','dockerfiles',_C,'Dockerfile.sandbox')
|
|
11
|
+
class ContextManager:
|
|
12
|
+
def __init__(A,dockerfile_path=DOCKERFILE_PATH,mode=_C):A.mode=mode;A.dockerfile_path=dockerfile_path;A._docker_containers={};A._workspace_mapping={};A._instruction_managers={};A._agent_managers={}
|
|
13
|
+
def create_environment(A,session_id=_B,workspace_path=_A):
|
|
14
|
+
E='.autodev';D=True;C=session_id;B=workspace_path;print(f"create_environment: {C}, {B}")
|
|
15
|
+
if B:os.makedirs(B,exist_ok=D)
|
|
16
|
+
else:raise ValueError('workspace_path 不能为空')
|
|
17
|
+
A._workspace_mapping[C]=B
|
|
18
|
+
if A.mode==_C:A._docker_containers[C]=Container(user_id=C,workspace_path=B,dockerfile_path=A.dockerfile_path);A._docker_containers[C].start()
|
|
19
|
+
elif A.mode=='local':A._docker_containers[C]=_A
|
|
20
|
+
else:raise ValueError(f"不支持的模式: {A.mode}")
|
|
21
|
+
F=os.path.join(B,E,'commands');os.makedirs(F,exist_ok=D);A._instruction_managers[C]=InstructionManager(project_dir=B);G=os.path.join(B,E,'agents');os.makedirs(G,exist_ok=D);A._agent_managers[C]=AgentManager(project_dir=B)
|
|
22
|
+
def get_container(A,session_id=_B):
|
|
23
|
+
B=session_id;C=_A
|
|
24
|
+
if A._docker_containers.get(B,_A)is _A and A.mode==_C:A.create_environment(B)
|
|
25
|
+
C=A._docker_containers.get(B,_A);return C
|
|
26
|
+
def get_workdpace(A,session_id=_B):
|
|
27
|
+
B=session_id
|
|
28
|
+
if A._workspace_mapping.get(B,_A)is _A:A.create_environment(B)
|
|
29
|
+
C=A._workspace_mapping.get(B,_A);return C
|
|
30
|
+
def get_instruction_manager(A,session_id=_B):
|
|
31
|
+
B=session_id
|
|
32
|
+
if A._instruction_managers.get(B,_A)is _A:A.create_environment(B)
|
|
33
|
+
C=A._instruction_managers.get(B,_A);return C
|
|
34
|
+
def get_agent_manager(A,session_id=_B):
|
|
35
|
+
B=session_id
|
|
36
|
+
if A._agent_managers.get(B,_A)is _A:A.create_environment(B)
|
|
37
|
+
C=A._agent_managers.get(B,_A);return C
|
|
38
|
+
def shutdown(C,session_id=_A):
|
|
39
|
+
B=session_id
|
|
40
|
+
if B:
|
|
41
|
+
A=C._docker_containers.get(B,_A)
|
|
42
|
+
if A:A.shutdown()
|
|
43
|
+
else:
|
|
44
|
+
for(B,A)in C._docker_containers.items():A.shutdown()
|
|
45
|
+
def main():
|
|
46
|
+
A=ContextManager()
|
|
47
|
+
try:C='/Users/brandon/workspace/coder/coder_tui/_workspace';A.create_environment(workspace_path=C);C=A.get_workdpace();print(f"工作空间:\n{C}");D=A.get_command_mananger();B=D.parse('/git --persona-角色名:10 --persona-CI --think-hard 项目文档');print(f"命令执行内容:\n{B['executed_command']}");print(f"输入内容:\n{B['message']}");E=A.get_container();B=E.execute_command('ls -al');print(f"容器执行结果:\n{B}")
|
|
48
|
+
finally:A.shutdown()
|
|
49
|
+
if __name__=='__main__':main()
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
_J='---\n\n'
|
|
2
|
+
_I='utf-8'
|
|
3
|
+
_H='executed_instruction'
|
|
4
|
+
_G=True
|
|
5
|
+
_F='message'
|
|
6
|
+
_E='description'
|
|
7
|
+
_D='default'
|
|
8
|
+
_C='user'
|
|
9
|
+
_B='project'
|
|
10
|
+
_A=None
|
|
11
|
+
from typing import Dict,List,Tuple,Optional,Any
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
import re,os
|
|
14
|
+
def read_instruction_file(file_path):
|
|
15
|
+
with open(file_path,'r',encoding=_I)as H:F=H.read()
|
|
16
|
+
I='^---\\s*(.*?)\\s*---\\s*(.*)$';E=re.search(I,F,re.DOTALL|re.MULTILINE)
|
|
17
|
+
if not E:return{},F.strip()
|
|
18
|
+
G=E.group(1).strip();J=E.group(2).strip();D={}
|
|
19
|
+
if G:
|
|
20
|
+
for B in G.split('\n'):
|
|
21
|
+
B=B.strip()
|
|
22
|
+
if not B:continue
|
|
23
|
+
if': 'in B:
|
|
24
|
+
C,A=B.split(': ',1);C=C.strip();A=A.strip()
|
|
25
|
+
if A.startswith('[')and A.endswith(']'):K=[A.strip().strip('"')for A in A[1:-1].split(',')if A.strip()];D[C]=K
|
|
26
|
+
elif A.startswith('"')and A.endswith('"'):D[C]=A[1:-1]
|
|
27
|
+
else:D[C]=A
|
|
28
|
+
return D,J
|
|
29
|
+
class Instruction:
|
|
30
|
+
def __init__(A,name,content,settings,src_):A.name=name;A.settings=settings;A.original_content=content;A.arguments_placeholder='$ARGUMENTS';A.src_=src_
|
|
31
|
+
def info(A):B='agents';C=A.original_content;return{'name':A.name,_E:A.settings.get(_E,''),B:A.settings.get(B,[]),'content':C,'source':A.src_}
|
|
32
|
+
def execute(A,arguments=''):B=A.original_content.replace(A.arguments_placeholder,arguments);return B
|
|
33
|
+
class InstructionManager:
|
|
34
|
+
def __init__(A,project_dir=_A):
|
|
35
|
+
C='commands';B='.autodev';A.project_dir=project_dir;A.user_dir=os.path.join(str(Path.home()),B,C)
|
|
36
|
+
if A.project_dir:A.project_commands_dir=os.path.abspath(os.path.join(A.project_dir,B,C))
|
|
37
|
+
else:A.project_commands_dir=_A
|
|
38
|
+
A.instructions={_B:{},_C:{}}
|
|
39
|
+
if A.project_commands_dir and not os.path.exists(A.project_commands_dir):os.makedirs(A.project_commands_dir)
|
|
40
|
+
if not os.path.exists(A.user_dir):os.makedirs(A.user_dir)
|
|
41
|
+
A.reload_instructions()
|
|
42
|
+
def bw(O,dir_path,source):
|
|
43
|
+
F=source;C=dir_path;A={}
|
|
44
|
+
if not os.path.exists(C):return A
|
|
45
|
+
for(G,P,I)in os.walk(C):
|
|
46
|
+
for D in I:
|
|
47
|
+
H=os.path.relpath(G,C)
|
|
48
|
+
if H=='.':B=_D
|
|
49
|
+
else:B=H.replace(os.sep,'/')
|
|
50
|
+
if B not in A:A[B]={}
|
|
51
|
+
if D.endswith('.md'):
|
|
52
|
+
J=os.path.join(G,D);E=os.path.splitext(D)[0]
|
|
53
|
+
try:K,L=read_instruction_file(J);M=Instruction(E,L,K,F);A[B][E]=M
|
|
54
|
+
except Exception as N:print(f"从 {F} 加载命令 {B}/{E} 失败: {str(N)}")
|
|
55
|
+
return A
|
|
56
|
+
def reload_instructions(A):
|
|
57
|
+
A.instructions[_B].clear();A.instructions[_C].clear();A.instructions[_C]=A.bw(A.user_dir,_C)
|
|
58
|
+
if A.project_commands_dir:A.instructions[_B]=A.bw(A.project_commands_dir,_B)
|
|
59
|
+
def bx(A,source,domain,name):
|
|
60
|
+
B=source
|
|
61
|
+
if B==_B:
|
|
62
|
+
if not A.project_commands_dir:raise ValueError('项目目录未初始化,无法操作project来源的指令')
|
|
63
|
+
C=A.project_commands_dir
|
|
64
|
+
elif B==_C:C=A.user_dir
|
|
65
|
+
else:raise ValueError(f"无效的来源: {B},仅支持 'project' 或 'user'")
|
|
66
|
+
D=os.path.join(C,domain.replace('/',os.sep));return os.path.join(D,f"{name}.md")
|
|
67
|
+
def exists(A,domain,name,source=_A):
|
|
68
|
+
C=source;B=domain
|
|
69
|
+
if C:return name in A.instructions.get(C,{}).get(B,{})
|
|
70
|
+
else:
|
|
71
|
+
for D in[_B,_C]:
|
|
72
|
+
if name in A.instructions.get(D,{}).get(B,{}):return _G
|
|
73
|
+
return False
|
|
74
|
+
def get_instruction_with_source(A,domain,name):
|
|
75
|
+
C=name;B=domain
|
|
76
|
+
if C in A.instructions.get(_B,{}).get(B,{}):return A.instructions[_B][B][C],_B
|
|
77
|
+
elif C in A.instructions.get(_C,{}).get(B,{}):return A.instructions[_C][B][C],_C
|
|
78
|
+
else:return _A,_A
|
|
79
|
+
def add_instruction(B,domain,name,settings,content,source=_B):
|
|
80
|
+
E=source;D=name;C=domain
|
|
81
|
+
if B.exists(C,D,E):raise ValueError(f"指令 '{E}/{C}/{D}' 已存在")
|
|
82
|
+
G=B.bx(E,C,D);os.makedirs(os.path.dirname(G),exist_ok=_G)
|
|
83
|
+
with open(G,'w',encoding=_I)as A:
|
|
84
|
+
A.write('---\n')
|
|
85
|
+
for(H,F)in settings.items():
|
|
86
|
+
if isinstance(F,list):A.write(f"{H}: [{', '.join([repr(A)for A in F])}]\n")
|
|
87
|
+
else:A.write(f'{H}: "{F}"\n')
|
|
88
|
+
A.write(_J);A.write(content)
|
|
89
|
+
B.reload_instructions();I,J=B.get_instruction_with_source(C,D);return I
|
|
90
|
+
def update_instruction(C,domain,name,settings=_A,content=_A,source=_A):
|
|
91
|
+
J=content;I=settings;F=source;B=name;A=domain
|
|
92
|
+
if F:
|
|
93
|
+
if not C.exists(A,B,F):raise ValueError(f"指令 '{F}/{A}/{B}' 不存在")
|
|
94
|
+
G=F
|
|
95
|
+
else:
|
|
96
|
+
M,G=C.get_instruction_with_source(A,B)
|
|
97
|
+
if not G:raise ValueError(f"指令 '{A}/{B}' 不存在于任何来源")
|
|
98
|
+
D=C.instructions[G][A][B]
|
|
99
|
+
if I is not _A:D.settings=I
|
|
100
|
+
if J is not _A:D.original_content=J
|
|
101
|
+
K=C.bx(G,A,B);os.makedirs(os.path.dirname(K),exist_ok=_G)
|
|
102
|
+
with open(K,'w',encoding=_I)as E:
|
|
103
|
+
E.write('---\n')
|
|
104
|
+
for(L,H)in D.settings.items():
|
|
105
|
+
if isinstance(H,list):E.write(f"{L}: [{', '.join([repr(A)for A in H])}]\n")
|
|
106
|
+
else:E.write(f'{L}: "{H}"\n')
|
|
107
|
+
E.write(_J);E.write(D.original_content)
|
|
108
|
+
C.reload_instructions();return D
|
|
109
|
+
def delete_instruction(C,domain,name,source=_A):
|
|
110
|
+
D=source;B=name;A=domain
|
|
111
|
+
if D:
|
|
112
|
+
if not C.exists(A,B,D):raise ValueError(f"指令 '{D}/{A}/{B}' 不存在")
|
|
113
|
+
E=D
|
|
114
|
+
else:
|
|
115
|
+
G,E=C.get_instruction_with_source(A,B)
|
|
116
|
+
if not E:raise ValueError(f"指令 '{A}/{B}' 不存在于任何来源")
|
|
117
|
+
F=C.bx(E,A,B)
|
|
118
|
+
if os.path.exists(F):os.remove(F)
|
|
119
|
+
C.reload_instructions()
|
|
120
|
+
def load_all_instructions(A,domain=_D,source=_A):
|
|
121
|
+
D=domain;B=source;C=[]
|
|
122
|
+
if B:
|
|
123
|
+
if B in A.instructions:C.extend(list(A.instructions[B].get(D,{}).values()))
|
|
124
|
+
else:
|
|
125
|
+
for E in[_B,_C]:C.extend(list(A.instructions[E].get(D,{}).values()))
|
|
126
|
+
return C
|
|
127
|
+
def get_instruction(A,domain,instruction_name,source=_A):
|
|
128
|
+
D=source;C=instruction_name;B=domain
|
|
129
|
+
if D:return A.instructions.get(D,{}).get(B,{}).get(C,_A)
|
|
130
|
+
else:
|
|
131
|
+
E=A.instructions.get(_B,{}).get(B,{}).get(C)
|
|
132
|
+
if E:return E
|
|
133
|
+
return A.instructions.get(_C,{}).get(B,{}).get(C,_A)
|
|
134
|
+
def list_domains(A,source=_A):
|
|
135
|
+
B=source;C=set()
|
|
136
|
+
if B:
|
|
137
|
+
if B in A.instructions:C.update(A.instructions[B].keys())
|
|
138
|
+
else:
|
|
139
|
+
for D in[_B,_C]:C.update(A.instructions[D].keys())
|
|
140
|
+
return list(C)
|
|
141
|
+
def list_instructions(D,domain=_A,source=_A):
|
|
142
|
+
F=source;B=domain;A={};L=[F]if F else[_B,_C]
|
|
143
|
+
for E in L:
|
|
144
|
+
if E not in D.instructions:continue
|
|
145
|
+
if B:
|
|
146
|
+
if B in D.instructions[E]:
|
|
147
|
+
if B not in A:A[B]=[]
|
|
148
|
+
A[B].extend(list(D.instructions[E][B].values()))
|
|
149
|
+
else:
|
|
150
|
+
for(C,M)in D.instructions[E].items():
|
|
151
|
+
if C not in A:A[C]=[]
|
|
152
|
+
A[C].extend(M.values())
|
|
153
|
+
if B:return A.get(B,[])
|
|
154
|
+
for C in A:
|
|
155
|
+
G=set();H=[]
|
|
156
|
+
for I in A[C]:
|
|
157
|
+
J=I.name
|
|
158
|
+
if J not in G:G.add(J);H.append(I)
|
|
159
|
+
A[C]=H
|
|
160
|
+
K=[]
|
|
161
|
+
for B in A.keys():N=A[B];K.extend(N)
|
|
162
|
+
return K
|
|
163
|
+
def descriptions_(B,source=_A):
|
|
164
|
+
C=source;D={};E=[C]if C else[_B,_C]
|
|
165
|
+
for A in E:
|
|
166
|
+
if A not in B.instructions:continue
|
|
167
|
+
for(F,G)in B.instructions[A].items():
|
|
168
|
+
for(H,I)in G.items():D[f"{A}/{F}/{H}"]=I.settings.get(_E,'')
|
|
169
|
+
return D
|
|
170
|
+
def execute_instruction(E,domain,instruction_name,arguments='',source=_A):
|
|
171
|
+
C=source;B=instruction_name;A=domain;D=E.get_instruction(A,B,C)
|
|
172
|
+
if not D:raise ValueError(f"命令 '{A}/{B}' 不存在(来源: {C or'all'})")
|
|
173
|
+
return D.execute(arguments)
|
|
174
|
+
def parse(F,query):
|
|
175
|
+
A=query
|
|
176
|
+
if not A.strip().startswith('/'):B=_D;C=_D;G=F.execute_instruction(B,C,arguments='');return{_H:_A,_F:A.strip()}
|
|
177
|
+
D=re.findall('"[^"]*"|\\S+',A.strip())
|
|
178
|
+
if not D:return{'instruction':_A,'args':[],_F:''}
|
|
179
|
+
L=D[0][1:];H=L.split('/');B='/'.join(H[:-1]);C=H[-1];I=[];J=[];K=False
|
|
180
|
+
for E in D[1:]:
|
|
181
|
+
if E.startswith('--')and not K:I.append(E)
|
|
182
|
+
else:K=_G;M=E.strip('"');J.append(M)
|
|
183
|
+
N=' '.join(J);O=' '.join(I);G=F.execute_instruction(B,C,O);return{_H:G,_F:N}
|
|
184
|
+
def curd():
|
|
185
|
+
E='backup';D='tags';B='deploy';G='/Users/brandon/workspace/coder/autocoder';A=InstructionManager(G);A.add_instruction(domain=_D,name=B,settings={_E:'部署项目',D:['ci','cd']},content='执行部署脚本: $ARGUMENTS',source=_B);A.add_instruction(domain=_D,name=E,settings={_E:'备份用户数据',D:[E]},content='执行备份脚本: $ARGUMENTS',source=_C);A.update_instruction(domain=_D,name=B,settings={_E:'更新部署流程',D:['ci',B]},content='新的部署命令: $ARGUMENTS',source=_B);C=A.load_all_instructions(domain=_D);print('所有指令:')
|
|
186
|
+
for F in C:print(f"- {F.name} (来源: {F.src_})")
|
|
187
|
+
A.delete_instruction(_D,B,source=_B);A.delete_instruction(_D,E,source=_C);C=A.load_all_instructions(domain=_D);print('\n删除后的指令:');print([A.name for A in C])
|
|
188
|
+
def main():
|
|
189
|
+
C='/Users/brandon/workspace/coder/autodev';A=InstructionManager(C);print('=== 列出所有域 ===');print(A.list_domains());print('\n=== 列出default域的所有指令 ===');print(A.list_instructions(domain=_D));print('\n=== 所有指令描述(包含来源)===');D=A.descriptions_()
|
|
190
|
+
for(E,F)in D.items():print(f"{E}: {F}")
|
|
191
|
+
print('\n=== 测试指令解析和执行 ===');B=A.parse('/git --persona-角色名:10 --persona-CI --think-hard 项目文档');print(f"命令执行内容:\n {B[_H]}");print(f"输入内容:\n {B[_F]}");print('\n=== 测试默认指令 ===');B=A.parse('@agent 你好,项目文档');print(f"命令执行内容:\n {B[_H]}");print(f"输入内容:\n {B[_F]}")
|
|
192
|
+
if __name__=='__main__':curd()
|
|
@@ -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
|
|
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
|
|
31
|
+
def br(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.
|
|
47
|
+
if C==_C and not A.bs():A.br()
|
|
48
48
|
try:
|
|
49
49
|
A.container=A.client.containers.get(B)
|
|
50
50
|
if A.container.status==E:logger.info(f"容器 {B} 已存在且运行中,直接复用")
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
_H='.tcode/security.md'
|
|
2
|
+
_G='security.md'
|
|
3
|
+
_F='.tcode/review.md'
|
|
4
|
+
_E='review.md'
|
|
5
|
+
_D='security_contents'
|
|
6
|
+
_C='review_contents'
|
|
7
|
+
_B='utf-8'
|
|
8
|
+
_A='r'
|
|
9
|
+
from typing import TYPE_CHECKING,Annotated,NotRequired,TypedDict
|
|
10
|
+
import logging,os
|
|
11
|
+
if TYPE_CHECKING:from collections.abc import Awaitable,Callable;from langchain_core.runnables import RunnableConfig;from langgraph.runtime import Runtime
|
|
12
|
+
from langchain.agents.middleware.types import AgentMiddleware,AgentState,ContextT,ModelRequest,ModelResponse,PrivateStateAttr,ResponseT
|
|
13
|
+
from langchain.tools import ToolRuntime
|
|
14
|
+
from langchain_core.messages import ContextBlock,SystemMessage
|
|
15
|
+
from src.middlewares.utils import append_to_system_message
|
|
16
|
+
logger=logging.getLogger(__name__)
|
|
17
|
+
class DynamicContentState(AgentState):review_contents:NotRequired[Annotated[dict[str,str],PrivateStateAttr]];security_contents:NotRequired[Annotated[dict[str,str],PrivateStateAttr]]
|
|
18
|
+
class DynamicContentUpdate(TypedDict):review_contents:dict[str,str];security_contents:dict[str,str]
|
|
19
|
+
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'
|
|
20
|
+
SECURITY_SYSTEM_PROMPT=' \n<security_rules>\n {security_rules}\n</security_rules>\n\n<<security_guidelines>\n 如果用户要求对项目进行 安全检查 ,请严格依照 security_rules 标签中的规则 对项目进行安全检查 。\n</security_guidelines>\n'
|
|
21
|
+
class DynamicContentMiddleware(AgentMiddleware[DynamicContentState,ContextT,ResponseT]):
|
|
22
|
+
def __init__(A,*,home_path=os.path.expanduser('~'),project_path=os.getcwd()):A.home_path=home_path;A.project_path=project_path
|
|
23
|
+
def cu(G,prompt,content_type,contents):
|
|
24
|
+
E='( No rules loaded)';C=contents;B=content_type;A=prompt
|
|
25
|
+
if not C:return A.format(**{B:E})
|
|
26
|
+
D=[f"{A}\n{C[A]}"for A in C]
|
|
27
|
+
if not D:return A.format(**{B:E})
|
|
28
|
+
F='\n\n'.join(D);return A.format(**{B:F})
|
|
29
|
+
def before_agent(A,state,runtime,config):
|
|
30
|
+
I=state
|
|
31
|
+
if _C in I and _D in I:return
|
|
32
|
+
C={};D=os.path.join(A.project_path,_E);E=os.path.join(A.home_path,_F)
|
|
33
|
+
if A.project_path and os.path.exists(D):
|
|
34
|
+
with open(D,_A,encoding=_B)as B:C[D]=B.read()
|
|
35
|
+
if A.home_path and os.path.exists(E):
|
|
36
|
+
with open(E,_A,encoding=_B)as B:C[E]=B.read()
|
|
37
|
+
F={};G=os.path.join(A.project_path,_G);H=os.path.join(A.home_path,_H)
|
|
38
|
+
if A.project_path and os.path.exists(G):
|
|
39
|
+
with open(G,_A,encoding=_B)as B:F[G]=B.read()
|
|
40
|
+
if A.home_path and os.path.exists(H):
|
|
41
|
+
with open(H,_A,encoding=_B)as B:F[H]=B.read()
|
|
42
|
+
return DynamicContentUpdate(review_contents=C,security_contents=F)
|
|
43
|
+
async def after_agent(A,state,runtime,config):
|
|
44
|
+
I=state
|
|
45
|
+
if _C in I and _D in I:return
|
|
46
|
+
C={};D=os.path.join(A.project_path,_E);E=os.path.join(A.home_path,_F)
|
|
47
|
+
if A.project_path and os.path.exists(D):
|
|
48
|
+
with open(D,_A,encoding=_B)as B:C[D]=B.read()
|
|
49
|
+
if A.home_path and os.path.exists(E):
|
|
50
|
+
with open(E,_A,encoding=_B)as B:C[E]=B.read()
|
|
51
|
+
F={};G=os.path.join(A.project_path,_G);H=os.path.join(A.home_path,_H)
|
|
52
|
+
if A.project_path and os.path.exists(G):
|
|
53
|
+
with open(G,_A,encoding=_B)as B:F[G]=B.read()
|
|
54
|
+
if A.home_path and os.path.exists(H):
|
|
55
|
+
with open(H,_A,encoding=_B)as B:F[H]=B.read()
|
|
56
|
+
return DynamicContentUpdate(review_contents=C,security_contents=F)
|
|
57
|
+
def modify_request(E,request):
|
|
58
|
+
B=request;F=B.state.get(_C,{});G=B.state.get(_D,{});C=E.cu(C,'review_rules',F);D=E.cu(D,'security_rules',G);A=B.system_message
|
|
59
|
+
if C:A=append_to_system_message(A,C)
|
|
60
|
+
if D:A=append_to_system_message(A,D)
|
|
61
|
+
return B.override(system_message=A)
|
|
62
|
+
def wrap_model_call(A,request,handler):B=A.modify_request(request);return handler(B)
|
|
63
|
+
async def awrap_model_call(A,request,handler):B=A.modify_request(request);return await handler(B)
|
|
@@ -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
|
|
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
|
|
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.
|
|
29
|
+
J,K=E.cd(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.
|
|
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]
|
|
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
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
_B='utf-8'
|
|
3
|
+
_A='memory_contents'
|
|
4
|
+
import logging,os
|
|
5
|
+
from typing import TYPE_CHECKING,Annotated,NotRequired,TypedDict
|
|
6
|
+
if TYPE_CHECKING:from collections.abc import Awaitable,Callable;from langchain_core.runnables import RunnableConfig;from langgraph.runtime import Runtime
|
|
7
|
+
from langchain.agents.middleware.types import AgentMiddleware,AgentState,ContextT,ModelRequest,ModelResponse,PrivateStateAttr,ResponseT
|
|
8
|
+
from langchain.tools import ToolRuntime
|
|
9
|
+
logger=logging.getLogger(__name__)
|
|
10
|
+
from src.middlewares.utils import append_to_system_message
|
|
11
|
+
class MemoryState(AgentState):memory_contents:NotRequired[Annotated[dict[str,str],PrivateStateAttr]]
|
|
12
|
+
class MemoryStateUpdate(TypedDict):memory_contents:dict[str,str]
|
|
13
|
+
MEMORY_SYSTEM_PROMPT=' \n<agent_memory>\n{agent_memory}\n</agent_memory>\n\n<memory_guidelines>\n The above <agent_memory> was loaded in from files in your filesystem. As you learn from your interactions with the user, you can save new knowledge by calling the `edit_file` tool.\n\n **Learning from feedback:**\n - One of your MAIN PRIORITIES is to learn from your interactions with the user. These learnings can be implicit or explicit. This means that in the future, you will remember this important information.\n - When you need to remember something, updating memory must be your FIRST, IMMEDIATE action - before responding to the user, before calling other tools, before doing anything else. Just update memory immediately.\n - When user says something is better/worse, capture WHY and encode it as a pattern.\n - Each correction is a chance to improve permanently - don\'t just fix the immediate issue, update your instructions.\n - A great opportunity to update your memories is when the user interrupts a tool call and provides feedback. You should update your memories immediately before revising the tool call.\n - Look for the underlying principle behind corrections, not just the specific mistake.\n - The user might not explicitly ask you to remember something, but if they provide information that is useful for future use, you should update your memories immediately.\n\n **Asking for information:**\n - If you lack context to perform an action (e.g. send a Slack DM, requires a user ID/email) you should explicitly ask the user for this information.\n - It is preferred for you to ask for information, don\'t assume anything that you do not know!\n - When the user provides information that is useful for future use, you should update your memories immediately.\n\n **When to update memories:**\n - When the user explicitly asks you to remember something (e.g., "remember my email", "save this preference")\n - When the user describes your role or how you should behave (e.g., "you are a web researcher", "always do X")\n - When the user gives feedback on your work - capture what was wrong and how to improve\n - When the user provides information required for tool use (e.g., slack channel ID, email addresses)\n - When the user provides context useful for future tasks, such as how to use tools, or which actions to take in a particular situation\n - When you discover new patterns or preferences (coding styles, conventions, workflows)\n\n **When to NOT update memories:**\n - When the information is temporary or transient (e.g., "I\'m running late", "I\'m on my phone right now")\n - When the information is a one-time task request (e.g., "Find me a recipe", "What\'s 25 * 4?")\n - When the information is a simple question that doesn\'t reveal lasting preferences (e.g., "What day is it?", "Can you explain X?")\n - When the information is an acknowledgment or small talk (e.g., "Sounds good!", "Hello", "Thanks for that")\n - When the information is stale or irrelevant in future conversations\n - Never store API keys, access tokens, passwords, or any other credentials in any file, memory, or system prompt.\n - If the user asks where to put API keys or provides an API key, do NOT echo or save it.\n\n **Examples:**\n Example 1 (remembering user information):\n User: Can you connect to my google account?\n Agent: Sure, I\'ll connect to your google account, what\'s your google account email?\n User: john@example.com\n Agent: Let me save this to my memory.\n Tool Call: edit_file(...) -> remembers that the user\'s google account email is john@example.com\n\n Example 2 (remembering implicit user preferences):\n User: Can you write me an example for creating a deep agent in LangChain?\n Agent: Sure, I\'ll write you an example for creating a deep agent in LangChain <example code in Python>\n User: Can you do this in JavaScript\n Agent: Let me save this to my memory.\n Tool Call: edit_file(...) -> remembers that the user prefers to get LangChain code examples in JavaScript\n Agent: Sure, here is the JavaScript example<example code in JavaScript>\n\n Example 3 (do not remember transient information):\n User: I\'m going to play basketball tonight so I will be offline for a few hours.\n Agent: Okay I\'ll add a block to your calendar.\n Tool Call: create_calendar_event(...) -> just calls a tool, does not commit anything to memory, as it is transient information\n</memory_guidelines>\n'
|
|
14
|
+
class MemoryMiddleware(AgentMiddleware[MemoryState,ContextT,ResponseT]):
|
|
15
|
+
state_schema=MemoryState
|
|
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):
|
|
18
|
+
C='(No memory loaded)';A=contents
|
|
19
|
+
if not A:return MEMORY_SYSTEM_PROMPT.format(agent_memory=C)
|
|
20
|
+
B=[f"{B}\n{A[B]}"for B in D.sources if A.get(B)]
|
|
21
|
+
if not B:return MEMORY_SYSTEM_PROMPT.format(agent_memory=C)
|
|
22
|
+
E='\n\n'.join(B);return MEMORY_SYSTEM_PROMPT.format(agent_memory=E)
|
|
23
|
+
def before_agent(A,state,runtime,config):
|
|
24
|
+
F='.tcode/MEMORY.md'
|
|
25
|
+
if _A in state:return
|
|
26
|
+
B={};C=os.path.join(A.project_path,F);D=os.path.join(A.home_path,F)
|
|
27
|
+
if A.project_path and os.path.exists(C):
|
|
28
|
+
with open(C,'r',encoding=_B)as E:B[C]=E.read()
|
|
29
|
+
if A.home_path and os.path.exists(D):
|
|
30
|
+
with open(D,'r',encoding=_B)as E:B[D]=E.read()
|
|
31
|
+
return MemoryStateUpdate(memory_contents=B)
|
|
32
|
+
async def abefore_agent(B,state,runtime,config):
|
|
33
|
+
D=state
|
|
34
|
+
if _A in D:return
|
|
35
|
+
F=B._get_backend(D,runtime,config);E={};G=await F.adownload_files(list(B.sources))
|
|
36
|
+
for(C,A)in zip(B.sources,G,strict=True):
|
|
37
|
+
if A.error is not None:
|
|
38
|
+
if A.error=='file_not_found':continue
|
|
39
|
+
H=f"Failed to download {C}: {A.error}";raise ValueError(H)
|
|
40
|
+
if A.content is not None:E[C]=A.content.decode(_B);logger.debug('Loaded memory from: %s',C)
|
|
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)
|
|
43
|
+
def wrap_model_call(A,request,handler):B=A.modify_request(request);return handler(B)
|
|
44
|
+
async def awrap_model_call(A,request,handler):B=A.modify_request(request);return await handler(B)
|