asktable-advisor 1.0.1__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.
@@ -0,0 +1,271 @@
1
+ """AskTable API client for Advisor Agent."""
2
+
3
+ import logging
4
+ from typing import Any, Dict, List, Optional
5
+
6
+ from asktable import Asktable
7
+
8
+ logger = logging.getLogger(__name__)
9
+
10
+
11
+ class AskTableClient:
12
+ """
13
+ AskTable API client wrapper.
14
+
15
+ Provides a clean interface to interact with AskTable resources.
16
+ """
17
+
18
+ def __init__(self, api_key: str, base_url: str):
19
+ """
20
+ Initialize AskTable client.
21
+
22
+ Args:
23
+ api_key: AskTable API key
24
+ base_url: AskTable API base URL
25
+ """
26
+ self.api_key = api_key
27
+ self.base_url = base_url
28
+ self._client: Optional[Asktable] = None
29
+
30
+ logger.info(f"AskTable client initialized: {base_url}")
31
+
32
+ @property
33
+ def client(self) -> Asktable:
34
+ """Get or create AskTable SDK client."""
35
+ if self._client is None:
36
+ self._client = Asktable(
37
+ base_url=self.base_url,
38
+ api_key=self.api_key,
39
+ )
40
+ return self._client
41
+
42
+ def test_connection(self) -> bool:
43
+ """
44
+ Test AskTable API connection.
45
+
46
+ Returns:
47
+ True if connection successful, False otherwise
48
+ """
49
+ try:
50
+ self.client.datasources.list()
51
+ logger.info("AskTable API connection successful")
52
+ return True
53
+ except Exception as e:
54
+ logger.error(f"AskTable API connection failed: {e}")
55
+ return False
56
+
57
+ # Datasource operations
58
+ def list_datasources(self) -> List[Dict[str, Any]]:
59
+ """List all datasources in the project."""
60
+ try:
61
+ result = self.client.datasources.list()
62
+ datasources = result.items if hasattr(result, 'items') else result
63
+
64
+ ds_list = []
65
+ for ds in datasources:
66
+ if hasattr(ds, 'model_dump'):
67
+ ds_list.append(ds.model_dump())
68
+ elif hasattr(ds, 'dict'):
69
+ ds_list.append(ds.dict())
70
+ else:
71
+ ds_list.append(ds)
72
+ return ds_list
73
+ except Exception as e:
74
+ logger.error(f"Failed to list datasources: {e}")
75
+ return []
76
+
77
+ def get_datasource(self, datasource_id: str) -> Optional[Dict[str, Any]]:
78
+ """Get datasource by ID."""
79
+ try:
80
+ datasource = self.client.datasources.retrieve(datasource_id)
81
+ if hasattr(datasource, 'model_dump'):
82
+ return datasource.model_dump()
83
+ elif hasattr(datasource, 'dict'):
84
+ return datasource.dict()
85
+ return datasource
86
+ except Exception as e:
87
+ logger.warning(f"Datasource not found: {e}")
88
+ return None
89
+
90
+ def create_datasource(
91
+ self,
92
+ name: str,
93
+ database_name: str,
94
+ description: Optional[str] = None,
95
+ host: Optional[str] = None,
96
+ port: Optional[int] = None,
97
+ username: Optional[str] = None,
98
+ password: Optional[str] = None,
99
+ ) -> Dict[str, Any]:
100
+ """Create a new datasource."""
101
+ try:
102
+ # Use provided connection info or fall back to env defaults
103
+ from ..config import get_settings
104
+ settings = get_settings()
105
+
106
+ datasource = self.client.datasources.create(
107
+ name=name,
108
+ engine="mysql",
109
+ access_config={
110
+ "host": host or settings.mysql_host,
111
+ "port": port or settings.mysql_port,
112
+ "db": database_name,
113
+ "user": username or settings.mysql_user,
114
+ "password": password or settings.mysql_password,
115
+ },
116
+ )
117
+
118
+ logger.info(f"Created datasource: {name}")
119
+
120
+ if hasattr(datasource, 'model_dump'):
121
+ return datasource.model_dump()
122
+ elif hasattr(datasource, 'dict'):
123
+ return datasource.dict()
124
+ return datasource
125
+ except Exception as e:
126
+ logger.error(f"Failed to create datasource: {e}")
127
+ raise
128
+
129
+ def delete_datasource(self, datasource_id: str) -> bool:
130
+ """Delete a datasource."""
131
+ try:
132
+ self.client.datasources.delete(datasource_id)
133
+ logger.info(f"Deleted datasource: {datasource_id}")
134
+ return True
135
+ except Exception as e:
136
+ logger.error(f"Failed to delete datasource: {e}")
137
+ return False
138
+
139
+ # Bot operations
140
+ def list_bots(self) -> List[Dict[str, Any]]:
141
+ """List all bots in the project."""
142
+ try:
143
+ result = self.client.bots.list()
144
+ bots = result.items if hasattr(result, 'items') else result
145
+
146
+ bot_list = []
147
+ for bot in bots:
148
+ if hasattr(bot, 'model_dump'):
149
+ bot_list.append(bot.model_dump())
150
+ elif hasattr(bot, 'dict'):
151
+ bot_list.append(bot.dict())
152
+ else:
153
+ bot_list.append(bot)
154
+ return bot_list
155
+ except Exception as e:
156
+ logger.error(f"Failed to list bots: {e}")
157
+ return []
158
+
159
+ def get_bot(self, bot_id: str) -> Optional[Dict[str, Any]]:
160
+ """Get bot by ID."""
161
+ try:
162
+ bot = self.client.bots.retrieve(bot_id)
163
+ if hasattr(bot, 'model_dump'):
164
+ return bot.model_dump()
165
+ elif hasattr(bot, 'dict'):
166
+ return bot.dict()
167
+ return bot
168
+ except Exception as e:
169
+ logger.warning(f"Bot not found: {e}")
170
+ return None
171
+
172
+ def create_bot(
173
+ self,
174
+ name: str,
175
+ datasource_id: str,
176
+ description: Optional[str] = None,
177
+ instructions: Optional[str] = None,
178
+ sample_questions: Optional[List[str]] = None,
179
+ ) -> Dict[str, Any]:
180
+ """Create a new bot."""
181
+ try:
182
+ bot = self.client.bots.create(
183
+ name=name,
184
+ datasource_ids=[datasource_id],
185
+ welcome_message=description,
186
+ magic_input=instructions,
187
+ sample_questions=sample_questions or [],
188
+ )
189
+
190
+ logger.info(f"Created bot: {name}")
191
+
192
+ if hasattr(bot, 'model_dump'):
193
+ return bot.model_dump()
194
+ elif hasattr(bot, 'dict'):
195
+ return bot.dict()
196
+ return bot
197
+ except Exception as e:
198
+ logger.error(f"Failed to create bot: {e}")
199
+ raise
200
+
201
+ def update_bot(
202
+ self,
203
+ bot_id: str,
204
+ name: Optional[str] = None,
205
+ instructions: Optional[str] = None,
206
+ sample_questions: Optional[List[str]] = None,
207
+ ) -> Dict[str, Any]:
208
+ """Update an existing bot."""
209
+ try:
210
+ update_data = {}
211
+ if name:
212
+ update_data["name"] = name
213
+ if instructions:
214
+ update_data["magic_input"] = instructions
215
+ if sample_questions:
216
+ update_data["sample_questions"] = sample_questions
217
+
218
+ bot = self.client.bots.update(bot_id, **update_data)
219
+ logger.info(f"Updated bot: {bot_id}")
220
+
221
+ if hasattr(bot, 'model_dump'):
222
+ return bot.model_dump()
223
+ elif hasattr(bot, 'dict'):
224
+ return bot.dict()
225
+ return bot
226
+ except Exception as e:
227
+ logger.error(f"Failed to update bot: {e}")
228
+ raise
229
+
230
+ # Chat operations
231
+ def list_chats(self) -> List[Dict[str, Any]]:
232
+ """List all chats in the project."""
233
+ try:
234
+ result = self.client.chats.list()
235
+ chats = result.items if hasattr(result, 'items') else result
236
+
237
+ chat_list = []
238
+ for chat in chats:
239
+ if hasattr(chat, 'model_dump'):
240
+ chat_list.append(chat.model_dump())
241
+ elif hasattr(chat, 'dict'):
242
+ chat_list.append(chat.dict())
243
+ else:
244
+ chat_list.append(chat)
245
+ return chat_list
246
+ except Exception as e:
247
+ logger.error(f"Failed to list chats: {e}")
248
+ return []
249
+
250
+ def create_chat(
251
+ self,
252
+ bot_id: str,
253
+ name: str,
254
+ ) -> Dict[str, Any]:
255
+ """Create a new chat session."""
256
+ try:
257
+ chat = self.client.chats.create(
258
+ bot_id=bot_id,
259
+ name=name,
260
+ )
261
+
262
+ logger.info(f"Created chat: {name}")
263
+
264
+ if hasattr(chat, 'model_dump'):
265
+ return chat.model_dump()
266
+ elif hasattr(chat, 'dict'):
267
+ return chat.dict()
268
+ return chat
269
+ except Exception as e:
270
+ logger.error(f"Failed to create chat: {e}")
271
+ raise
@@ -0,0 +1,210 @@
1
+ """AskTable project inspector for analyzing existing resources."""
2
+
3
+ import logging
4
+ from typing import Any, Dict, List
5
+
6
+ from .client import AskTableClient
7
+
8
+ logger = logging.getLogger(__name__)
9
+
10
+
11
+ class AskTableInspector:
12
+ """
13
+ Inspector for analyzing AskTable projects.
14
+
15
+ Reads and organizes information about existing datasources, bots, chats, etc.
16
+ """
17
+
18
+ def __init__(self, client: AskTableClient):
19
+ """
20
+ Initialize inspector.
21
+
22
+ Args:
23
+ client: AskTable client instance
24
+ """
25
+ self.client = client
26
+
27
+ def inspect_project(self, detailed: bool = False) -> Dict[str, Any]:
28
+ """
29
+ Inspect the entire AskTable project.
30
+
31
+ Args:
32
+ detailed: If True, include full configuration for each resource
33
+
34
+ Returns:
35
+ Dict containing organized project information
36
+ """
37
+ logger.info("Inspecting AskTable project...")
38
+
39
+ project_info = {
40
+ "datasources": self._inspect_datasources(detailed),
41
+ "bots": self._inspect_bots(detailed),
42
+ "chats": self._inspect_chats(detailed),
43
+ "summary": {},
44
+ }
45
+
46
+ # Add summary statistics
47
+ project_info["summary"] = {
48
+ "total_datasources": len(project_info["datasources"]),
49
+ "total_bots": len(project_info["bots"]),
50
+ "total_chats": len(project_info["chats"]),
51
+ }
52
+
53
+ logger.info(
54
+ f"Project inspection complete: "
55
+ f"{project_info['summary']['total_datasources']} datasources, "
56
+ f"{project_info['summary']['total_bots']} bots, "
57
+ f"{project_info['summary']['total_chats']} chats"
58
+ )
59
+
60
+ return project_info
61
+
62
+ def _inspect_datasources(self, detailed: bool) -> List[Dict[str, Any]]:
63
+ """Inspect all datasources."""
64
+ datasources = self.client.list_datasources()
65
+
66
+ if not detailed:
67
+ # Return simplified view
68
+ return [
69
+ {
70
+ "id": ds.get("id"),
71
+ "name": ds.get("name"),
72
+ "engine": ds.get("engine"),
73
+ "database": ds.get("access_config", {}).get("db"),
74
+ }
75
+ for ds in datasources
76
+ ]
77
+
78
+ return datasources
79
+
80
+ def _inspect_bots(self, detailed: bool) -> List[Dict[str, Any]]:
81
+ """Inspect all bots."""
82
+ bots = self.client.list_bots()
83
+
84
+ if not detailed:
85
+ # Return simplified view
86
+ return [
87
+ {
88
+ "id": bot.get("id"),
89
+ "name": bot.get("name"),
90
+ "datasource_ids": bot.get("datasource_ids", []),
91
+ "sample_question_count": len(bot.get("sample_questions", [])),
92
+ }
93
+ for bot in bots
94
+ ]
95
+
96
+ return bots
97
+
98
+ def _inspect_chats(self, detailed: bool) -> List[Dict[str, Any]]:
99
+ """Inspect all chats."""
100
+ chats = self.client.list_chats()
101
+
102
+ if not detailed:
103
+ # Return simplified view
104
+ return [
105
+ {
106
+ "id": chat.get("id"),
107
+ "name": chat.get("name"),
108
+ "bot_id": chat.get("bot_id"),
109
+ }
110
+ for chat in chats
111
+ ]
112
+
113
+ return chats
114
+
115
+ def identify_scenarios(self) -> List[Dict[str, Any]]:
116
+ """
117
+ Identify distinct scenarios in the project.
118
+
119
+ A scenario is typically a datasource + associated bots + chats.
120
+
121
+ Returns:
122
+ List of identified scenarios
123
+ """
124
+ datasources = self.client.list_datasources()
125
+ bots = self.client.list_bots()
126
+ chats = self.client.list_chats()
127
+
128
+ scenarios = []
129
+
130
+ for ds in datasources:
131
+ ds_id = ds.get("id")
132
+
133
+ # Find bots using this datasource
134
+ related_bots = [
135
+ bot for bot in bots
136
+ if ds_id in bot.get("datasource_ids", [])
137
+ ]
138
+
139
+ # Find chats for these bots
140
+ related_chats = []
141
+ for bot in related_bots:
142
+ bot_id = bot.get("id")
143
+ bot_chats = [
144
+ chat for chat in chats
145
+ if chat.get("bot_id") == bot_id
146
+ ]
147
+ related_chats.extend(bot_chats)
148
+
149
+ scenario = {
150
+ "datasource": {
151
+ "id": ds.get("id"),
152
+ "name": ds.get("name"),
153
+ "engine": ds.get("engine"),
154
+ },
155
+ "bots": [
156
+ {
157
+ "id": bot.get("id"),
158
+ "name": bot.get("name"),
159
+ "sample_question_count": len(bot.get("sample_questions", [])),
160
+ }
161
+ for bot in related_bots
162
+ ],
163
+ "chats": [
164
+ {
165
+ "id": chat.get("id"),
166
+ "name": chat.get("name"),
167
+ }
168
+ for chat in related_chats
169
+ ],
170
+ }
171
+
172
+ scenarios.append(scenario)
173
+
174
+ logger.info(f"Identified {len(scenarios)} scenarios")
175
+ return scenarios
176
+
177
+ def get_optimization_suggestions(self) -> List[str]:
178
+ """
179
+ Analyze project and provide optimization suggestions.
180
+
181
+ Returns:
182
+ List of suggestion strings
183
+ """
184
+ suggestions = []
185
+
186
+ datasources = self.client.list_datasources()
187
+ bots = self.client.list_bots()
188
+
189
+ # Check for datasources without bots
190
+ for ds in datasources:
191
+ ds_id = ds.get("id")
192
+ has_bots = any(
193
+ ds_id in bot.get("datasource_ids", [])
194
+ for bot in bots
195
+ )
196
+ if not has_bots:
197
+ suggestions.append(
198
+ f"数据源 '{ds.get('name')}' 没有关联的 Bot,建议创建一个 Bot 来使用这个数据源"
199
+ )
200
+
201
+ # Check for bots with few sample questions
202
+ for bot in bots:
203
+ question_count = len(bot.get("sample_questions", []))
204
+ if question_count < 10:
205
+ suggestions.append(
206
+ f"Bot '{bot.get('name')}' 只有 {question_count} 个示例问题,"
207
+ f"建议添加更多(推荐 15-25 个)"
208
+ )
209
+
210
+ return suggestions
File without changes
@@ -0,0 +1,79 @@
1
+ """Configuration management for AskTable Advisor AI Agent."""
2
+
3
+ from pydantic import Field
4
+ from pydantic_settings import BaseSettings, SettingsConfigDict
5
+
6
+
7
+ class AdvisorSettings(BaseSettings):
8
+ """Application settings loaded from environment variables."""
9
+
10
+ model_config = SettingsConfigDict(
11
+ env_file=".env",
12
+ env_file_encoding="utf-8",
13
+ case_sensitive=False,
14
+ extra="ignore",
15
+ )
16
+
17
+ # AskTable API Configuration
18
+ asktable_api_key: str = Field(..., description="AskTable API key")
19
+ asktable_api_base: str = Field(
20
+ default="https://api.asktable.com",
21
+ description="AskTable API base URL",
22
+ )
23
+
24
+ # LLM API Configuration (Aliyun Bailian - qwen3-max)
25
+ anthropic_base_url: str = Field(
26
+ default="https://dashscope.aliyuncs.com/apps/anthropic",
27
+ description="Anthropic-compatible API base URL (Aliyun Bailian)",
28
+ )
29
+ anthropic_api_key: str = Field(
30
+ ...,
31
+ description="DashScope API key for Aliyun Bailian",
32
+ )
33
+
34
+ # Agent Configuration
35
+ agent_model: str = Field(
36
+ default="qwen3-max",
37
+ description="LLM model to use for the advisor agent",
38
+ )
39
+ agent_temperature: float = Field(
40
+ default=0.7,
41
+ description="Temperature for LLM responses (0.0-1.0)",
42
+ )
43
+ agent_max_tokens: int = Field(
44
+ default=4096,
45
+ description="Maximum tokens for LLM responses",
46
+ )
47
+
48
+ # MySQL Database Configuration
49
+ mysql_host: str = Field(..., description="MySQL host")
50
+ mysql_port: int = Field(default=3306, description="MySQL port")
51
+ mysql_user: str = Field(..., description="MySQL username")
52
+ mysql_password: str = Field(..., description="MySQL password")
53
+ mysql_database: str = Field(
54
+ default="asktable_advisor",
55
+ description="MySQL database name",
56
+ )
57
+
58
+ # Optional: Database connection pool settings
59
+ mysql_pool_size: int = Field(
60
+ default=5,
61
+ description="Database connection pool size",
62
+ )
63
+ mysql_max_overflow: int = Field(
64
+ default=10,
65
+ description="Maximum overflow connections",
66
+ )
67
+
68
+ @property
69
+ def mysql_url(self) -> str:
70
+ """Generate SQLAlchemy database URL."""
71
+ return (
72
+ f"mysql+pymysql://{self.mysql_user}:{self.mysql_password}"
73
+ f"@{self.mysql_host}:{self.mysql_port}/{self.mysql_database}"
74
+ )
75
+
76
+
77
+ def get_settings() -> AdvisorSettings:
78
+ """Load and return application settings."""
79
+ return AdvisorSettings()
File without changes
File without changes