quanttide-agent 0.2.0__tar.gz → 0.2.3__tar.gz
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.
- {quanttide_agent-0.2.0 → quanttide_agent-0.2.3}/CHANGELOG.md +16 -0
- {quanttide_agent-0.2.0 → quanttide_agent-0.2.3}/PKG-INFO +1 -1
- quanttide_agent-0.2.3/ROADMAP.md +19 -0
- {quanttide_agent-0.2.0 → quanttide_agent-0.2.3}/pyproject.toml +1 -1
- {quanttide_agent-0.2.0 → quanttide_agent-0.2.3}/src/quanttide_agent/agent.py +5 -5
- {quanttide_agent-0.2.0 → quanttide_agent-0.2.3}/src/quanttide_agent/llm.py +8 -2
- {quanttide_agent-0.2.0 → quanttide_agent-0.2.3}/tests/test_agent.py +4 -4
- {quanttide_agent-0.2.0 → quanttide_agent-0.2.3}/tests/test_llm.py +118 -36
- quanttide_agent-0.2.0/ROADMAP.md +0 -26
- {quanttide_agent-0.2.0 → quanttide_agent-0.2.3}/.gitignore +0 -0
- {quanttide_agent-0.2.0 → quanttide_agent-0.2.3}/AGENTS.md +0 -0
- {quanttide_agent-0.2.0 → quanttide_agent-0.2.3}/LICENSE +0 -0
- {quanttide_agent-0.2.0 → quanttide_agent-0.2.3}/README.md +0 -0
- {quanttide_agent-0.2.0 → quanttide_agent-0.2.3}/STATUS.md +0 -0
- {quanttide_agent-0.2.0 → quanttide_agent-0.2.3}/docs/README.md +0 -0
- {quanttide_agent-0.2.0 → quanttide_agent-0.2.3}/docs/api.md +0 -0
- {quanttide_agent-0.2.0 → quanttide_agent-0.2.3}/examples/__init__.py +0 -0
- {quanttide_agent-0.2.0 → quanttide_agent-0.2.3}/examples/autogen/.gitignore +0 -0
- {quanttide_agent-0.2.0 → quanttide_agent-0.2.3}/examples/autogen/__init__.py +0 -0
- {quanttide_agent-0.2.0 → quanttide_agent-0.2.3}/examples/autogen/quick_start.py +0 -0
- {quanttide_agent-0.2.0 → quanttide_agent-0.2.3}/examples/autogen/requirements.txt +0 -0
- {quanttide_agent-0.2.0 → quanttide_agent-0.2.3}/examples/rag/README.md +0 -0
- {quanttide_agent-0.2.0 → quanttide_agent-0.2.3}/examples/rag/__init__.py +0 -0
- {quanttide_agent-0.2.0 → quanttide_agent-0.2.3}/examples/rag/chunker.py +0 -0
- {quanttide_agent-0.2.0 → quanttide_agent-0.2.3}/examples/rag/compare.py +0 -0
- {quanttide_agent-0.2.0 → quanttide_agent-0.2.3}/examples/rag/config.py +0 -0
- {quanttide_agent-0.2.0 → quanttide_agent-0.2.3}/examples/rag/embedding.py +0 -0
- {quanttide_agent-0.2.0 → quanttide_agent-0.2.3}/examples/rag/llms.py +0 -0
- {quanttide_agent-0.2.0 → quanttide_agent-0.2.3}/examples/rag/main.py +0 -0
- {quanttide_agent-0.2.0 → quanttide_agent-0.2.3}/examples/rag/models.py +0 -0
- {quanttide_agent-0.2.0 → quanttide_agent-0.2.3}/examples/rag/read_file.py +0 -0
- {quanttide_agent-0.2.0 → quanttide_agent-0.2.3}/examples/rag/requirements.txt +0 -0
- {quanttide_agent-0.2.0 → quanttide_agent-0.2.3}/examples/rag/text_process.py +0 -0
- {quanttide_agent-0.2.0 → quanttide_agent-0.2.3}/examples/tencent/.gitignore +0 -0
- {quanttide_agent-0.2.0 → quanttide_agent-0.2.3}/examples/tencent/__init__.py +0 -0
- {quanttide_agent-0.2.0 → quanttide_agent-0.2.3}/examples/tencent/config.py +0 -0
- {quanttide_agent-0.2.0 → quanttide_agent-0.2.3}/examples/tencent/hunyuan.py +0 -0
- {quanttide_agent-0.2.0 → quanttide_agent-0.2.3}/examples/tencent/requirements.txt +0 -0
- {quanttide_agent-0.2.0 → quanttide_agent-0.2.3}/examples/tencent/vectordb.py +0 -0
- {quanttide_agent-0.2.0 → quanttide_agent-0.2.3}/src/quanttide_agent/__init__.py +0 -0
- {quanttide_agent-0.2.0 → quanttide_agent-0.2.3}/src/quanttide_agent/config.py +0 -0
- {quanttide_agent-0.2.0 → quanttide_agent-0.2.3}/src/quanttide_agent/cost.py +0 -0
- {quanttide_agent-0.2.0 → quanttide_agent-0.2.3}/src/quanttide_agent/message.py +0 -0
- {quanttide_agent-0.2.0 → quanttide_agent-0.2.3}/src/quanttide_agent/tool.py +0 -0
- {quanttide_agent-0.2.0 → quanttide_agent-0.2.3}/tests/__init__.py +0 -0
- {quanttide_agent-0.2.0 → quanttide_agent-0.2.3}/uv.lock +0 -0
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# CHANGELOG
|
|
2
2
|
|
|
3
|
+
## [0.2.1] - 2026-05-20
|
|
4
|
+
## [0.2.3] - 2026-05-21
|
|
5
|
+
|
|
6
|
+
### Fixed
|
|
7
|
+
|
|
8
|
+
- 修复 Vault 密钥认证失败问题
|
|
9
|
+
- 修复 pydantic-settings 依赖版本冲突
|
|
10
|
+
|
|
11
|
+
## [0.2.2] - 2026-05-20
|
|
12
|
+
|
|
13
|
+
- Fix: ActionParser fallback to {} when LLM returns invalid JSON
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
- Fix: ReActAgent uses `role="user"` for tool results (DeepSeek API compat)
|
|
17
|
+
- Add `LLM.complete()` method, deprecate `chat()` (removed in v0.3.0)
|
|
18
|
+
|
|
3
19
|
## [0.2.0] - 2026-05-20
|
|
4
20
|
|
|
5
21
|
**Breaking changes:**
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# ROADMAP
|
|
2
|
+
|
|
3
|
+
## v0.2.x — 已完成
|
|
4
|
+
|
|
5
|
+
- [x] `LLM.complete()` 替换 `chat()`(chat 标记废弃,v0.3.0 移除)
|
|
6
|
+
- [x] `ReActAgent` + `ActionParser` + `Tool` 进入标准库
|
|
7
|
+
- [x] `config` 模块 with pydantic-settings + vault 支持
|
|
8
|
+
- [x] `Usage.from_api()` 标准化字段
|
|
9
|
+
- [x] 100% 测试覆盖率
|
|
10
|
+
- [x] 在 `qtcloud-knowl` 项目中实地验证通过
|
|
11
|
+
|
|
12
|
+
## v0.3.0 计划
|
|
13
|
+
|
|
14
|
+
- 移除 `LLM.chat()`(v0.2.1 标记废弃,按约定 v0.3.0 移除)
|
|
15
|
+
|
|
16
|
+
## 待考察
|
|
17
|
+
|
|
18
|
+
- Streaming / Async — 有需求时再实现
|
|
19
|
+
- Retry 策略细化 — 遇到重试误吞时再改
|
|
@@ -53,8 +53,10 @@ class ActionParser:
|
|
|
53
53
|
raw = m.group(2).strip()
|
|
54
54
|
try:
|
|
55
55
|
inp = json.loads(raw)
|
|
56
|
+
if not isinstance(inp, dict):
|
|
57
|
+
inp = {}
|
|
56
58
|
except json.JSONDecodeError:
|
|
57
|
-
inp =
|
|
59
|
+
inp = {}
|
|
58
60
|
return Action(name=name, args=inp)
|
|
59
61
|
|
|
60
62
|
|
|
@@ -88,7 +90,7 @@ class ReActAgent:
|
|
|
88
90
|
def run(self, messages: list[Message]) -> str:
|
|
89
91
|
messages = list(messages)
|
|
90
92
|
for _ in range(self.max_steps):
|
|
91
|
-
resp = self.llm.
|
|
93
|
+
resp = self.llm.complete([m.to_dict() for m in messages])
|
|
92
94
|
output = resp.content.strip()
|
|
93
95
|
|
|
94
96
|
if "Final Answer:" in output:
|
|
@@ -106,9 +108,7 @@ class ReActAgent:
|
|
|
106
108
|
|
|
107
109
|
tool = self._tools.get(action.name)
|
|
108
110
|
result = tool.execute(action.args) if tool else f"未知工具: {action.name}"
|
|
109
|
-
messages.append(
|
|
110
|
-
Message(role="tool", tool_call_id=action.name, content=result)
|
|
111
|
-
)
|
|
111
|
+
messages.append(Message(role="user", content=result))
|
|
112
112
|
|
|
113
113
|
return "达到最大步数,未得到最终答案。"
|
|
114
114
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import warnings
|
|
3
4
|
from typing import Any, Literal
|
|
4
5
|
|
|
5
6
|
import httpx
|
|
@@ -25,7 +26,7 @@ class LLM:
|
|
|
25
26
|
Usage::
|
|
26
27
|
|
|
27
28
|
llm = LLM(model="deepseek-v4-pro", api_key="sk-...")
|
|
28
|
-
resp = llm.
|
|
29
|
+
resp = llm.complete("Hello")
|
|
29
30
|
print(resp.content)
|
|
30
31
|
"""
|
|
31
32
|
def __init__(
|
|
@@ -45,7 +46,7 @@ class LLM:
|
|
|
45
46
|
timeout=120,
|
|
46
47
|
)
|
|
47
48
|
|
|
48
|
-
def
|
|
49
|
+
def complete(
|
|
49
50
|
self,
|
|
50
51
|
messages: list[Message] | list[dict] | str,
|
|
51
52
|
*,
|
|
@@ -135,3 +136,8 @@ class LLM:
|
|
|
135
136
|
tool_calls=tool_calls,
|
|
136
137
|
usage=usage,
|
|
137
138
|
)
|
|
139
|
+
|
|
140
|
+
def chat(self, *args, **kwargs) -> ChatResponse:
|
|
141
|
+
"""Deprecated: use complete() instead. Will be removed in v0.3.0."""
|
|
142
|
+
warnings.warn("LLM.chat() is deprecated, use LLM.complete() instead", DeprecationWarning, stacklevel=2)
|
|
143
|
+
return self.complete(*args, **kwargs)
|
|
@@ -86,14 +86,14 @@ class TestTool:
|
|
|
86
86
|
class TestReActAgent:
|
|
87
87
|
def test_direct_answer(self):
|
|
88
88
|
llm = MagicMock()
|
|
89
|
-
llm.
|
|
89
|
+
llm.complete.return_value = ChatResponse(content="Final Answer: done", model="deepseek")
|
|
90
90
|
agent = ReActAgent(llm, [], max_steps=5)
|
|
91
91
|
result = agent.run([Message(role="user", content="hi")])
|
|
92
92
|
assert result == "done"
|
|
93
93
|
|
|
94
94
|
def test_tool_call_loop(self):
|
|
95
95
|
llm = MagicMock()
|
|
96
|
-
llm.
|
|
96
|
+
llm.complete.side_effect = [
|
|
97
97
|
ChatResponse(content="Action name: test\nAction args: {}", model="deepseek"),
|
|
98
98
|
ChatResponse(content="Final Answer: ok", model="deepseek"),
|
|
99
99
|
]
|
|
@@ -106,7 +106,7 @@ class TestReActAgent:
|
|
|
106
106
|
|
|
107
107
|
def test_max_steps(self):
|
|
108
108
|
llm = MagicMock()
|
|
109
|
-
llm.
|
|
109
|
+
llm.complete.return_value = ChatResponse(content="Action name: test\nAction args: {}", model="deepseek")
|
|
110
110
|
t = Tool(name="test", executor=lambda args: "ok")
|
|
111
111
|
agent = ReActAgent(llm, [t], max_steps=2)
|
|
112
112
|
result = agent.run([Message(role="user", content="x")])
|
|
@@ -114,7 +114,7 @@ class TestReActAgent:
|
|
|
114
114
|
|
|
115
115
|
def test_malformed_action(self):
|
|
116
116
|
llm = MagicMock()
|
|
117
|
-
llm.
|
|
117
|
+
llm.complete.side_effect = [
|
|
118
118
|
ChatResponse(content="乱写", model="deepseek"),
|
|
119
119
|
ChatResponse(content="Final Answer: fixed", model="deepseek"),
|
|
120
120
|
]
|
|
@@ -2,6 +2,7 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import copy
|
|
4
4
|
import json
|
|
5
|
+
from unittest.mock import patch
|
|
5
6
|
|
|
6
7
|
import httpx
|
|
7
8
|
import pytest
|
|
@@ -57,17 +58,17 @@ class TestLLMInit:
|
|
|
57
58
|
class TestChatStringInput:
|
|
58
59
|
def test_returns_chat_response(self):
|
|
59
60
|
llm, reqs = _make_llm()
|
|
60
|
-
resp = llm.
|
|
61
|
+
resp = llm.complete("Hello")
|
|
61
62
|
assert isinstance(resp, ChatResponse)
|
|
62
63
|
|
|
63
64
|
def test_content(self):
|
|
64
65
|
llm, reqs = _make_llm()
|
|
65
|
-
resp = llm.
|
|
66
|
+
resp = llm.complete("Hello")
|
|
66
67
|
assert resp.content == "Hello! How can I help?"
|
|
67
68
|
|
|
68
69
|
def test_sends_correct_body(self):
|
|
69
70
|
llm, reqs = _make_llm()
|
|
70
|
-
llm.
|
|
71
|
+
llm.complete("Hello")
|
|
71
72
|
body = _body(reqs[0])
|
|
72
73
|
assert body["model"] == "deepseek-v4-pro"
|
|
73
74
|
assert body["messages"][0]["role"] == "user"
|
|
@@ -81,12 +82,12 @@ class TestChatListInput:
|
|
|
81
82
|
{"role": "system", "content": "You are helpful"},
|
|
82
83
|
{"role": "user", "content": "Hi"},
|
|
83
84
|
]
|
|
84
|
-
resp = llm.
|
|
85
|
+
resp = llm.complete(messages)
|
|
85
86
|
assert resp.content == "Hello! How can I help?"
|
|
86
87
|
|
|
87
88
|
def test_sends_messages_in_body(self):
|
|
88
89
|
llm, reqs = _make_llm()
|
|
89
|
-
llm.
|
|
90
|
+
llm.complete([{"role": "user", "content": "test"}])
|
|
90
91
|
body = _body(reqs[0])
|
|
91
92
|
assert len(body["messages"]) == 1
|
|
92
93
|
assert body["messages"][0]["role"] == "user"
|
|
@@ -96,42 +97,42 @@ class TestChatListInput:
|
|
|
96
97
|
class TestChatParameters:
|
|
97
98
|
def test_temperature(self):
|
|
98
99
|
llm, reqs = _make_llm()
|
|
99
|
-
llm.
|
|
100
|
+
llm.complete("Hi", temperature=0.7)
|
|
100
101
|
assert _body(reqs[0])["temperature"] == 0.7
|
|
101
102
|
|
|
102
103
|
def test_max_tokens(self):
|
|
103
104
|
llm, reqs = _make_llm()
|
|
104
|
-
llm.
|
|
105
|
+
llm.complete("Hi", max_tokens=100)
|
|
105
106
|
assert _body(reqs[0])["max_tokens"] == 100
|
|
106
107
|
|
|
107
108
|
def test_top_p(self):
|
|
108
109
|
llm, reqs = _make_llm()
|
|
109
|
-
llm.
|
|
110
|
+
llm.complete("Hi", top_p=0.9)
|
|
110
111
|
assert _body(reqs[0])["top_p"] == 0.9
|
|
111
112
|
|
|
112
113
|
def test_stop_string(self):
|
|
113
114
|
llm, reqs = _make_llm()
|
|
114
|
-
llm.
|
|
115
|
+
llm.complete("Hi", stop="\n")
|
|
115
116
|
assert _body(reqs[0])["stop"] == "\n"
|
|
116
117
|
|
|
117
118
|
def test_stop_list(self):
|
|
118
119
|
llm, reqs = _make_llm()
|
|
119
|
-
llm.
|
|
120
|
+
llm.complete("Hi", stop=["\n", "END"])
|
|
120
121
|
assert _body(reqs[0])["stop"] == ["\n", "END"]
|
|
121
122
|
|
|
122
123
|
def test_frequency_penalty(self):
|
|
123
124
|
llm, reqs = _make_llm()
|
|
124
|
-
llm.
|
|
125
|
+
llm.complete("Hi", frequency_penalty=0.5)
|
|
125
126
|
assert _body(reqs[0])["frequency_penalty"] == 0.5
|
|
126
127
|
|
|
127
128
|
def test_presence_penalty(self):
|
|
128
129
|
llm, reqs = _make_llm()
|
|
129
|
-
llm.
|
|
130
|
+
llm.complete("Hi", presence_penalty=0.5)
|
|
130
131
|
assert _body(reqs[0])["presence_penalty"] == 0.5
|
|
131
132
|
|
|
132
133
|
def test_model_override(self):
|
|
133
134
|
llm, reqs = _make_llm()
|
|
134
|
-
llm.
|
|
135
|
+
llm.complete("Hi", model="deepseek-chat")
|
|
135
136
|
assert _body(reqs[0])["model"] == "deepseek-chat"
|
|
136
137
|
|
|
137
138
|
def test_tools(self):
|
|
@@ -139,46 +140,46 @@ class TestChatParameters:
|
|
|
139
140
|
tools = [
|
|
140
141
|
ToolSchema(name="get_weather", description="Get weather", parameters={"type": "object", "properties": {"location": {"type": "string"}}, "required": ["location"]}),
|
|
141
142
|
]
|
|
142
|
-
llm.
|
|
143
|
+
llm.complete("Weather?", tools=tools)
|
|
143
144
|
assert "tools" in _body(reqs[0])
|
|
144
145
|
|
|
145
146
|
def test_tool_choice(self):
|
|
146
147
|
llm, reqs = _make_llm()
|
|
147
|
-
llm.
|
|
148
|
+
llm.complete("Hi", tool_choice="auto")
|
|
148
149
|
assert _body(reqs[0])["tool_choice"] == "auto"
|
|
149
150
|
|
|
150
151
|
def test_response_format(self):
|
|
151
152
|
llm, reqs = _make_llm()
|
|
152
|
-
llm.
|
|
153
|
+
llm.complete("Hi", response_format={"type": "json_object"})
|
|
153
154
|
assert _body(reqs[0])["response_format"] == {"type": "json_object"}
|
|
154
155
|
|
|
155
156
|
|
|
156
157
|
class TestThinkingMode:
|
|
157
158
|
def test_thinking_enabled(self):
|
|
158
159
|
llm, reqs = _make_llm()
|
|
159
|
-
llm.
|
|
160
|
+
llm.complete("Hi", thinking=True)
|
|
160
161
|
assert _body(reqs[0])["thinking"] == {"type": "enabled"}
|
|
161
162
|
|
|
162
163
|
def test_thinking_disabled(self):
|
|
163
164
|
llm, reqs = _make_llm()
|
|
164
|
-
llm.
|
|
165
|
+
llm.complete("Hi", thinking=False)
|
|
165
166
|
assert _body(reqs[0])["thinking"] == {"type": "disabled"}
|
|
166
167
|
|
|
167
168
|
def test_reasoning_effort(self):
|
|
168
169
|
llm, reqs = _make_llm()
|
|
169
|
-
llm.
|
|
170
|
+
llm.complete("Hi", reasoning_effort="high")
|
|
170
171
|
assert _body(reqs[0])["reasoning_effort"] == "high"
|
|
171
172
|
|
|
172
173
|
def test_parses_reasoning_content(self):
|
|
173
174
|
mock = copy.deepcopy(MOCK_CHAT_RESPONSE)
|
|
174
175
|
mock["choices"][0]["message"]["reasoning_content"] = "I need to think..."
|
|
175
176
|
llm, reqs = _make_llm(mock)
|
|
176
|
-
resp = llm.
|
|
177
|
+
resp = llm.complete("Hi", thinking=True)
|
|
177
178
|
assert resp.reasoning_content == "I need to think..."
|
|
178
179
|
|
|
179
180
|
def test_no_reasoning_content_by_default(self):
|
|
180
181
|
llm, reqs = _make_llm()
|
|
181
|
-
resp = llm.
|
|
182
|
+
resp = llm.complete("Hi")
|
|
182
183
|
assert resp.reasoning_content is None
|
|
183
184
|
|
|
184
185
|
|
|
@@ -194,7 +195,7 @@ class TestToolCalls:
|
|
|
194
195
|
]
|
|
195
196
|
mock["choices"][0]["message"]["content"] = None
|
|
196
197
|
llm, reqs = _make_llm(mock)
|
|
197
|
-
resp = llm.
|
|
198
|
+
resp = llm.complete("Weather?")
|
|
198
199
|
assert resp.tool_calls is not None
|
|
199
200
|
assert len(resp.tool_calls) == 1
|
|
200
201
|
assert resp.tool_calls[0].name == "get_weather"
|
|
@@ -202,14 +203,14 @@ class TestToolCalls:
|
|
|
202
203
|
|
|
203
204
|
def test_no_tool_calls_by_default(self):
|
|
204
205
|
llm, reqs = _make_llm()
|
|
205
|
-
resp = llm.
|
|
206
|
+
resp = llm.complete("Hi")
|
|
206
207
|
assert resp.tool_calls is None
|
|
207
208
|
|
|
208
209
|
|
|
209
210
|
class TestUsage:
|
|
210
211
|
def test_parses_usage(self):
|
|
211
212
|
llm, reqs = _make_llm()
|
|
212
|
-
resp = llm.
|
|
213
|
+
resp = llm.complete("Hi")
|
|
213
214
|
assert resp.usage is not None
|
|
214
215
|
assert resp.usage.input_tokens == 10
|
|
215
216
|
assert resp.usage.output_tokens == 5
|
|
@@ -217,21 +218,21 @@ class TestUsage:
|
|
|
217
218
|
|
|
218
219
|
def test_finish_reason(self):
|
|
219
220
|
llm, reqs = _make_llm()
|
|
220
|
-
resp = llm.
|
|
221
|
+
resp = llm.complete("Hi")
|
|
221
222
|
assert resp.finish_reason == "stop"
|
|
222
223
|
|
|
223
224
|
def test_no_usage(self):
|
|
224
225
|
mock = copy.deepcopy(MOCK_CHAT_RESPONSE)
|
|
225
226
|
del mock["usage"]
|
|
226
227
|
llm, reqs = _make_llm(mock)
|
|
227
|
-
resp = llm.
|
|
228
|
+
resp = llm.complete("Hi")
|
|
228
229
|
assert resp.usage is None
|
|
229
230
|
|
|
230
231
|
def test_partial_usage(self):
|
|
231
232
|
mock = copy.deepcopy(MOCK_CHAT_RESPONSE)
|
|
232
233
|
mock["usage"] = {"prompt_tokens": 10}
|
|
233
234
|
llm, reqs = _make_llm(mock)
|
|
234
|
-
resp = llm.
|
|
235
|
+
resp = llm.complete("Hi")
|
|
235
236
|
assert resp.usage is not None
|
|
236
237
|
assert resp.usage.input_tokens == 10
|
|
237
238
|
assert resp.usage.output_tokens == 0
|
|
@@ -243,21 +244,21 @@ class TestModel:
|
|
|
243
244
|
mock = copy.deepcopy(MOCK_CHAT_RESPONSE)
|
|
244
245
|
mock["model"] = "deepseek-chat"
|
|
245
246
|
llm, reqs = _make_llm(mock)
|
|
246
|
-
resp = llm.
|
|
247
|
+
resp = llm.complete("Hi")
|
|
247
248
|
assert resp.model == "deepseek-chat"
|
|
248
249
|
|
|
249
250
|
def test_model_fallback_to_constructor(self):
|
|
250
251
|
mock = copy.deepcopy(MOCK_CHAT_RESPONSE)
|
|
251
252
|
del mock["model"]
|
|
252
253
|
llm, reqs = _make_llm(mock)
|
|
253
|
-
resp = llm.
|
|
254
|
+
resp = llm.complete("Hi")
|
|
254
255
|
assert resp.model == "deepseek-v4-pro"
|
|
255
256
|
|
|
256
257
|
|
|
257
258
|
class TestRetry:
|
|
258
259
|
def test_no_retry_on_success(self):
|
|
259
260
|
llm, reqs = _make_llm()
|
|
260
|
-
llm.
|
|
261
|
+
llm.complete("Hi", retry=2)
|
|
261
262
|
assert len(reqs) == 1
|
|
262
263
|
|
|
263
264
|
def test_retry_then_success(self):
|
|
@@ -275,7 +276,7 @@ class TestRetry:
|
|
|
275
276
|
transport = httpx.MockTransport(handler)
|
|
276
277
|
client = httpx.Client(transport=transport, base_url="http://test")
|
|
277
278
|
llm = LLM(model="deepseek-v4-pro", api_key="sk-test", _http_client=client)
|
|
278
|
-
resp = llm.
|
|
279
|
+
resp = llm.complete("Hi", retry=3)
|
|
279
280
|
assert resp.content == "Hello! How can I help?"
|
|
280
281
|
assert len(requests) == 3
|
|
281
282
|
|
|
@@ -287,7 +288,7 @@ class TestRetry:
|
|
|
287
288
|
client = httpx.Client(transport=transport, base_url="http://test")
|
|
288
289
|
llm = LLM(model="deepseek-v4-pro", api_key="sk-test", _http_client=client)
|
|
289
290
|
with pytest.raises(LLMError, match="chat failed after retries"):
|
|
290
|
-
llm.
|
|
291
|
+
llm.complete("Hi", retry=2)
|
|
291
292
|
|
|
292
293
|
def test_zero_retry_fails_once(self):
|
|
293
294
|
requests: list[httpx.Request] = []
|
|
@@ -300,7 +301,7 @@ class TestRetry:
|
|
|
300
301
|
client = httpx.Client(transport=transport, base_url="http://test")
|
|
301
302
|
llm = LLM(model="deepseek-v4-pro", api_key="sk-test", _http_client=client)
|
|
302
303
|
with pytest.raises(LLMError):
|
|
303
|
-
llm.
|
|
304
|
+
llm.complete("Hi", retry=0)
|
|
304
305
|
assert len(requests) == 1
|
|
305
306
|
|
|
306
307
|
|
|
@@ -341,7 +342,7 @@ class TestAPIConnection:
|
|
|
341
342
|
transport = httpx.MockTransport(handler)
|
|
342
343
|
client = httpx.Client(transport=transport, base_url="http://test")
|
|
343
344
|
llm = LLM(model="m", api_key="k", _http_client=client)
|
|
344
|
-
llm.
|
|
345
|
+
llm.complete("Hi")
|
|
345
346
|
assert requests[0].url.path == "/chat/completions"
|
|
346
347
|
|
|
347
348
|
|
|
@@ -350,12 +351,93 @@ class TestEdgeCases:
|
|
|
350
351
|
mock = copy.deepcopy(MOCK_CHAT_RESPONSE)
|
|
351
352
|
mock["choices"][0]["message"]["content"] = ""
|
|
352
353
|
llm, reqs = _make_llm(mock)
|
|
353
|
-
resp = llm.
|
|
354
|
+
resp = llm.complete("Hi")
|
|
354
355
|
assert resp.content == ""
|
|
355
356
|
|
|
356
357
|
def test_none_content_in_response(self):
|
|
357
358
|
mock = copy.deepcopy(MOCK_CHAT_RESPONSE)
|
|
358
359
|
mock["choices"][0]["message"]["content"] = None
|
|
359
360
|
llm, reqs = _make_llm(mock)
|
|
360
|
-
resp = llm.
|
|
361
|
+
resp = llm.complete("Hi")
|
|
361
362
|
assert resp.content == ""
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
class TestMessageInput:
|
|
366
|
+
def test_message_list(self):
|
|
367
|
+
from quanttide_agent import Message
|
|
368
|
+
|
|
369
|
+
llm, reqs = _make_llm()
|
|
370
|
+
resp = llm.complete([Message(role="user", content="Hi")])
|
|
371
|
+
assert resp.content == "Hello! How can I help?"
|
|
372
|
+
assert b"Hi" in reqs[0].read()
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
class TestChatDeprecated:
|
|
376
|
+
def test_chat_warns(self):
|
|
377
|
+
llm, reqs = _make_llm()
|
|
378
|
+
import warnings
|
|
379
|
+
|
|
380
|
+
with warnings.catch_warnings(record=True) as w:
|
|
381
|
+
warnings.simplefilter("always")
|
|
382
|
+
resp = llm.chat("Hi")
|
|
383
|
+
assert len(w) == 1
|
|
384
|
+
assert issubclass(w[0].category, DeprecationWarning)
|
|
385
|
+
assert "complete()" in str(w[0].message)
|
|
386
|
+
assert resp.content == "Hello! How can I help?"
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
class TestBaseUrlTrailingSlash:
|
|
390
|
+
def test_trailing_slash_stripped(self):
|
|
391
|
+
transport = httpx.MockTransport(lambda r: httpx.Response(200, json=MOCK_CHAT_RESPONSE))
|
|
392
|
+
client = httpx.Client(transport=transport, base_url="http://test")
|
|
393
|
+
llm = LLM(model="test", base_url="http://test/", api_key="k", _http_client=client)
|
|
394
|
+
assert str(llm._client.base_url) == "http://test"
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
class TestActionParserEdgeCases:
|
|
398
|
+
def test_non_dict_json_fallback(self):
|
|
399
|
+
from quanttide_agent.agent import ActionParser
|
|
400
|
+
|
|
401
|
+
p = ActionParser()
|
|
402
|
+
result = p.parse('Action name: test\nAction args: []')
|
|
403
|
+
assert result is not None
|
|
404
|
+
assert result.name == "test"
|
|
405
|
+
assert result.args == {}
|
|
406
|
+
|
|
407
|
+
def test_invalid_json_fallback(self):
|
|
408
|
+
from quanttide_agent.agent import ActionParser
|
|
409
|
+
|
|
410
|
+
p = ActionParser()
|
|
411
|
+
result = p.parse('Action name: test\nAction args: not-json')
|
|
412
|
+
assert result is not None
|
|
413
|
+
assert result.name == "test"
|
|
414
|
+
assert result.args == {}
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
class TestUsageFromApi:
|
|
418
|
+
def test_empty_dict(self):
|
|
419
|
+
from quanttide_agent.cost import Usage
|
|
420
|
+
|
|
421
|
+
u = Usage.from_api({"prompt_tokens": 0})
|
|
422
|
+
assert u is not None
|
|
423
|
+
assert u.input_tokens == 0
|
|
424
|
+
|
|
425
|
+
def test_none_data(self):
|
|
426
|
+
from quanttide_agent.cost import Usage
|
|
427
|
+
|
|
428
|
+
assert Usage.from_api(None) is None
|
|
429
|
+
assert Usage.from_api({}) is None
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
class TestConfigVaultFallback:
|
|
433
|
+
def test_import_fallback(self):
|
|
434
|
+
import importlib
|
|
435
|
+
import sys
|
|
436
|
+
|
|
437
|
+
with patch.dict(sys.modules, {"pydantic_vault": None}):
|
|
438
|
+
if "quanttide_agent.config" in sys.modules:
|
|
439
|
+
del sys.modules["quanttide_agent.config"]
|
|
440
|
+
mod = importlib.import_module("quanttide_agent.config")
|
|
441
|
+
importlib.reload(mod)
|
|
442
|
+
assert mod._HAS_VAULT is False
|
|
443
|
+
assert mod.VaultSettingsSource is None
|
quanttide_agent-0.2.0/ROADMAP.md
DELETED
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
# ROADMAP
|
|
2
|
-
|
|
3
|
-
## v0.2.x 目标
|
|
4
|
-
|
|
5
|
-
- [ ] 确认 v0.2.0 在实际项目中的兼容性
|
|
6
|
-
- [ ] 收集使用反馈
|
|
7
|
-
|
|
8
|
-
## 待考察方向
|
|
9
|
-
|
|
10
|
-
以下问题暂不排期。
|
|
11
|
-
|
|
12
|
-
### Provider 特化参数
|
|
13
|
-
|
|
14
|
-
`chat()` 的 `thinking` / `reasoning_effort` 参数是 DeepSeek 特化的。
|
|
15
|
-
|
|
16
|
-
- 触发条件:需要支持 DeepSeek 以外的 Provider
|
|
17
|
-
|
|
18
|
-
### Streaming / Async
|
|
19
|
-
|
|
20
|
-
- 触发条件:有项目需要流式输出
|
|
21
|
-
|
|
22
|
-
### Retry 策略
|
|
23
|
-
|
|
24
|
-
当前对所有 4xx/5xx 统一重试。
|
|
25
|
-
|
|
26
|
-
- 触发条件:实际遇到重试误吞问题
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|