quanttide-agent 0.2.3__tar.gz → 0.4.0__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 (47) hide show
  1. {quanttide_agent-0.2.3 → quanttide_agent-0.4.0}/CHANGELOG.md +25 -1
  2. {quanttide_agent-0.2.3 → quanttide_agent-0.4.0}/PKG-INFO +1 -2
  3. {quanttide_agent-0.2.3 → quanttide_agent-0.4.0}/pyproject.toml +7 -2
  4. quanttide_agent-0.4.0/src/quanttide_agent/llm.py +289 -0
  5. {quanttide_agent-0.2.3 → quanttide_agent-0.4.0}/tests/test_llm.py +295 -28
  6. quanttide_agent-0.4.0/uv.lock +260 -0
  7. quanttide_agent-0.2.3/src/quanttide_agent/llm.py +0 -143
  8. quanttide_agent-0.2.3/uv.lock +0 -3
  9. {quanttide_agent-0.2.3 → quanttide_agent-0.4.0}/.gitignore +0 -0
  10. {quanttide_agent-0.2.3 → quanttide_agent-0.4.0}/AGENTS.md +0 -0
  11. {quanttide_agent-0.2.3 → quanttide_agent-0.4.0}/LICENSE +0 -0
  12. {quanttide_agent-0.2.3 → quanttide_agent-0.4.0}/README.md +0 -0
  13. {quanttide_agent-0.2.3 → quanttide_agent-0.4.0}/ROADMAP.md +0 -0
  14. {quanttide_agent-0.2.3 → quanttide_agent-0.4.0}/STATUS.md +0 -0
  15. {quanttide_agent-0.2.3 → quanttide_agent-0.4.0}/docs/README.md +0 -0
  16. {quanttide_agent-0.2.3 → quanttide_agent-0.4.0}/docs/api.md +0 -0
  17. {quanttide_agent-0.2.3 → quanttide_agent-0.4.0}/examples/__init__.py +0 -0
  18. {quanttide_agent-0.2.3 → quanttide_agent-0.4.0}/examples/autogen/.gitignore +0 -0
  19. {quanttide_agent-0.2.3 → quanttide_agent-0.4.0}/examples/autogen/__init__.py +0 -0
  20. {quanttide_agent-0.2.3 → quanttide_agent-0.4.0}/examples/autogen/quick_start.py +0 -0
  21. {quanttide_agent-0.2.3 → quanttide_agent-0.4.0}/examples/autogen/requirements.txt +0 -0
  22. {quanttide_agent-0.2.3 → quanttide_agent-0.4.0}/examples/rag/README.md +0 -0
  23. {quanttide_agent-0.2.3 → quanttide_agent-0.4.0}/examples/rag/__init__.py +0 -0
  24. {quanttide_agent-0.2.3 → quanttide_agent-0.4.0}/examples/rag/chunker.py +0 -0
  25. {quanttide_agent-0.2.3 → quanttide_agent-0.4.0}/examples/rag/compare.py +0 -0
  26. {quanttide_agent-0.2.3 → quanttide_agent-0.4.0}/examples/rag/config.py +0 -0
  27. {quanttide_agent-0.2.3 → quanttide_agent-0.4.0}/examples/rag/embedding.py +0 -0
  28. {quanttide_agent-0.2.3 → quanttide_agent-0.4.0}/examples/rag/llms.py +0 -0
  29. {quanttide_agent-0.2.3 → quanttide_agent-0.4.0}/examples/rag/main.py +0 -0
  30. {quanttide_agent-0.2.3 → quanttide_agent-0.4.0}/examples/rag/models.py +0 -0
  31. {quanttide_agent-0.2.3 → quanttide_agent-0.4.0}/examples/rag/read_file.py +0 -0
  32. {quanttide_agent-0.2.3 → quanttide_agent-0.4.0}/examples/rag/requirements.txt +0 -0
  33. {quanttide_agent-0.2.3 → quanttide_agent-0.4.0}/examples/rag/text_process.py +0 -0
  34. {quanttide_agent-0.2.3 → quanttide_agent-0.4.0}/examples/tencent/.gitignore +0 -0
  35. {quanttide_agent-0.2.3 → quanttide_agent-0.4.0}/examples/tencent/__init__.py +0 -0
  36. {quanttide_agent-0.2.3 → quanttide_agent-0.4.0}/examples/tencent/config.py +0 -0
  37. {quanttide_agent-0.2.3 → quanttide_agent-0.4.0}/examples/tencent/hunyuan.py +0 -0
  38. {quanttide_agent-0.2.3 → quanttide_agent-0.4.0}/examples/tencent/requirements.txt +0 -0
  39. {quanttide_agent-0.2.3 → quanttide_agent-0.4.0}/examples/tencent/vectordb.py +0 -0
  40. {quanttide_agent-0.2.3 → quanttide_agent-0.4.0}/src/quanttide_agent/__init__.py +0 -0
  41. {quanttide_agent-0.2.3 → quanttide_agent-0.4.0}/src/quanttide_agent/agent.py +0 -0
  42. {quanttide_agent-0.2.3 → quanttide_agent-0.4.0}/src/quanttide_agent/config.py +0 -0
  43. {quanttide_agent-0.2.3 → quanttide_agent-0.4.0}/src/quanttide_agent/cost.py +0 -0
  44. {quanttide_agent-0.2.3 → quanttide_agent-0.4.0}/src/quanttide_agent/message.py +0 -0
  45. {quanttide_agent-0.2.3 → quanttide_agent-0.4.0}/src/quanttide_agent/tool.py +0 -0
  46. {quanttide_agent-0.2.3 → quanttide_agent-0.4.0}/tests/__init__.py +0 -0
  47. {quanttide_agent-0.2.3 → quanttide_agent-0.4.0}/tests/test_agent.py +0 -0
@@ -1,5 +1,30 @@
1
1
  # CHANGELOG
2
2
 
3
+ ## [0.4.0] - 2026-06-23
4
+
5
+ ### Added
6
+
7
+ - `AsyncLLM` class 异步 `complete()` 接口
8
+ - `BaseLLM` 基类,公开 `build_chat_body()` 和 `parse_chat_response()` 方法
9
+ - 异步测试覆盖(pytest-asyncio)
10
+
11
+ ### Changed
12
+
13
+ - `LLM` 重构为继承 `BaseLLM`,接口完全向后兼容
14
+
15
+ ## [0.3.0] - 2026-05-21
16
+
17
+ **Breaking changes:**
18
+
19
+ - `config.py` removed `settings` singleton — users must now pass values directly to `LLM()` or instantiate `Settings` explicitly
20
+ - Removed `pydantic-settings` and Vault support from `config.py`; `Settings` is now a `dataclass`
21
+ - `LLM.__init__` no longer falls back to global `settings`; uses inline defaults instead
22
+ - Removed deprecated `LLM.chat()` method — use `LLM.complete()` instead
23
+
24
+ **Dependencies:**
25
+
26
+ - Removed `pydantic-settings` from pyproject.toml
27
+
3
28
  ## [0.2.1] - 2026-05-20
4
29
  ## [0.2.3] - 2026-05-21
5
30
 
@@ -14,7 +39,6 @@
14
39
 
15
40
 
16
41
  - Fix: ReActAgent uses `role="user"` for tool results (DeepSeek API compat)
17
- - Add `LLM.complete()` method, deprecate `chat()` (removed in v0.3.0)
18
42
 
19
43
  ## [0.2.0] - 2026-05-20
20
44
 
@@ -1,13 +1,12 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: quanttide-agent
3
- Version: 0.2.3
3
+ Version: 0.4.0
4
4
  Summary: 量潮智能体标准Python工具箱
5
5
  Author-email: "QuantTide Inc." <opensource@quanttide.com>
6
6
  License: Apache 2.0
7
7
  License-File: LICENSE
8
8
  Requires-Python: >=3.10
9
9
  Requires-Dist: httpx>=0.28
10
- Requires-Dist: pydantic-settings>=2.0
11
10
  Requires-Dist: pydantic>=2.0
12
11
  Description-Content-Type: text/markdown
13
12
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "quanttide-agent"
3
- version = "0.2.3"
3
+ version = "0.4.0"
4
4
  description = "量潮智能体标准Python工具箱"
5
5
  authors = [{name = "QuantTide Inc.", email = "opensource@quanttide.com"}]
6
6
  license = {text = "Apache 2.0"}
@@ -9,7 +9,6 @@ requires-python = ">=3.10"
9
9
  dependencies = [
10
10
  "httpx>=0.28",
11
11
  "pydantic>=2.0",
12
- "pydantic-settings>=2.0",
13
12
  ]
14
13
 
15
14
  [build-system]
@@ -18,3 +17,9 @@ build-backend = "hatchling.build"
18
17
 
19
18
  [tool.hatch.build.targets.wheel]
20
19
  packages = ["src/quanttide_agent"]
20
+
21
+ [dependency-groups]
22
+ dev = [
23
+ "pytest>=8",
24
+ "pytest-asyncio>=0.24",
25
+ ]
@@ -0,0 +1,289 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Literal
4
+
5
+ import anyio
6
+ import httpx
7
+
8
+ from .config import settings
9
+ from .cost import Usage
10
+ from .message import ChatResponse, Message
11
+ from .tool import ToolCall, ToolSchema
12
+
13
+
14
+ class LLMError(Exception):
15
+ """Raised when LLM chat fails after retries.
16
+
17
+ >>> issubclass(LLMError, Exception)
18
+ True
19
+ """
20
+
21
+
22
+ class BaseLLM:
23
+ """Shared initialization and chat helpers for sync/async LLM clients."""
24
+
25
+ def __init__(
26
+ self,
27
+ model: str | None = None,
28
+ base_url: str | None = None,
29
+ api_key: str | None = None,
30
+ *,
31
+ _client: httpx.Client | httpx.AsyncClient | None = None,
32
+ ):
33
+ self.model = model or settings.llm_model
34
+ api_key = api_key or settings.llm_api_key
35
+ base_url = (base_url or settings.llm_base_url).rstrip("/")
36
+ self._client: httpx.Client | httpx.AsyncClient = _client
37
+
38
+ @staticmethod
39
+ def build_chat_body(
40
+ messages: list[Message] | list[dict] | str,
41
+ *,
42
+ model: str,
43
+ temperature: float | None = None,
44
+ max_tokens: int | None = None,
45
+ top_p: float | None = None,
46
+ stop: str | list[str] | None = None,
47
+ frequency_penalty: float | None = None,
48
+ presence_penalty: float | None = None,
49
+ thinking: bool | None = None,
50
+ reasoning_effort: Literal["low", "medium", "high", "max"] | None = None,
51
+ tools: list[ToolSchema] | None = None,
52
+ tool_choice: str | None = None,
53
+ response_format: dict | None = None,
54
+ ) -> dict:
55
+ if isinstance(messages, str):
56
+ body_messages: list[dict] = [{"role": "user", "content": messages}]
57
+ elif messages and isinstance(messages[0], Message):
58
+ body_messages = [m.to_dict() for m in messages]
59
+ else:
60
+ body_messages = messages # type: ignore
61
+
62
+ body: dict[str, Any] = {"model": model, "messages": body_messages}
63
+
64
+ _params: dict[str, Any] = {
65
+ "temperature": temperature,
66
+ "max_tokens": max_tokens,
67
+ "top_p": top_p,
68
+ "stop": stop,
69
+ "frequency_penalty": frequency_penalty,
70
+ "presence_penalty": presence_penalty,
71
+ "reasoning_effort": reasoning_effort,
72
+ "tool_choice": tool_choice,
73
+ "response_format": response_format,
74
+ }
75
+ body.update({k: v for k, v in _params.items() if v is not None})
76
+
77
+ if thinking is not None:
78
+ body["thinking"] = {"type": "enabled" if thinking else "disabled"}
79
+ if tools is not None:
80
+ body["tools"] = [
81
+ {
82
+ "type": "function",
83
+ "function": {
84
+ "name": t.name,
85
+ "description": t.description,
86
+ "parameters": t.parameters
87
+ or {"type": "object", "properties": {}},
88
+ },
89
+ }
90
+ for t in tools
91
+ ]
92
+
93
+ return body
94
+
95
+ @staticmethod
96
+ def parse_chat_response(data: dict, model: str) -> ChatResponse:
97
+ choice = data["choices"][0]
98
+ msg = choice["message"]
99
+
100
+ tool_calls = None
101
+ if msg.get("tool_calls"):
102
+ tool_calls = [
103
+ ToolCall(
104
+ id=tc["id"],
105
+ name=tc["function"]["name"],
106
+ arguments=tc["function"]["arguments"],
107
+ )
108
+ for tc in msg["tool_calls"]
109
+ ]
110
+
111
+ usage_raw = data.get("usage")
112
+ usage = Usage.from_api(usage_raw) if usage_raw else None
113
+
114
+ return ChatResponse(
115
+ content=msg.get("content", "") or "",
116
+ model=data.get("model", model),
117
+ finish_reason=choice.get("finish_reason", "stop"),
118
+ reasoning_content=msg.get("reasoning_content"),
119
+ tool_calls=tool_calls,
120
+ usage=usage,
121
+ )
122
+
123
+
124
+ class LLM(BaseLLM):
125
+ """Sync LLM client.
126
+
127
+ Usage::
128
+
129
+ llm = LLM(model="deepseek-v4-pro", api_key="sk-...")
130
+ resp = llm.complete("Hello")
131
+ print(resp.content)
132
+ """
133
+
134
+ def __init__(
135
+ self,
136
+ model: str | None = None,
137
+ base_url: str | None = None,
138
+ api_key: str | None = None,
139
+ *,
140
+ _http_client: httpx.Client | None = None,
141
+ ):
142
+ api_key = api_key or settings.llm_api_key
143
+ base_url = (base_url or settings.llm_base_url).rstrip("/")
144
+ super().__init__(
145
+ model=model,
146
+ _client=_http_client
147
+ or httpx.Client(
148
+ base_url=base_url,
149
+ headers={
150
+ "Authorization": f"Bearer {api_key}",
151
+ "Content-Type": "application/json",
152
+ },
153
+ timeout=120,
154
+ ),
155
+ )
156
+
157
+ def complete(
158
+ self,
159
+ messages: list[Message] | list[dict] | str,
160
+ *,
161
+ model: str | None = None,
162
+ temperature: float | None = None,
163
+ max_tokens: int | None = None,
164
+ top_p: float | None = None,
165
+ stop: str | list[str] | None = None,
166
+ frequency_penalty: float | None = None,
167
+ presence_penalty: float | None = None,
168
+ thinking: bool | None = None,
169
+ reasoning_effort: Literal["low", "medium", "high", "max"] | None = None,
170
+ tools: list[ToolSchema] | None = None,
171
+ tool_choice: str | None = None,
172
+ response_format: dict | None = None,
173
+ retry: int = 0,
174
+ ) -> ChatResponse:
175
+ body = self.build_chat_body(
176
+ messages,
177
+ model=model or self.model,
178
+ temperature=temperature,
179
+ max_tokens=max_tokens,
180
+ top_p=top_p,
181
+ stop=stop,
182
+ frequency_penalty=frequency_penalty,
183
+ presence_penalty=presence_penalty,
184
+ thinking=thinking,
185
+ reasoning_effort=reasoning_effort,
186
+ tools=tools,
187
+ tool_choice=tool_choice,
188
+ response_format=response_format,
189
+ )
190
+
191
+ last_error: Exception | None = None
192
+ for _ in range(max(retry + 1, 1)):
193
+ try:
194
+ resp = self._client.post("/chat/completions", json=body) # type: ignore[union-attr]
195
+ resp.raise_for_status()
196
+ data: dict = resp.json()
197
+ break
198
+ except httpx.HTTPStatusError as e:
199
+ last_error = e
200
+ continue
201
+ else:
202
+ assert last_error is not None
203
+ raise LLMError("chat failed after retries") from last_error
204
+
205
+ return self.parse_chat_response(data, model or self.model)
206
+
207
+
208
+ class AsyncLLM(BaseLLM):
209
+ """Async LLM client.
210
+
211
+ Usage::
212
+
213
+ llm = AsyncLLM(model="deepseek-v4-pro", api_key="sk-...")
214
+ resp = await llm.complete("Hello")
215
+ print(resp.content)
216
+ """
217
+
218
+ def __init__(
219
+ self,
220
+ model: str | None = None,
221
+ base_url: str | None = None,
222
+ api_key: str | None = None,
223
+ *,
224
+ _http_client: httpx.AsyncClient | None = None,
225
+ ):
226
+ api_key = api_key or settings.llm_api_key
227
+ base_url = (base_url or settings.llm_base_url).rstrip("/")
228
+ super().__init__(
229
+ model=model,
230
+ _client=_http_client
231
+ or httpx.AsyncClient(
232
+ base_url=base_url,
233
+ headers={
234
+ "Authorization": f"Bearer {api_key}",
235
+ "Content-Type": "application/json",
236
+ },
237
+ timeout=120,
238
+ ),
239
+ )
240
+
241
+ async def complete(
242
+ self,
243
+ messages: list[Message] | list[dict] | str,
244
+ *,
245
+ model: str | None = None,
246
+ temperature: float | None = None,
247
+ max_tokens: int | None = None,
248
+ top_p: float | None = None,
249
+ stop: str | list[str] | None = None,
250
+ frequency_penalty: float | None = None,
251
+ presence_penalty: float | None = None,
252
+ thinking: bool | None = None,
253
+ reasoning_effort: Literal["low", "medium", "high", "max"] | None = None,
254
+ tools: list[ToolSchema] | None = None,
255
+ tool_choice: str | None = None,
256
+ response_format: dict | None = None,
257
+ retry: int = 0,
258
+ ) -> ChatResponse:
259
+ body = self.build_chat_body(
260
+ messages,
261
+ model=model or self.model,
262
+ temperature=temperature,
263
+ max_tokens=max_tokens,
264
+ top_p=top_p,
265
+ stop=stop,
266
+ frequency_penalty=frequency_penalty,
267
+ presence_penalty=presence_penalty,
268
+ thinking=thinking,
269
+ reasoning_effort=reasoning_effort,
270
+ tools=tools,
271
+ tool_choice=tool_choice,
272
+ response_format=response_format,
273
+ )
274
+
275
+ last_error: Exception | None = None
276
+ for _ in range(max(retry + 1, 1)):
277
+ try:
278
+ resp = await self._client.post("/chat/completions", json=body)
279
+ resp.raise_for_status()
280
+ data: dict = resp.json()
281
+ break
282
+ except httpx.HTTPStatusError as e:
283
+ last_error = e
284
+ continue
285
+ else:
286
+ assert last_error is not None
287
+ raise LLMError("chat failed after retries") from last_error
288
+
289
+ return self.parse_chat_response(data, model or self.model)