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.
Files changed (46) hide show
  1. {quanttide_agent-0.2.0 → quanttide_agent-0.2.3}/CHANGELOG.md +16 -0
  2. {quanttide_agent-0.2.0 → quanttide_agent-0.2.3}/PKG-INFO +1 -1
  3. quanttide_agent-0.2.3/ROADMAP.md +19 -0
  4. {quanttide_agent-0.2.0 → quanttide_agent-0.2.3}/pyproject.toml +1 -1
  5. {quanttide_agent-0.2.0 → quanttide_agent-0.2.3}/src/quanttide_agent/agent.py +5 -5
  6. {quanttide_agent-0.2.0 → quanttide_agent-0.2.3}/src/quanttide_agent/llm.py +8 -2
  7. {quanttide_agent-0.2.0 → quanttide_agent-0.2.3}/tests/test_agent.py +4 -4
  8. {quanttide_agent-0.2.0 → quanttide_agent-0.2.3}/tests/test_llm.py +118 -36
  9. quanttide_agent-0.2.0/ROADMAP.md +0 -26
  10. {quanttide_agent-0.2.0 → quanttide_agent-0.2.3}/.gitignore +0 -0
  11. {quanttide_agent-0.2.0 → quanttide_agent-0.2.3}/AGENTS.md +0 -0
  12. {quanttide_agent-0.2.0 → quanttide_agent-0.2.3}/LICENSE +0 -0
  13. {quanttide_agent-0.2.0 → quanttide_agent-0.2.3}/README.md +0 -0
  14. {quanttide_agent-0.2.0 → quanttide_agent-0.2.3}/STATUS.md +0 -0
  15. {quanttide_agent-0.2.0 → quanttide_agent-0.2.3}/docs/README.md +0 -0
  16. {quanttide_agent-0.2.0 → quanttide_agent-0.2.3}/docs/api.md +0 -0
  17. {quanttide_agent-0.2.0 → quanttide_agent-0.2.3}/examples/__init__.py +0 -0
  18. {quanttide_agent-0.2.0 → quanttide_agent-0.2.3}/examples/autogen/.gitignore +0 -0
  19. {quanttide_agent-0.2.0 → quanttide_agent-0.2.3}/examples/autogen/__init__.py +0 -0
  20. {quanttide_agent-0.2.0 → quanttide_agent-0.2.3}/examples/autogen/quick_start.py +0 -0
  21. {quanttide_agent-0.2.0 → quanttide_agent-0.2.3}/examples/autogen/requirements.txt +0 -0
  22. {quanttide_agent-0.2.0 → quanttide_agent-0.2.3}/examples/rag/README.md +0 -0
  23. {quanttide_agent-0.2.0 → quanttide_agent-0.2.3}/examples/rag/__init__.py +0 -0
  24. {quanttide_agent-0.2.0 → quanttide_agent-0.2.3}/examples/rag/chunker.py +0 -0
  25. {quanttide_agent-0.2.0 → quanttide_agent-0.2.3}/examples/rag/compare.py +0 -0
  26. {quanttide_agent-0.2.0 → quanttide_agent-0.2.3}/examples/rag/config.py +0 -0
  27. {quanttide_agent-0.2.0 → quanttide_agent-0.2.3}/examples/rag/embedding.py +0 -0
  28. {quanttide_agent-0.2.0 → quanttide_agent-0.2.3}/examples/rag/llms.py +0 -0
  29. {quanttide_agent-0.2.0 → quanttide_agent-0.2.3}/examples/rag/main.py +0 -0
  30. {quanttide_agent-0.2.0 → quanttide_agent-0.2.3}/examples/rag/models.py +0 -0
  31. {quanttide_agent-0.2.0 → quanttide_agent-0.2.3}/examples/rag/read_file.py +0 -0
  32. {quanttide_agent-0.2.0 → quanttide_agent-0.2.3}/examples/rag/requirements.txt +0 -0
  33. {quanttide_agent-0.2.0 → quanttide_agent-0.2.3}/examples/rag/text_process.py +0 -0
  34. {quanttide_agent-0.2.0 → quanttide_agent-0.2.3}/examples/tencent/.gitignore +0 -0
  35. {quanttide_agent-0.2.0 → quanttide_agent-0.2.3}/examples/tencent/__init__.py +0 -0
  36. {quanttide_agent-0.2.0 → quanttide_agent-0.2.3}/examples/tencent/config.py +0 -0
  37. {quanttide_agent-0.2.0 → quanttide_agent-0.2.3}/examples/tencent/hunyuan.py +0 -0
  38. {quanttide_agent-0.2.0 → quanttide_agent-0.2.3}/examples/tencent/requirements.txt +0 -0
  39. {quanttide_agent-0.2.0 → quanttide_agent-0.2.3}/examples/tencent/vectordb.py +0 -0
  40. {quanttide_agent-0.2.0 → quanttide_agent-0.2.3}/src/quanttide_agent/__init__.py +0 -0
  41. {quanttide_agent-0.2.0 → quanttide_agent-0.2.3}/src/quanttide_agent/config.py +0 -0
  42. {quanttide_agent-0.2.0 → quanttide_agent-0.2.3}/src/quanttide_agent/cost.py +0 -0
  43. {quanttide_agent-0.2.0 → quanttide_agent-0.2.3}/src/quanttide_agent/message.py +0 -0
  44. {quanttide_agent-0.2.0 → quanttide_agent-0.2.3}/src/quanttide_agent/tool.py +0 -0
  45. {quanttide_agent-0.2.0 → quanttide_agent-0.2.3}/tests/__init__.py +0 -0
  46. {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:**
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: quanttide-agent
3
- Version: 0.2.0
3
+ Version: 0.2.3
4
4
  Summary: 量潮智能体标准Python工具箱
5
5
  Author-email: "QuantTide Inc." <opensource@quanttide.com>
6
6
  License: Apache 2.0
@@ -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 策略细化 — 遇到重试误吞时再改
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "quanttide-agent"
3
- version = "0.2.0"
3
+ version = "0.2.3"
4
4
  description = "量潮智能体标准Python工具箱"
5
5
  authors = [{name = "QuantTide Inc.", email = "opensource@quanttide.com"}]
6
6
  license = {text = "Apache 2.0"}
@@ -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 = raw
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.chat([m.to_dict() for m in messages])
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.chat("Hello")
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 chat(
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.chat.return_value = ChatResponse(content="Final Answer: done", model="deepseek")
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.chat.side_effect = [
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.chat.return_value = ChatResponse(content="Action name: test\nAction args: {}", model="deepseek")
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.chat.side_effect = [
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.chat("Hello")
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.chat("Hello")
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.chat("Hello")
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.chat(messages)
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.chat([{"role": "user", "content": "test"}])
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.chat("Hi", temperature=0.7)
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.chat("Hi", max_tokens=100)
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.chat("Hi", top_p=0.9)
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.chat("Hi", stop="\n")
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.chat("Hi", stop=["\n", "END"])
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.chat("Hi", frequency_penalty=0.5)
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.chat("Hi", presence_penalty=0.5)
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.chat("Hi", model="deepseek-chat")
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.chat("Weather?", tools=tools)
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.chat("Hi", tool_choice="auto")
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.chat("Hi", response_format={"type": "json_object"})
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.chat("Hi", thinking=True)
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.chat("Hi", thinking=False)
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.chat("Hi", reasoning_effort="high")
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.chat("Hi", thinking=True)
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.chat("Hi")
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.chat("Weather?")
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.chat("Hi")
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.chat("Hi")
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.chat("Hi")
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.chat("Hi")
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.chat("Hi")
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.chat("Hi")
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.chat("Hi")
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.chat("Hi", retry=2)
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.chat("Hi", retry=3)
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.chat("Hi", retry=2)
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.chat("Hi", retry=0)
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.chat("Hi")
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.chat("Hi")
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.chat("Hi")
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
@@ -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