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.
- {quanttide_agent-0.2.3 → quanttide_agent-0.4.0}/CHANGELOG.md +25 -1
- {quanttide_agent-0.2.3 → quanttide_agent-0.4.0}/PKG-INFO +1 -2
- {quanttide_agent-0.2.3 → quanttide_agent-0.4.0}/pyproject.toml +7 -2
- quanttide_agent-0.4.0/src/quanttide_agent/llm.py +289 -0
- {quanttide_agent-0.2.3 → quanttide_agent-0.4.0}/tests/test_llm.py +295 -28
- quanttide_agent-0.4.0/uv.lock +260 -0
- quanttide_agent-0.2.3/src/quanttide_agent/llm.py +0 -143
- quanttide_agent-0.2.3/uv.lock +0 -3
- {quanttide_agent-0.2.3 → quanttide_agent-0.4.0}/.gitignore +0 -0
- {quanttide_agent-0.2.3 → quanttide_agent-0.4.0}/AGENTS.md +0 -0
- {quanttide_agent-0.2.3 → quanttide_agent-0.4.0}/LICENSE +0 -0
- {quanttide_agent-0.2.3 → quanttide_agent-0.4.0}/README.md +0 -0
- {quanttide_agent-0.2.3 → quanttide_agent-0.4.0}/ROADMAP.md +0 -0
- {quanttide_agent-0.2.3 → quanttide_agent-0.4.0}/STATUS.md +0 -0
- {quanttide_agent-0.2.3 → quanttide_agent-0.4.0}/docs/README.md +0 -0
- {quanttide_agent-0.2.3 → quanttide_agent-0.4.0}/docs/api.md +0 -0
- {quanttide_agent-0.2.3 → quanttide_agent-0.4.0}/examples/__init__.py +0 -0
- {quanttide_agent-0.2.3 → quanttide_agent-0.4.0}/examples/autogen/.gitignore +0 -0
- {quanttide_agent-0.2.3 → quanttide_agent-0.4.0}/examples/autogen/__init__.py +0 -0
- {quanttide_agent-0.2.3 → quanttide_agent-0.4.0}/examples/autogen/quick_start.py +0 -0
- {quanttide_agent-0.2.3 → quanttide_agent-0.4.0}/examples/autogen/requirements.txt +0 -0
- {quanttide_agent-0.2.3 → quanttide_agent-0.4.0}/examples/rag/README.md +0 -0
- {quanttide_agent-0.2.3 → quanttide_agent-0.4.0}/examples/rag/__init__.py +0 -0
- {quanttide_agent-0.2.3 → quanttide_agent-0.4.0}/examples/rag/chunker.py +0 -0
- {quanttide_agent-0.2.3 → quanttide_agent-0.4.0}/examples/rag/compare.py +0 -0
- {quanttide_agent-0.2.3 → quanttide_agent-0.4.0}/examples/rag/config.py +0 -0
- {quanttide_agent-0.2.3 → quanttide_agent-0.4.0}/examples/rag/embedding.py +0 -0
- {quanttide_agent-0.2.3 → quanttide_agent-0.4.0}/examples/rag/llms.py +0 -0
- {quanttide_agent-0.2.3 → quanttide_agent-0.4.0}/examples/rag/main.py +0 -0
- {quanttide_agent-0.2.3 → quanttide_agent-0.4.0}/examples/rag/models.py +0 -0
- {quanttide_agent-0.2.3 → quanttide_agent-0.4.0}/examples/rag/read_file.py +0 -0
- {quanttide_agent-0.2.3 → quanttide_agent-0.4.0}/examples/rag/requirements.txt +0 -0
- {quanttide_agent-0.2.3 → quanttide_agent-0.4.0}/examples/rag/text_process.py +0 -0
- {quanttide_agent-0.2.3 → quanttide_agent-0.4.0}/examples/tencent/.gitignore +0 -0
- {quanttide_agent-0.2.3 → quanttide_agent-0.4.0}/examples/tencent/__init__.py +0 -0
- {quanttide_agent-0.2.3 → quanttide_agent-0.4.0}/examples/tencent/config.py +0 -0
- {quanttide_agent-0.2.3 → quanttide_agent-0.4.0}/examples/tencent/hunyuan.py +0 -0
- {quanttide_agent-0.2.3 → quanttide_agent-0.4.0}/examples/tencent/requirements.txt +0 -0
- {quanttide_agent-0.2.3 → quanttide_agent-0.4.0}/examples/tencent/vectordb.py +0 -0
- {quanttide_agent-0.2.3 → quanttide_agent-0.4.0}/src/quanttide_agent/__init__.py +0 -0
- {quanttide_agent-0.2.3 → quanttide_agent-0.4.0}/src/quanttide_agent/agent.py +0 -0
- {quanttide_agent-0.2.3 → quanttide_agent-0.4.0}/src/quanttide_agent/config.py +0 -0
- {quanttide_agent-0.2.3 → quanttide_agent-0.4.0}/src/quanttide_agent/cost.py +0 -0
- {quanttide_agent-0.2.3 → quanttide_agent-0.4.0}/src/quanttide_agent/message.py +0 -0
- {quanttide_agent-0.2.3 → quanttide_agent-0.4.0}/src/quanttide_agent/tool.py +0 -0
- {quanttide_agent-0.2.3 → quanttide_agent-0.4.0}/tests/__init__.py +0 -0
- {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.
|
|
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.
|
|
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)
|