asktable-advisor 1.0.3__py3-none-any.whl → 1.0.4__py3-none-any.whl
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.
- asktable_advisor/__version__.py +1 -1
- asktable_advisor/agent/advisor.py +93 -0
- asktable_advisor/agent/prompts.py +84 -0
- asktable_advisor/agent/tools.py +128 -0
- asktable_advisor/asktable/api_schema.py +226 -0
- asktable_advisor/asktable/client.py +164 -0
- {asktable_advisor-1.0.3.dist-info → asktable_advisor-1.0.4.dist-info}/METADATA +1 -1
- {asktable_advisor-1.0.3.dist-info → asktable_advisor-1.0.4.dist-info}/RECORD +12 -11
- {asktable_advisor-1.0.3.dist-info → asktable_advisor-1.0.4.dist-info}/WHEEL +0 -0
- {asktable_advisor-1.0.3.dist-info → asktable_advisor-1.0.4.dist-info}/entry_points.txt +0 -0
- {asktable_advisor-1.0.3.dist-info → asktable_advisor-1.0.4.dist-info}/licenses/LICENSE +0 -0
- {asktable_advisor-1.0.3.dist-info → asktable_advisor-1.0.4.dist-info}/top_level.txt +0 -0
asktable_advisor/__version__.py
CHANGED
|
@@ -9,6 +9,7 @@ from anthropic.types import Message, MessageParam
|
|
|
9
9
|
from ..config import AdvisorSettings
|
|
10
10
|
from ..asktable.client import AskTableClient
|
|
11
11
|
from ..asktable.inspector import AskTableInspector
|
|
12
|
+
from ..asktable.api_schema import get_api_schema
|
|
12
13
|
from ..database.manager import DatabaseManager
|
|
13
14
|
from ..database.schema_generator import SchemaGenerator
|
|
14
15
|
from ..database.data_generator import DataGenerator
|
|
@@ -43,6 +44,7 @@ class AdvisorAgent:
|
|
|
43
44
|
base_url=settings.asktable_api_base,
|
|
44
45
|
)
|
|
45
46
|
self.asktable_inspector = AskTableInspector(self.asktable_client)
|
|
47
|
+
self.api_schema = get_api_schema(settings.asktable_api_base)
|
|
46
48
|
|
|
47
49
|
# Generators
|
|
48
50
|
self.schema_generator = SchemaGenerator(self.llm_client)
|
|
@@ -199,6 +201,24 @@ class AdvisorAgent:
|
|
|
199
201
|
elif tool_name == "create_permissions":
|
|
200
202
|
return self._tool_create_permissions(tool_input)
|
|
201
203
|
|
|
204
|
+
elif tool_name == "ask_datasource":
|
|
205
|
+
return self._tool_ask_datasource(tool_input)
|
|
206
|
+
|
|
207
|
+
elif tool_name == "ask_bot":
|
|
208
|
+
return self._tool_ask_bot(tool_input)
|
|
209
|
+
|
|
210
|
+
elif tool_name == "list_available_datasources":
|
|
211
|
+
return self._tool_list_datasources(tool_input)
|
|
212
|
+
|
|
213
|
+
elif tool_name == "list_available_bots":
|
|
214
|
+
return self._tool_list_bots(tool_input)
|
|
215
|
+
|
|
216
|
+
elif tool_name == "search_asktable_api":
|
|
217
|
+
return self._tool_search_api(tool_input)
|
|
218
|
+
|
|
219
|
+
elif tool_name == "call_asktable_api":
|
|
220
|
+
return self._tool_call_api(tool_input)
|
|
221
|
+
|
|
202
222
|
elif tool_name == "ask_user":
|
|
203
223
|
return self._tool_ask_user(tool_input)
|
|
204
224
|
|
|
@@ -312,6 +332,79 @@ class AdvisorAgent:
|
|
|
312
332
|
"roles": params.get("roles", []),
|
|
313
333
|
}
|
|
314
334
|
|
|
335
|
+
def _tool_ask_datasource(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
|
336
|
+
"""Ask a question to a datasource."""
|
|
337
|
+
datasource_id = params["datasource_id"]
|
|
338
|
+
question = params["question"]
|
|
339
|
+
max_rows = params.get("max_rows")
|
|
340
|
+
|
|
341
|
+
answer = self.asktable_client.ask_datasource(
|
|
342
|
+
datasource_id=datasource_id,
|
|
343
|
+
question=question,
|
|
344
|
+
max_rows=max_rows,
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
return answer
|
|
348
|
+
|
|
349
|
+
def _tool_ask_bot(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
|
350
|
+
"""Ask a question to a bot."""
|
|
351
|
+
bot_id = params["bot_id"]
|
|
352
|
+
question = params["question"]
|
|
353
|
+
chat_id = params.get("chat_id")
|
|
354
|
+
|
|
355
|
+
message = self.asktable_client.ask_bot(
|
|
356
|
+
bot_id=bot_id,
|
|
357
|
+
question=question,
|
|
358
|
+
chat_id=chat_id,
|
|
359
|
+
)
|
|
360
|
+
|
|
361
|
+
return message
|
|
362
|
+
|
|
363
|
+
def _tool_list_datasources(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
|
364
|
+
"""List all available datasources."""
|
|
365
|
+
datasources = self.asktable_client.list_datasources()
|
|
366
|
+
|
|
367
|
+
return {
|
|
368
|
+
"datasources": datasources,
|
|
369
|
+
"count": len(datasources),
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
def _tool_list_bots(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
|
373
|
+
"""List all available bots."""
|
|
374
|
+
bots = self.asktable_client.list_bots()
|
|
375
|
+
|
|
376
|
+
return {
|
|
377
|
+
"bots": bots,
|
|
378
|
+
"count": len(bots),
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
def _tool_search_api(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
|
382
|
+
"""Search for AskTable API endpoints."""
|
|
383
|
+
query = params["query"]
|
|
384
|
+
results = self.api_schema.search_endpoints(query)
|
|
385
|
+
|
|
386
|
+
return {
|
|
387
|
+
"query": query,
|
|
388
|
+
"results": results,
|
|
389
|
+
"count": len(results),
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
def _tool_call_api(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
|
393
|
+
"""Call any AskTable API endpoint."""
|
|
394
|
+
method = params["method"]
|
|
395
|
+
path = params["path"]
|
|
396
|
+
query_params = params.get("params")
|
|
397
|
+
json_data = params.get("json_data")
|
|
398
|
+
|
|
399
|
+
result = self.asktable_client.call_api(
|
|
400
|
+
method=method,
|
|
401
|
+
path=path,
|
|
402
|
+
params=query_params,
|
|
403
|
+
json_data=json_data,
|
|
404
|
+
)
|
|
405
|
+
|
|
406
|
+
return result
|
|
407
|
+
|
|
315
408
|
def _tool_ask_user(self, params: Dict[str, Any]) -> str:
|
|
316
409
|
"""Ask user a question (returns question to be displayed)."""
|
|
317
410
|
# In CLI mode, this will be handled by the CLI layer
|
|
@@ -20,12 +20,36 @@ SYSTEM_PROMPT = """你是 AskTable Advisor,一个专业的 AskTable 场景构
|
|
|
20
20
|
- 深入理解 AskTable 的核心概念:Datasource、Bot、Chat、Role、Policy
|
|
21
21
|
- 熟悉 AskTable 的最佳实践和配置技巧
|
|
22
22
|
- 能够优化 Bot 配置,提升用户体验
|
|
23
|
+
- **能够使用 AskTable 回答用户的数据分析问题**
|
|
23
24
|
|
|
24
25
|
### 数据生成专家
|
|
25
26
|
- 能够生成真实感、符合业务逻辑的虚拟数据
|
|
26
27
|
- 确保数据之间的关联关系正确(外键约束、业务规则等)
|
|
27
28
|
- 生成的数据具有多样性和代表性
|
|
28
29
|
|
|
30
|
+
### 数据分析助手
|
|
31
|
+
- **当用户询问数据相关问题时,可以使用 AskTable 来查询和分析数据**
|
|
32
|
+
- 支持两种查询方式:
|
|
33
|
+
- **ask_datasource**:直接向数据源提问,快速获取数据
|
|
34
|
+
- **ask_bot**:通过 Bot 提问,获得更专业的分析和洞察
|
|
35
|
+
- 能够解读查询结果,为用户提供清晰的答案
|
|
36
|
+
|
|
37
|
+
### API 调用专家(新能力!)
|
|
38
|
+
- **可以调用 AskTable 的任何 API**,不仅限于预定义的工具
|
|
39
|
+
- 使用 **search_asktable_api** 搜索需要的 API 端点
|
|
40
|
+
- 使用 **call_asktable_api** 调用找到的 API
|
|
41
|
+
- 支持的功能包括:
|
|
42
|
+
- Canvas(画布)管理
|
|
43
|
+
- Report(报表)管理
|
|
44
|
+
- 高级权限配置
|
|
45
|
+
- 项目设置
|
|
46
|
+
- 以及所有 130+ 个 AskTable API 端点
|
|
47
|
+
- **工作流程**:
|
|
48
|
+
1. 用户提出需求(如"创建一个 canvas")
|
|
49
|
+
2. 使用 search_asktable_api 搜索相关 API
|
|
50
|
+
3. 使用 call_asktable_api 调用 API
|
|
51
|
+
4. 解读结果并反馈给用户
|
|
52
|
+
|
|
29
53
|
## 工作方式
|
|
30
54
|
|
|
31
55
|
### 1. 对话式交互
|
|
@@ -97,6 +121,20 @@ SYSTEM_PROMPT = """你是 AskTable Advisor,一个专业的 AskTable 场景构
|
|
|
97
121
|
|
|
98
122
|
## 处理用户请求的策略
|
|
99
123
|
|
|
124
|
+
### 回答数据分析问题(新功能!)
|
|
125
|
+
当用户询问与数据相关的问题时:
|
|
126
|
+
1. **识别问题类型**:
|
|
127
|
+
- 简单数据查询(如"最近 7 天的订单量")→ 使用 ask_datasource
|
|
128
|
+
- 需要深入分析(如"分析销售趋势并给出建议")→ 使用 ask_bot
|
|
129
|
+
2. **获取可用资源**:
|
|
130
|
+
- 调用 list_available_datasources 查看可用数据源
|
|
131
|
+
- 调用 list_available_bots 查看可用 Bot
|
|
132
|
+
3. **执行查询**:
|
|
133
|
+
- 使用 ask_datasource 或 ask_bot 获取答案
|
|
134
|
+
- 解读结果并以清晰的方式呈现给用户
|
|
135
|
+
4. **提示**:
|
|
136
|
+
- 如果没有合适的数据源或 Bot,可以建议用户先创建场景
|
|
137
|
+
|
|
100
138
|
### 创建新场景
|
|
101
139
|
1. 识别场景类型(电商、教育、CRM 等)
|
|
102
140
|
2. 询问关键信息:
|
|
@@ -118,6 +156,52 @@ SYSTEM_PROMPT = """你是 AskTable Advisor,一个专业的 AskTable 场景构
|
|
|
118
156
|
3. 提出优化方案并征求确认
|
|
119
157
|
4. 执行优化操作
|
|
120
158
|
|
|
159
|
+
### 使用动态 API 调用(强大的新功能!)
|
|
160
|
+
当用户需要使用 AskTable 的其他功能时(如 Canvas、Report、高级配置等):
|
|
161
|
+
|
|
162
|
+
1. **搜索 API**:
|
|
163
|
+
- 使用 search_asktable_api 搜索相关的 API
|
|
164
|
+
- 提供搜索关键词(如"canvas"、"report"、"role")
|
|
165
|
+
- 查看返回的 API 端点列表和描述
|
|
166
|
+
|
|
167
|
+
2. **调用 API**:
|
|
168
|
+
- 使用 call_asktable_api 调用找到的 API
|
|
169
|
+
- 指定 method(GET/POST/PUT/DELETE/PATCH)
|
|
170
|
+
- 指定 path(如 /v1/canvas)
|
|
171
|
+
- 提供必要的参数或请求体
|
|
172
|
+
|
|
173
|
+
3. **常见场景示例**:
|
|
174
|
+
- **创建 Canvas**:
|
|
175
|
+
```
|
|
176
|
+
search_asktable_api(query="canvas create")
|
|
177
|
+
call_asktable_api(method="POST", path="/v1/canvas", json_data={"name": "xxx", ...})
|
|
178
|
+
```
|
|
179
|
+
- **创建 Report**:
|
|
180
|
+
```
|
|
181
|
+
search_asktable_api(query="report")
|
|
182
|
+
call_asktable_api(method="POST", path="/v1/reports", json_data={...})
|
|
183
|
+
```
|
|
184
|
+
- **列出资源**:
|
|
185
|
+
```
|
|
186
|
+
call_asktable_api(method="GET", path="/v1/canvas")
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
4. **最佳实践**:
|
|
190
|
+
- 先搜索,再调用(确保使用正确的 API)
|
|
191
|
+
- 检查 API 的参数要求
|
|
192
|
+
- 向用户解释将要执行的操作
|
|
193
|
+
- 清晰地呈现 API 返回的结果
|
|
194
|
+
|
|
195
|
+
5. **可用的资源类型**:
|
|
196
|
+
- bots(Bot 管理)
|
|
197
|
+
- chats(对话管理)
|
|
198
|
+
- datasources(数据源管理)
|
|
199
|
+
- canvas(画布 - 可视化分析)
|
|
200
|
+
- reports(报表)
|
|
201
|
+
- roles、policies(权限管理)
|
|
202
|
+
- single-turn(单轮查询)
|
|
203
|
+
- 以及更多...
|
|
204
|
+
|
|
121
205
|
## 重要提示
|
|
122
206
|
|
|
123
207
|
1. **主动性**:不要被动等待,主动询问必要的信息
|
asktable_advisor/agent/tools.py
CHANGED
|
@@ -282,6 +282,134 @@ def get_tools() -> List[Dict[str, Any]]:
|
|
|
282
282
|
"required": ["datasource_id", "roles"],
|
|
283
283
|
},
|
|
284
284
|
},
|
|
285
|
+
{
|
|
286
|
+
"name": "ask_datasource",
|
|
287
|
+
"description": (
|
|
288
|
+
"直接向 AskTable 数据源提问,获取数据查询结果。"
|
|
289
|
+
"适用于需要快速查询数据的场景。"
|
|
290
|
+
"可以用自然语言提问,AskTable 会自动生成 SQL 并返回结果。"
|
|
291
|
+
),
|
|
292
|
+
"input_schema": {
|
|
293
|
+
"type": "object",
|
|
294
|
+
"properties": {
|
|
295
|
+
"datasource_id": {
|
|
296
|
+
"type": "string",
|
|
297
|
+
"description": "数据源 ID,可以先使用 inspect_asktable_project 获取",
|
|
298
|
+
},
|
|
299
|
+
"question": {
|
|
300
|
+
"type": "string",
|
|
301
|
+
"description": "自然语言问题,如'最近7天的销售额是多少?'",
|
|
302
|
+
},
|
|
303
|
+
"max_rows": {
|
|
304
|
+
"type": "integer",
|
|
305
|
+
"description": "最大返回行数,默认不限制",
|
|
306
|
+
},
|
|
307
|
+
},
|
|
308
|
+
"required": ["datasource_id", "question"],
|
|
309
|
+
},
|
|
310
|
+
},
|
|
311
|
+
{
|
|
312
|
+
"name": "ask_bot",
|
|
313
|
+
"description": (
|
|
314
|
+
"通过 AskTable Bot 提问,获取智能数据分析结果。"
|
|
315
|
+
"Bot 具有业务知识和分析能力,可以提供更专业的回答。"
|
|
316
|
+
"适用于需要深入分析和洞察的场景。"
|
|
317
|
+
),
|
|
318
|
+
"input_schema": {
|
|
319
|
+
"type": "object",
|
|
320
|
+
"properties": {
|
|
321
|
+
"bot_id": {
|
|
322
|
+
"type": "string",
|
|
323
|
+
"description": "Bot ID,可以先使用 inspect_asktable_project 获取",
|
|
324
|
+
},
|
|
325
|
+
"question": {
|
|
326
|
+
"type": "string",
|
|
327
|
+
"description": "自然语言问题",
|
|
328
|
+
},
|
|
329
|
+
"chat_id": {
|
|
330
|
+
"type": "string",
|
|
331
|
+
"description": "Chat ID(可选),如果要在已有对话中继续提问",
|
|
332
|
+
},
|
|
333
|
+
},
|
|
334
|
+
"required": ["bot_id", "question"],
|
|
335
|
+
},
|
|
336
|
+
},
|
|
337
|
+
{
|
|
338
|
+
"name": "list_available_datasources",
|
|
339
|
+
"description": (
|
|
340
|
+
"列出当前项目中所有可用的数据源。"
|
|
341
|
+
"用于查看可以查询哪些数据源。"
|
|
342
|
+
"返回数据源的 ID、名称、描述等信息。"
|
|
343
|
+
),
|
|
344
|
+
"input_schema": {
|
|
345
|
+
"type": "object",
|
|
346
|
+
"properties": {},
|
|
347
|
+
"required": [],
|
|
348
|
+
},
|
|
349
|
+
},
|
|
350
|
+
{
|
|
351
|
+
"name": "list_available_bots",
|
|
352
|
+
"description": (
|
|
353
|
+
"列出当前项目中所有可用的 Bot。"
|
|
354
|
+
"用于查看可以使用哪些 Bot 来回答问题。"
|
|
355
|
+
"返回 Bot 的 ID、名称、描述、能力等信息。"
|
|
356
|
+
),
|
|
357
|
+
"input_schema": {
|
|
358
|
+
"type": "object",
|
|
359
|
+
"properties": {},
|
|
360
|
+
"required": [],
|
|
361
|
+
},
|
|
362
|
+
},
|
|
363
|
+
{
|
|
364
|
+
"name": "search_asktable_api",
|
|
365
|
+
"description": (
|
|
366
|
+
"搜索 AskTable API 端点。"
|
|
367
|
+
"当需要查找特定功能的 API 时使用此工具。"
|
|
368
|
+
"返回匹配的 API 端点列表,包括路径、方法和描述。"
|
|
369
|
+
),
|
|
370
|
+
"input_schema": {
|
|
371
|
+
"type": "object",
|
|
372
|
+
"properties": {
|
|
373
|
+
"query": {
|
|
374
|
+
"type": "string",
|
|
375
|
+
"description": "搜索关键词,如'canvas'、'report'、'message'等",
|
|
376
|
+
},
|
|
377
|
+
},
|
|
378
|
+
"required": ["query"],
|
|
379
|
+
},
|
|
380
|
+
},
|
|
381
|
+
{
|
|
382
|
+
"name": "call_asktable_api",
|
|
383
|
+
"description": (
|
|
384
|
+
"调用任意 AskTable API 端点。"
|
|
385
|
+
"这是一个通用工具,可以调用 AskTable 的任何 API。"
|
|
386
|
+
"在调用前建议先使用 search_asktable_api 查找合适的 API。"
|
|
387
|
+
"支持 GET、POST、PUT、DELETE、PATCH 等方法。"
|
|
388
|
+
),
|
|
389
|
+
"input_schema": {
|
|
390
|
+
"type": "object",
|
|
391
|
+
"properties": {
|
|
392
|
+
"method": {
|
|
393
|
+
"type": "string",
|
|
394
|
+
"enum": ["GET", "POST", "PUT", "DELETE", "PATCH"],
|
|
395
|
+
"description": "HTTP 方法",
|
|
396
|
+
},
|
|
397
|
+
"path": {
|
|
398
|
+
"type": "string",
|
|
399
|
+
"description": "API 路径,如'/v1/canvas'或'/v1/reports'",
|
|
400
|
+
},
|
|
401
|
+
"params": {
|
|
402
|
+
"type": "object",
|
|
403
|
+
"description": "查询参数(用于 GET 请求)",
|
|
404
|
+
},
|
|
405
|
+
"json_data": {
|
|
406
|
+
"type": "object",
|
|
407
|
+
"description": "请求体数据(用于 POST/PUT/PATCH 请求)",
|
|
408
|
+
},
|
|
409
|
+
},
|
|
410
|
+
"required": ["method", "path"],
|
|
411
|
+
},
|
|
412
|
+
},
|
|
285
413
|
{
|
|
286
414
|
"name": "ask_user",
|
|
287
415
|
"description": (
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
"""AskTable OpenAPI schema parser and documentation generator."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import logging
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any, Dict, List, Optional
|
|
7
|
+
|
|
8
|
+
import httpx
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class AskTableAPISchema:
|
|
14
|
+
"""
|
|
15
|
+
Parse and provide AskTable OpenAPI schema documentation.
|
|
16
|
+
|
|
17
|
+
This class fetches the OpenAPI specification from AskTable API
|
|
18
|
+
and generates simplified documentation for LLM consumption.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
def __init__(self, api_base_url: str = "https://api.asktable.com"):
|
|
22
|
+
"""
|
|
23
|
+
Initialize API schema parser.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
api_base_url: AskTable API base URL
|
|
27
|
+
"""
|
|
28
|
+
self.api_base_url = api_base_url
|
|
29
|
+
self.openapi_url = f"{api_base_url}/openapi.json"
|
|
30
|
+
self._spec: Optional[Dict[str, Any]] = None
|
|
31
|
+
self._cache_file = Path("/tmp/asktable_openapi_cache.json")
|
|
32
|
+
|
|
33
|
+
@property
|
|
34
|
+
def spec(self) -> Dict[str, Any]:
|
|
35
|
+
"""Get OpenAPI specification (cached)."""
|
|
36
|
+
if self._spec is None:
|
|
37
|
+
self._spec = self._load_spec()
|
|
38
|
+
return self._spec
|
|
39
|
+
|
|
40
|
+
def _load_spec(self) -> Dict[str, Any]:
|
|
41
|
+
"""Load OpenAPI spec from cache or fetch from API."""
|
|
42
|
+
# Try cache first
|
|
43
|
+
if self._cache_file.exists():
|
|
44
|
+
try:
|
|
45
|
+
with open(self._cache_file, 'r') as f:
|
|
46
|
+
spec = json.load(f)
|
|
47
|
+
logger.info("Loaded OpenAPI spec from cache")
|
|
48
|
+
return spec
|
|
49
|
+
except Exception as e:
|
|
50
|
+
logger.warning(f"Failed to load cache: {e}")
|
|
51
|
+
|
|
52
|
+
# Fetch from API
|
|
53
|
+
try:
|
|
54
|
+
logger.info(f"Fetching OpenAPI spec from {self.openapi_url}")
|
|
55
|
+
response = httpx.get(self.openapi_url, timeout=30.0)
|
|
56
|
+
response.raise_for_status()
|
|
57
|
+
spec = response.json()
|
|
58
|
+
|
|
59
|
+
# Save to cache
|
|
60
|
+
try:
|
|
61
|
+
with open(self._cache_file, 'w') as f:
|
|
62
|
+
json.dump(spec, f)
|
|
63
|
+
logger.info("Saved OpenAPI spec to cache")
|
|
64
|
+
except Exception as e:
|
|
65
|
+
logger.warning(f"Failed to save cache: {e}")
|
|
66
|
+
|
|
67
|
+
return spec
|
|
68
|
+
except Exception as e:
|
|
69
|
+
logger.error(f"Failed to fetch OpenAPI spec: {e}")
|
|
70
|
+
return {"paths": {}}
|
|
71
|
+
|
|
72
|
+
def get_api_categories(self) -> Dict[str, List[str]]:
|
|
73
|
+
"""
|
|
74
|
+
Get API endpoints grouped by category.
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
Dict mapping category name to list of endpoint paths
|
|
78
|
+
"""
|
|
79
|
+
paths = self.spec.get('paths', {})
|
|
80
|
+
categories: Dict[str, List[str]] = {}
|
|
81
|
+
|
|
82
|
+
for path in paths.keys():
|
|
83
|
+
# Extract category from path (e.g., /v1/bots/{id} -> bots)
|
|
84
|
+
parts = path.strip('/').split('/')
|
|
85
|
+
if len(parts) > 1 and parts[0] == 'v1':
|
|
86
|
+
category = parts[1]
|
|
87
|
+
else:
|
|
88
|
+
category = 'other'
|
|
89
|
+
|
|
90
|
+
if category not in categories:
|
|
91
|
+
categories[category] = []
|
|
92
|
+
categories[category].append(path)
|
|
93
|
+
|
|
94
|
+
return categories
|
|
95
|
+
|
|
96
|
+
def get_endpoint_info(self, path: str, method: str) -> Optional[Dict[str, Any]]:
|
|
97
|
+
"""
|
|
98
|
+
Get detailed information about an API endpoint.
|
|
99
|
+
|
|
100
|
+
Args:
|
|
101
|
+
path: API path (e.g., /v1/bots)
|
|
102
|
+
method: HTTP method (e.g., GET, POST)
|
|
103
|
+
|
|
104
|
+
Returns:
|
|
105
|
+
Endpoint information or None if not found
|
|
106
|
+
"""
|
|
107
|
+
paths = self.spec.get('paths', {})
|
|
108
|
+
if path not in paths:
|
|
109
|
+
return None
|
|
110
|
+
|
|
111
|
+
method_lower = method.lower()
|
|
112
|
+
endpoint = paths[path].get(method_lower)
|
|
113
|
+
if not endpoint:
|
|
114
|
+
return None
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
'path': path,
|
|
118
|
+
'method': method.upper(),
|
|
119
|
+
'summary': endpoint.get('summary', ''),
|
|
120
|
+
'description': endpoint.get('description', ''),
|
|
121
|
+
'parameters': endpoint.get('parameters', []),
|
|
122
|
+
'requestBody': endpoint.get('requestBody'),
|
|
123
|
+
'responses': endpoint.get('responses', {}),
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
def generate_api_documentation(self, max_endpoints: int = 50) -> str:
|
|
127
|
+
"""
|
|
128
|
+
Generate simplified API documentation for LLM.
|
|
129
|
+
|
|
130
|
+
Args:
|
|
131
|
+
max_endpoints: Maximum number of endpoints to include
|
|
132
|
+
|
|
133
|
+
Returns:
|
|
134
|
+
Formatted API documentation string
|
|
135
|
+
"""
|
|
136
|
+
categories = self.get_api_categories()
|
|
137
|
+
doc_lines = [
|
|
138
|
+
"# AskTable API 参考文档",
|
|
139
|
+
"",
|
|
140
|
+
f"API 版本: {self.spec.get('info', {}).get('version', 'unknown')}",
|
|
141
|
+
f"Base URL: {self.api_base_url}",
|
|
142
|
+
"",
|
|
143
|
+
"## API 分类",
|
|
144
|
+
"",
|
|
145
|
+
]
|
|
146
|
+
|
|
147
|
+
# Priority categories (most commonly used)
|
|
148
|
+
priority_categories = [
|
|
149
|
+
'bots', 'chats', 'datasources', 'single-turn',
|
|
150
|
+
'canvas', 'reports', 'roles', 'policies'
|
|
151
|
+
]
|
|
152
|
+
|
|
153
|
+
endpoint_count = 0
|
|
154
|
+
for category in priority_categories:
|
|
155
|
+
if category not in categories:
|
|
156
|
+
continue
|
|
157
|
+
if endpoint_count >= max_endpoints:
|
|
158
|
+
break
|
|
159
|
+
|
|
160
|
+
doc_lines.append(f"### {category}")
|
|
161
|
+
doc_lines.append("")
|
|
162
|
+
|
|
163
|
+
paths = self.spec.get('paths', {})
|
|
164
|
+
for path in categories[category][:5]: # Limit per category
|
|
165
|
+
if endpoint_count >= max_endpoints:
|
|
166
|
+
break
|
|
167
|
+
|
|
168
|
+
for method in ['get', 'post', 'put', 'delete', 'patch']:
|
|
169
|
+
if method in paths.get(path, {}):
|
|
170
|
+
endpoint = paths[path][method]
|
|
171
|
+
summary = endpoint.get('summary', '')
|
|
172
|
+
if summary:
|
|
173
|
+
doc_lines.append(f"- `{method.upper()} {path}` - {summary}")
|
|
174
|
+
endpoint_count += 1
|
|
175
|
+
|
|
176
|
+
doc_lines.append("")
|
|
177
|
+
|
|
178
|
+
return "\n".join(doc_lines)
|
|
179
|
+
|
|
180
|
+
def search_endpoints(self, query: str) -> List[Dict[str, Any]]:
|
|
181
|
+
"""
|
|
182
|
+
Search for API endpoints by keyword.
|
|
183
|
+
|
|
184
|
+
Args:
|
|
185
|
+
query: Search query (e.g., "chat", "message", "datasource")
|
|
186
|
+
|
|
187
|
+
Returns:
|
|
188
|
+
List of matching endpoints with their info
|
|
189
|
+
"""
|
|
190
|
+
query_lower = query.lower()
|
|
191
|
+
results = []
|
|
192
|
+
|
|
193
|
+
paths = self.spec.get('paths', {})
|
|
194
|
+
for path, methods in paths.items():
|
|
195
|
+
# Check if query matches path
|
|
196
|
+
if query_lower in path.lower():
|
|
197
|
+
for method, endpoint in methods.items():
|
|
198
|
+
if method.upper() in ['GET', 'POST', 'PUT', 'DELETE', 'PATCH']:
|
|
199
|
+
results.append({
|
|
200
|
+
'path': path,
|
|
201
|
+
'method': method.upper(),
|
|
202
|
+
'summary': endpoint.get('summary', ''),
|
|
203
|
+
'score': 2 if query_lower in path.lower() else 1
|
|
204
|
+
})
|
|
205
|
+
# Check if query matches summary/description
|
|
206
|
+
else:
|
|
207
|
+
for method, endpoint in methods.items():
|
|
208
|
+
if method.upper() in ['GET', 'POST', 'PUT', 'DELETE', 'PATCH']:
|
|
209
|
+
summary = endpoint.get('summary', '').lower()
|
|
210
|
+
description = endpoint.get('description', '').lower()
|
|
211
|
+
if query_lower in summary or query_lower in description:
|
|
212
|
+
results.append({
|
|
213
|
+
'path': path,
|
|
214
|
+
'method': method.upper(),
|
|
215
|
+
'summary': endpoint.get('summary', ''),
|
|
216
|
+
'score': 1
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
# Sort by relevance score
|
|
220
|
+
results.sort(key=lambda x: x['score'], reverse=True)
|
|
221
|
+
return results[:10] # Return top 10
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def get_api_schema(api_base_url: str = "https://api.asktable.com") -> AskTableAPISchema:
|
|
225
|
+
"""Get or create AskTable API schema instance."""
|
|
226
|
+
return AskTableAPISchema(api_base_url)
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import logging
|
|
4
4
|
from typing import Any, Dict, List, Optional
|
|
5
5
|
|
|
6
|
+
import httpx
|
|
6
7
|
from asktable import Asktable
|
|
7
8
|
|
|
8
9
|
logger = logging.getLogger(__name__)
|
|
@@ -269,3 +270,166 @@ class AskTableClient:
|
|
|
269
270
|
except Exception as e:
|
|
270
271
|
logger.error(f"Failed to create chat: {e}")
|
|
271
272
|
raise
|
|
273
|
+
|
|
274
|
+
# Question & Answer operations
|
|
275
|
+
def ask_datasource(
|
|
276
|
+
self,
|
|
277
|
+
datasource_id: str,
|
|
278
|
+
question: str,
|
|
279
|
+
max_rows: Optional[int] = None,
|
|
280
|
+
) -> Dict[str, Any]:
|
|
281
|
+
"""
|
|
282
|
+
Ask a question directly to a datasource.
|
|
283
|
+
|
|
284
|
+
Args:
|
|
285
|
+
datasource_id: Target datasource ID
|
|
286
|
+
question: Natural language question
|
|
287
|
+
max_rows: Maximum rows to return (default: unlimited)
|
|
288
|
+
|
|
289
|
+
Returns:
|
|
290
|
+
Answer response with query results
|
|
291
|
+
"""
|
|
292
|
+
try:
|
|
293
|
+
params = {
|
|
294
|
+
"datasource_id": datasource_id,
|
|
295
|
+
"question": question,
|
|
296
|
+
}
|
|
297
|
+
if max_rows is not None:
|
|
298
|
+
params["max_rows"] = max_rows
|
|
299
|
+
|
|
300
|
+
answer = self.client.answers.create(**params)
|
|
301
|
+
|
|
302
|
+
logger.info(f"Asked datasource {datasource_id}: {question[:50]}...")
|
|
303
|
+
|
|
304
|
+
if hasattr(answer, 'model_dump'):
|
|
305
|
+
return answer.model_dump()
|
|
306
|
+
elif hasattr(answer, 'dict'):
|
|
307
|
+
return answer.dict()
|
|
308
|
+
return answer
|
|
309
|
+
except Exception as e:
|
|
310
|
+
logger.error(f"Failed to ask datasource: {e}")
|
|
311
|
+
raise
|
|
312
|
+
|
|
313
|
+
def ask_bot(
|
|
314
|
+
self,
|
|
315
|
+
bot_id: str,
|
|
316
|
+
question: str,
|
|
317
|
+
chat_id: Optional[str] = None,
|
|
318
|
+
) -> Dict[str, Any]:
|
|
319
|
+
"""
|
|
320
|
+
Ask a question to a bot via chat.
|
|
321
|
+
|
|
322
|
+
Args:
|
|
323
|
+
bot_id: Target bot ID
|
|
324
|
+
question: Natural language question
|
|
325
|
+
chat_id: Existing chat ID (if None, creates a new chat)
|
|
326
|
+
|
|
327
|
+
Returns:
|
|
328
|
+
Message response with bot's answer
|
|
329
|
+
"""
|
|
330
|
+
try:
|
|
331
|
+
# Create chat if not provided
|
|
332
|
+
if not chat_id:
|
|
333
|
+
chat = self.client.chats.create(
|
|
334
|
+
bot_id=bot_id,
|
|
335
|
+
name=f"Query: {question[:30]}...",
|
|
336
|
+
)
|
|
337
|
+
if hasattr(chat, 'id'):
|
|
338
|
+
chat_id = chat.id
|
|
339
|
+
elif isinstance(chat, dict):
|
|
340
|
+
chat_id = chat.get('id')
|
|
341
|
+
else:
|
|
342
|
+
raise ValueError("Failed to get chat ID")
|
|
343
|
+
|
|
344
|
+
# Send message to chat
|
|
345
|
+
message = self.client.chats.messages.create(
|
|
346
|
+
chat_id=chat_id,
|
|
347
|
+
question=question,
|
|
348
|
+
)
|
|
349
|
+
|
|
350
|
+
logger.info(f"Asked bot {bot_id}: {question[:50]}...")
|
|
351
|
+
|
|
352
|
+
if hasattr(message, 'model_dump'):
|
|
353
|
+
return message.model_dump()
|
|
354
|
+
elif hasattr(message, 'dict'):
|
|
355
|
+
return message.dict()
|
|
356
|
+
return message
|
|
357
|
+
except Exception as e:
|
|
358
|
+
logger.error(f"Failed to ask bot: {e}")
|
|
359
|
+
raise
|
|
360
|
+
|
|
361
|
+
# Generic API operations
|
|
362
|
+
def call_api(
|
|
363
|
+
self,
|
|
364
|
+
method: str,
|
|
365
|
+
path: str,
|
|
366
|
+
params: Optional[Dict[str, Any]] = None,
|
|
367
|
+
json_data: Optional[Dict[str, Any]] = None,
|
|
368
|
+
) -> Dict[str, Any]:
|
|
369
|
+
"""
|
|
370
|
+
Call any AskTable API endpoint dynamically.
|
|
371
|
+
|
|
372
|
+
Args:
|
|
373
|
+
method: HTTP method (GET, POST, PUT, DELETE, PATCH)
|
|
374
|
+
path: API path (e.g., /v1/bots)
|
|
375
|
+
params: Query parameters
|
|
376
|
+
json_data: Request body (for POST/PUT/PATCH)
|
|
377
|
+
|
|
378
|
+
Returns:
|
|
379
|
+
API response as dict
|
|
380
|
+
|
|
381
|
+
Example:
|
|
382
|
+
# List all canvas
|
|
383
|
+
response = client.call_api('GET', '/v1/canvas')
|
|
384
|
+
|
|
385
|
+
# Create a report
|
|
386
|
+
response = client.call_api(
|
|
387
|
+
'POST',
|
|
388
|
+
'/v1/reports',
|
|
389
|
+
json_data={'name': 'My Report', 'bot_id': 'bot_xxx'}
|
|
390
|
+
)
|
|
391
|
+
"""
|
|
392
|
+
try:
|
|
393
|
+
# Build full URL
|
|
394
|
+
if path.startswith('/'):
|
|
395
|
+
url = f"{self.base_url}{path}"
|
|
396
|
+
else:
|
|
397
|
+
url = f"{self.base_url}/{path}"
|
|
398
|
+
|
|
399
|
+
# Prepare headers
|
|
400
|
+
headers = {
|
|
401
|
+
"Authorization": f"Bearer {self.api_key}",
|
|
402
|
+
"Content-Type": "application/json",
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
# Make request
|
|
406
|
+
method_upper = method.upper()
|
|
407
|
+
logger.info(f"Calling API: {method_upper} {path}")
|
|
408
|
+
|
|
409
|
+
with httpx.Client(timeout=60.0) as http_client:
|
|
410
|
+
response = http_client.request(
|
|
411
|
+
method=method_upper,
|
|
412
|
+
url=url,
|
|
413
|
+
params=params,
|
|
414
|
+
json=json_data,
|
|
415
|
+
headers=headers,
|
|
416
|
+
)
|
|
417
|
+
response.raise_for_status()
|
|
418
|
+
|
|
419
|
+
# Try to parse JSON response
|
|
420
|
+
try:
|
|
421
|
+
result = response.json()
|
|
422
|
+
except Exception:
|
|
423
|
+
result = {"status": "success", "text": response.text}
|
|
424
|
+
|
|
425
|
+
logger.info(f"API call successful: {method_upper} {path}")
|
|
426
|
+
return result
|
|
427
|
+
|
|
428
|
+
except httpx.HTTPStatusError as e:
|
|
429
|
+
error_msg = f"HTTP {e.response.status_code}: {e.response.text}"
|
|
430
|
+
logger.error(f"API call failed: {error_msg}")
|
|
431
|
+
raise Exception(error_msg)
|
|
432
|
+
except Exception as e:
|
|
433
|
+
logger.error(f"API call failed: {e}")
|
|
434
|
+
raise
|
|
435
|
+
|
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
asktable_advisor/__init__.py,sha256=21OEclDkj7fT8swRC2d7JZAqB2cB52lt-LLJno5eVFY,442
|
|
2
2
|
asktable_advisor/__main__.py,sha256=7hZeayoibCKUeGIZSgU8Pg9N-OejQJ35kq8A-yRBFT8,4637
|
|
3
|
-
asktable_advisor/__version__.py,sha256=
|
|
3
|
+
asktable_advisor/__version__.py,sha256=jTdn_ZXtpnulTaIS6B6Tb2WjaZlXCW8oChDoK-o5-h0,136
|
|
4
4
|
asktable_advisor/config.py,sha256=l2vPCaUIPUeaJt-2NhlOFIUB9pVWOx7uD58qgAOO1Bk,2667
|
|
5
5
|
asktable_advisor/agent/__init__.py,sha256=QU-dutlZwnjeJ1EMtdjTLeia3SloykUAatiuHuEeHq4,147
|
|
6
|
-
asktable_advisor/agent/advisor.py,sha256=
|
|
6
|
+
asktable_advisor/agent/advisor.py,sha256=5sppaVs3sMEQFKkRz21IZAY7-4gvjbRSZtH2FePNQso,14263
|
|
7
7
|
asktable_advisor/agent/llm_client.py,sha256=f8nVteCXiXDF0TWlYVKEhpUXuvAskuK_qbc-ipLTZR4,5780
|
|
8
|
-
asktable_advisor/agent/prompts.py,sha256=
|
|
9
|
-
asktable_advisor/agent/tools.py,sha256=
|
|
8
|
+
asktable_advisor/agent/prompts.py,sha256=sblicv1G4wcadxzU6lxqU1YM27BVPwmZY8zTmtlexhk,8625
|
|
9
|
+
asktable_advisor/agent/tools.py,sha256=sqgWHQDaKPbmWWOpXBNPnDLMMqmTEuqmk1fURhQHM1Q,18035
|
|
10
10
|
asktable_advisor/asktable/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
11
|
-
asktable_advisor/asktable/
|
|
11
|
+
asktable_advisor/asktable/api_schema.py,sha256=SDnNjn3j5iWzgIyR3sphmIPO7uorem084tac6ety12Q,7812
|
|
12
|
+
asktable_advisor/asktable/client.py,sha256=acpwdhbd27DY727BJ-UA6H7HBWiguI-Vhaid5C3xXus,13773
|
|
12
13
|
asktable_advisor/asktable/inspector.py,sha256=m07duioYJ6xxOJq8F5bIfsMEjpIlHJywBfJoN4ElxEI,6409
|
|
13
14
|
asktable_advisor/asktable/resources/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
14
15
|
asktable_advisor/conversation/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
@@ -18,9 +19,9 @@ asktable_advisor/database/manager.py,sha256=0dMh7s7Xs-9vpC92VgIpDJSIiu_YxJApvnmi
|
|
|
18
19
|
asktable_advisor/database/schema_generator.py,sha256=_K8FlJ3FMtATmxzYO-4Fogace5Zsvh2p8dcGBBNkjPw,4535
|
|
19
20
|
asktable_advisor/knowledge/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
20
21
|
asktable_advisor/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
21
|
-
asktable_advisor-1.0.
|
|
22
|
-
asktable_advisor-1.0.
|
|
23
|
-
asktable_advisor-1.0.
|
|
24
|
-
asktable_advisor-1.0.
|
|
25
|
-
asktable_advisor-1.0.
|
|
26
|
-
asktable_advisor-1.0.
|
|
22
|
+
asktable_advisor-1.0.4.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
|
|
23
|
+
asktable_advisor-1.0.4.dist-info/METADATA,sha256=UWF2rWbmXjbOa8gE68K_LI0XNp_EmbfC7oNFTfb1iKo,7443
|
|
24
|
+
asktable_advisor-1.0.4.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
25
|
+
asktable_advisor-1.0.4.dist-info/entry_points.txt,sha256=m4MJ7R-05wjqqPf-awcMj9JqtwgiK2Du8UV_l6VNh6k,68
|
|
26
|
+
asktable_advisor-1.0.4.dist-info/top_level.txt,sha256=cDBvLmuBSm43p7q7GEoZoIsB0q2al1NZbbDktRe42bQ,17
|
|
27
|
+
asktable_advisor-1.0.4.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|