-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathconversation.py
More file actions
215 lines (182 loc) · 8.4 KB
/
Copy pathconversation.py
File metadata and controls
215 lines (182 loc) · 8.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
# 来源:公众号@小林coding
# 后端八股网站:xiaolincoding.com
# Agent网站:xiaolinnote.com
# 简历模版:jianli.xiaolinnote.com
from __future__ import annotations
import json
from dataclasses import dataclass, field
from typing import Any
@dataclass
class ToolUseBlock:
tool_use_id: str
tool_name: str
arguments: dict[str, Any]
@dataclass
class ToolResultBlock:
tool_use_id: str
content: str
is_error: bool = False
# 结构化 content block;填了就用它替代 content 发出去(见 serialization.py)。
# content 里仍保留等价文本,token 估算和 TUI 展示都走它。
content_blocks: list[dict[str, Any]] | None = None
@dataclass
class ThinkingBlock:
thinking: str
signature: str
@dataclass
class Message:
role: str # "user" | "assistant"
content: str
tool_uses: list[ToolUseBlock] = field(default_factory=list)
tool_results: list[ToolResultBlock] = field(default_factory=list)
thinking_blocks: list[ThinkingBlock] = field(default_factory=list)
# 估算最后一次 API 用量锚点之后追加的消息 token 开销时使用的字符/token 比率。
# 与 context.manager 中的恢复状态启发值保持一致,全代码库统一使用同一比率。
_CHARS_PER_TOKEN = 3.5
def _message_chars(m: Message) -> int:
n = len(m.content)
for tb in m.thinking_blocks:
n += len(tb.thinking)
for tu in m.tool_uses:
n += len(tu.tool_name) + len(json.dumps(tu.arguments, ensure_ascii=False))
for tr in m.tool_results:
n += len(tr.content)
return n
def estimate_tokens(messages: list[Message]) -> int:
"""基于字符数对一组消息做 token 估算。
刻意做得粗略——它只覆盖那些尚未锚定到真实 API 用量数值的消息,这部分的
精确度本就无关紧要。统计内容包括消息正文、thinking、工具调用参数以及
工具结果内容。
"""
total = sum(_message_chars(m) for m in messages)
return int(total / _CHARS_PER_TOKEN)
@dataclass
class ConversationManager:
history: list[Message] = field(default_factory=list)
env_injected: bool = field(default=False, init=False)
ltm_injected: bool = field(default=False, init=False)
# API 报告的每轮真实 prompt 大小,保留用于向后兼容。
# 现在与 baseline_tokens 一致(input + cache_read + cache_creation + output)。
last_input_tokens: int = field(default=0, init=False)
# 真实用量锚点。baseline_tokens 是上一轮 API 计费的完整 prompt+output 大小;
# anchor_count 是记录该数值时的消息数量。两者配合让 current_tokens() 在
# anchor_count 以内信任 API 数据,只对之后追加的消息做字符估算。
# baseline_tokens == 0 表示"尚无锚点"(冷启动),此时退化为纯字符估算。
baseline_tokens: int = field(default=0, init=False)
anchor_count: int = field(default=0, init=False)
def record_usage_anchor(
self,
input_tokens: int,
output_tokens: int = 0,
cache_read: int = 0,
cache_creation: int = 0,
) -> None:
"""根据一次 API 响应钉下一个真实用量锚点。
baseline = input + cache_read + cache_creation + output。各家服务商
返回的 input_tokens 已经排除了命中缓存的 token,所以这三个 input 分量
是相加关系,合起来才是真正的 prompt 大小;之所以再加上 output,是因为
assistant 的回复此刻已成为历史的一部分。anchor_count 对齐到当前的消息
数量,这样后续新追加的消息就成了唯一需要估算的部分。
"""
self.baseline_tokens = (
input_tokens + cache_read + cache_creation + output_tokens
)
self.anchor_count = len(self.history)
# 保持旧字段同步,兼容仍在使用它的读取方。
self.last_input_tokens = self.baseline_tokens
def current_tokens(self) -> int:
"""对当前对话中的 token 数量做出最佳估算。
有锚点时:baseline(真实用量)+ 仅对锚点之后追加的那些消息做字符估算。
没有锚点时(冷启动,或刚经历一次压缩重置):对整个历史做字符估算,
这样在第一次 API 响应到来之前阈值检查依然能正常工作。
"""
if self.baseline_tokens <= 0:
return estimate_tokens(self.history)
tail = self.history[self.anchor_count :]
return self.baseline_tokens + estimate_tokens(tail)
def add_user_message(self, content: str) -> None:
self.history.append(Message(role="user", content=content))
def add_assistant_message(
self,
content: str,
tool_uses: list[ToolUseBlock] | None = None,
thinking_blocks: list[ThinkingBlock] | None = None,
) -> None:
self.history.append(
Message(
role="assistant",
content=content,
tool_uses=tool_uses or [],
thinking_blocks=thinking_blocks or [],
)
)
def add_system_reminder(self, content: str) -> None:
self.history.append(
Message(
role="user",
content=f"<system-reminder>\n{content}\n</system-reminder>",
)
)
def has_reminder_containing(self, marker: str) -> bool:
"""历史里还有没有包含 marker 的提醒。
用来判断一条「只需要说一次」的提醒是否还在上下文里。compact 会把历史压
成摘要,原来那条提醒随之消失,这时候必须重发,否则模型再也看不到。调用方
拿这个结果决定重发,就不用在 compact 那边额外挂钩子。
"""
return any(
msg.role == "user" and marker in (msg.content or "") for msg in self.history
)
def add_tool_results_message(self, tool_results: list[ToolResultBlock]) -> None:
self.history.append(Message(role="user", content="", tool_results=tool_results))
def inject_environment(self, context: str) -> None:
if not self.env_injected:
self.history.insert(0, Message(role="user", content=context))
self.env_injected = True
def inject_long_term_memory(
self, instructions: str, memories: str, skills: str = ""
) -> None:
if self.ltm_injected:
return
sections: list[str] = []
if instructions:
sections.append(
"# kkcodeMd\n"
"Codebase and user instructions are shown below. "
"Be sure to adhere to these instructions. "
"IMPORTANT: These instructions OVERRIDE any default behavior "
"and you MUST follow them exactly as written.\n\n" + instructions
)
if memories:
sections.append("# autoMemory\n" + memories)
# Skill 清单跟着项目走,放系统提示词会让每个项目各有一份、跨项目缓存全失效,
# 所以和指令、记忆一样放在这条消息里
if skills:
sections.append("# availableSkills\n" + skills)
if not sections:
return
from datetime import date
sections.append(f"# currentDate\nToday's date is {date.today().isoformat()}.")
body = "\n\n".join(sections)
wrapped = (
"<system-reminder>\n"
"As you answer the user's questions, you can use the following context:\n"
+ body
+ "\n\n IMPORTANT: this context may or may not be relevant to your tasks."
" You should not respond to this context unless it is highly relevant to your task.\n"
"</system-reminder>"
)
pos = 1 if self.env_injected else 0
self.history.insert(pos, Message(role="user", content=wrapped))
self.ltm_injected = True
def replace_history(self, new_messages: list[Message]) -> None:
self.history = new_messages
self.env_injected = False
self.ltm_injected = False
# 旧的用量锚点描述的是压缩前的对话记录,这里清除它,
# 使 current_tokens() 退化为字符估算,直到下次 API 响应
# 基于摘要后的历史重新建立锚点。
self.baseline_tokens = 0
self.anchor_count = 0
self.last_input_tokens = 0
def get_messages(self) -> list[Message]:
return list(self.history)