asktable-advisor 1.0.2__tar.gz → 1.0.4__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 (37) hide show
  1. {asktable_advisor-1.0.2 → asktable_advisor-1.0.4}/.env.example +5 -0
  2. {asktable_advisor-1.0.2 → asktable_advisor-1.0.4}/PKG-INFO +1 -1
  3. {asktable_advisor-1.0.2 → asktable_advisor-1.0.4}/asktable_advisor/__main__.py +26 -13
  4. {asktable_advisor-1.0.2 → asktable_advisor-1.0.4}/asktable_advisor/__version__.py +1 -1
  5. {asktable_advisor-1.0.2 → asktable_advisor-1.0.4}/asktable_advisor/agent/advisor.py +93 -0
  6. {asktable_advisor-1.0.2 → asktable_advisor-1.0.4}/asktable_advisor/agent/prompts.py +84 -0
  7. {asktable_advisor-1.0.2 → asktable_advisor-1.0.4}/asktable_advisor/agent/tools.py +128 -0
  8. asktable_advisor-1.0.4/asktable_advisor/asktable/api_schema.py +226 -0
  9. {asktable_advisor-1.0.2 → asktable_advisor-1.0.4}/asktable_advisor/asktable/client.py +164 -0
  10. {asktable_advisor-1.0.2 → asktable_advisor-1.0.4}/asktable_advisor/config.py +6 -0
  11. {asktable_advisor-1.0.2 → asktable_advisor-1.0.4}/asktable_advisor.egg-info/PKG-INFO +1 -1
  12. {asktable_advisor-1.0.2 → asktable_advisor-1.0.4}/asktable_advisor.egg-info/SOURCES.txt +1 -0
  13. {asktable_advisor-1.0.2 → asktable_advisor-1.0.4}/pyproject.toml +1 -1
  14. {asktable_advisor-1.0.2 → asktable_advisor-1.0.4}/CLAUDE.md +0 -0
  15. {asktable_advisor-1.0.2 → asktable_advisor-1.0.4}/LICENSE +0 -0
  16. {asktable_advisor-1.0.2 → asktable_advisor-1.0.4}/MANIFEST.in +0 -0
  17. {asktable_advisor-1.0.2 → asktable_advisor-1.0.4}/README.md +0 -0
  18. {asktable_advisor-1.0.2 → asktable_advisor-1.0.4}/asktable_advisor/__init__.py +0 -0
  19. {asktable_advisor-1.0.2 → asktable_advisor-1.0.4}/asktable_advisor/agent/__init__.py +0 -0
  20. {asktable_advisor-1.0.2 → asktable_advisor-1.0.4}/asktable_advisor/agent/llm_client.py +0 -0
  21. {asktable_advisor-1.0.2 → asktable_advisor-1.0.4}/asktable_advisor/asktable/__init__.py +0 -0
  22. {asktable_advisor-1.0.2 → asktable_advisor-1.0.4}/asktable_advisor/asktable/inspector.py +0 -0
  23. {asktable_advisor-1.0.2 → asktable_advisor-1.0.4}/asktable_advisor/asktable/resources/__init__.py +0 -0
  24. {asktable_advisor-1.0.2 → asktable_advisor-1.0.4}/asktable_advisor/conversation/__init__.py +0 -0
  25. {asktable_advisor-1.0.2 → asktable_advisor-1.0.4}/asktable_advisor/database/__init__.py +0 -0
  26. {asktable_advisor-1.0.2 → asktable_advisor-1.0.4}/asktable_advisor/database/data_generator.py +0 -0
  27. {asktable_advisor-1.0.2 → asktable_advisor-1.0.4}/asktable_advisor/database/manager.py +0 -0
  28. {asktable_advisor-1.0.2 → asktable_advisor-1.0.4}/asktable_advisor/database/schema_generator.py +0 -0
  29. {asktable_advisor-1.0.2 → asktable_advisor-1.0.4}/asktable_advisor/knowledge/__init__.py +0 -0
  30. {asktable_advisor-1.0.2 → asktable_advisor-1.0.4}/asktable_advisor/utils/__init__.py +0 -0
  31. {asktable_advisor-1.0.2 → asktable_advisor-1.0.4}/asktable_advisor.egg-info/dependency_links.txt +0 -0
  32. {asktable_advisor-1.0.2 → asktable_advisor-1.0.4}/asktable_advisor.egg-info/entry_points.txt +0 -0
  33. {asktable_advisor-1.0.2 → asktable_advisor-1.0.4}/asktable_advisor.egg-info/requires.txt +0 -0
  34. {asktable_advisor-1.0.2 → asktable_advisor-1.0.4}/asktable_advisor.egg-info/top_level.txt +0 -0
  35. {asktable_advisor-1.0.2 → asktable_advisor-1.0.4}/setup.cfg +0 -0
  36. {asktable_advisor-1.0.2 → asktable_advisor-1.0.4}/tests/test_config.py +0 -0
  37. {asktable_advisor-1.0.2 → asktable_advisor-1.0.4}/tests/test_scenarios.py +0 -0
@@ -17,6 +17,11 @@ AGENT_MODEL=qwen3-max
17
17
  AGENT_TEMPERATURE=0.7
18
18
  AGENT_MAX_TOKENS=8192 # Maximum: 65536 for qwen3-max
19
19
 
20
+ # Logging Configuration
21
+ # Options: DEBUG, INFO, WARNING, ERROR, CRITICAL
22
+ # Use WARNING or ERROR to reduce log output in production
23
+ LOG_LEVEL=WARNING
24
+
20
25
  # MySQL Database Configuration
21
26
  MYSQL_HOST=
22
27
  MYSQL_PORT=
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: asktable-advisor
3
- Version: 1.0.2
3
+ Version: 1.0.4
4
4
  Summary: Conversational AI Agent for building AskTable demo scenarios
5
5
  Author-email: DataMini <contact@asktable.com>
6
6
  Maintainer-email: DataMini <contact@asktable.com>
@@ -1,29 +1,39 @@
1
1
  """Command-line interface for AskTable Advisor."""
2
2
 
3
- import sys
4
3
  import logging
5
- from pathlib import Path
4
+ import sys
6
5
 
7
6
  from rich.console import Console
8
- from rich.panel import Panel
9
7
  from rich.markdown import Markdown
8
+ from rich.panel import Panel
10
9
  from rich.prompt import Prompt
11
10
 
12
- from .config import AdvisorSettings, get_settings
13
11
  from .agent.advisor import AdvisorAgent
12
+ from .config import get_settings
14
13
 
15
14
  # Setup console
16
15
  console = Console()
17
16
 
18
- # Setup logging
19
- logging.basicConfig(
20
- level=logging.INFO,
21
- format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
22
- handlers=[
23
- logging.FileHandler("asktable_advisor.log"),
24
- logging.StreamHandler(sys.stderr),
25
- ],
26
- )
17
+
18
+ def setup_logging(log_level: str = "WARNING") -> None:
19
+ """
20
+ Setup logging configuration.
21
+
22
+ Args:
23
+ log_level: Logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL)
24
+ """
25
+ # Convert string to logging level
26
+ numeric_level = getattr(logging, log_level.upper(), logging.WARNING)
27
+
28
+ logging.basicConfig(
29
+ level=numeric_level,
30
+ format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
31
+ handlers=[
32
+ logging.FileHandler("asktable_advisor.log"),
33
+ logging.StreamHandler(sys.stderr),
34
+ ],
35
+ force=True, # Override any existing configuration
36
+ )
27
37
 
28
38
 
29
39
  def print_welcome() -> None:
@@ -128,6 +138,9 @@ def main() -> None:
128
138
  # Load settings
129
139
  settings = get_settings()
130
140
 
141
+ # Setup logging with configured level
142
+ setup_logging(settings.log_level)
143
+
131
144
  # Print welcome
132
145
  print_welcome()
133
146
 
@@ -1,4 +1,4 @@
1
1
  """Version information for asktable-advisor."""
2
2
 
3
- __version__ = "1.0.2"
3
+ __version__ = "1.0.4"
4
4
  __version_info__ = tuple(int(x) for x in __version__.split("."))
@@ -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. **主动性**:不要被动等待,主动询问必要的信息
@@ -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
+
@@ -45,6 +45,12 @@ class AdvisorSettings(BaseSettings):
45
45
  description="Maximum tokens for LLM responses",
46
46
  )
47
47
 
48
+ # Logging Configuration
49
+ log_level: str = Field(
50
+ default="WARNING",
51
+ description="Logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL)",
52
+ )
53
+
48
54
  # MySQL Database Configuration
49
55
  mysql_host: str = Field(..., description="MySQL host")
50
56
  mysql_port: int = Field(default=3306, description="MySQL port")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: asktable-advisor
3
- Version: 1.0.2
3
+ Version: 1.0.4
4
4
  Summary: Conversational AI Agent for building AskTable demo scenarios
5
5
  Author-email: DataMini <contact@asktable.com>
6
6
  Maintainer-email: DataMini <contact@asktable.com>
@@ -20,6 +20,7 @@ asktable_advisor/agent/llm_client.py
20
20
  asktable_advisor/agent/prompts.py
21
21
  asktable_advisor/agent/tools.py
22
22
  asktable_advisor/asktable/__init__.py
23
+ asktable_advisor/asktable/api_schema.py
23
24
  asktable_advisor/asktable/client.py
24
25
  asktable_advisor/asktable/inspector.py
25
26
  asktable_advisor/asktable/resources/__init__.py
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "asktable-advisor"
3
- version = "1.0.2"
3
+ version = "1.0.4"
4
4
  description = "Conversational AI Agent for building AskTable demo scenarios"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"