mdan-cli 2.5.1 → 2.7.0
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.
- package/AGENTS.md +76 -1
- package/README.md +274 -4
- package/agents/auto-orchestrator.md +343 -0
- package/agents/devops.md +511 -94
- package/cli/mdan.py +111 -6
- package/cli/mdan_crewai.py +539 -0
- package/core/crewai_orchestrator.md +419 -0
- package/core/debate-protocol.md +454 -0
- package/core/universal-envelope.md +113 -0
- package/integrations/__init__.py +33 -0
- package/integrations/crewai/__init__.py +27 -0
- package/integrations/crewai/agents/__init__.py +21 -0
- package/integrations/crewai/agents/architect_agent.py +264 -0
- package/integrations/crewai/agents/dev_agent.py +271 -0
- package/integrations/crewai/agents/devops_agent.py +421 -0
- package/integrations/crewai/agents/doc_agent.py +388 -0
- package/integrations/crewai/agents/product_agent.py +203 -0
- package/integrations/crewai/agents/security_agent.py +386 -0
- package/integrations/crewai/agents/test_agent.py +358 -0
- package/integrations/crewai/agents/ux_agent.py +257 -0
- package/integrations/crewai/flows/__init__.py +13 -0
- package/integrations/crewai/flows/auto_flow.py +451 -0
- package/integrations/crewai/flows/build_flow.py +297 -0
- package/integrations/crewai/flows/debate_flow.py +422 -0
- package/integrations/crewai/flows/discovery_flow.py +267 -0
- package/integrations/crewai/orchestrator.py +558 -0
- package/integrations/crewai/skills/__init__.py +8 -0
- package/integrations/crewai/skills/skill_router.py +534 -0
- package/integrations/crewai/tools/__init__.py +11 -0
- package/integrations/crewai/tools/file_tool.py +355 -0
- package/integrations/crewai/tools/serper_tool.py +169 -0
- package/integrations/crewai/tools/sql_tool.py +435 -0
- package/memory/CONTEXT-SAVE-FORMAT.md +328 -0
- package/memory/MEMORY-AUTO.json +66 -0
- package/memory/RESUME-PROTOCOL.md +379 -0
- package/package.json +1 -1
- package/phases/auto-01-load.md +165 -0
- package/phases/auto-02-discover.md +207 -0
- package/phases/auto-03-plan.md +509 -0
- package/phases/auto-04-architect.md +567 -0
- package/phases/auto-05-implement.md +713 -0
- package/phases/auto-06-test.md +559 -0
- package/phases/auto-07-deploy.md +510 -0
- package/phases/auto-08-doc.md +970 -0
- package/skills/azure-devops/skill.md +1757 -0
- package/templates/dotnet-blazor/README.md +415 -0
- package/templates/external-services/ExampleService.cs +361 -0
- package/templates/external-services/IService.cs +113 -0
- package/templates/external-services/README.md +325 -0
- package/templates/external-services/ServiceBase.cs +492 -0
- package/templates/external-services/ServiceProvider.cs +243 -0
- package/templates/prompts/devops-agent.yaml +327 -0
- package/templates/prompts.json +15 -1
- package/templates/sql-server/README.md +37 -0
- package/templates/sql-server/functions.sql +158 -0
- package/templates/sql-server/schema.sql +188 -0
- package/templates/sql-server/stored-procedures.sql +284 -0
|
@@ -0,0 +1,435 @@
|
|
|
1
|
+
"""Async SQL Database Connector Tool"""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import asyncio
|
|
5
|
+
from typing import Optional, List, Dict, Any, Union
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from enum import Enum
|
|
8
|
+
import logging
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class DatabaseType(Enum):
|
|
14
|
+
"""Supported database types"""
|
|
15
|
+
|
|
16
|
+
POSTGRESQL = "postgresql"
|
|
17
|
+
MYSQL = "mysql"
|
|
18
|
+
SQLSERVER = "sqlserver"
|
|
19
|
+
SQLITE = "sqlite"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass
|
|
23
|
+
class QueryResult:
|
|
24
|
+
"""Represents a query result"""
|
|
25
|
+
|
|
26
|
+
columns: List[str]
|
|
27
|
+
rows: List[Dict[str, Any]]
|
|
28
|
+
row_count: int
|
|
29
|
+
execution_time: float
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class SQLTool:
|
|
33
|
+
"""Async SQL database connector with support for multiple database types"""
|
|
34
|
+
|
|
35
|
+
def __init__(
|
|
36
|
+
self,
|
|
37
|
+
connection_string: Optional[str] = None,
|
|
38
|
+
db_type: DatabaseType = DatabaseType.POSTGRESQL,
|
|
39
|
+
**kwargs,
|
|
40
|
+
):
|
|
41
|
+
"""
|
|
42
|
+
Initialize SQLTool
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
connection_string: Database connection string. If None, reads from DATABASE_URL
|
|
46
|
+
db_type: Type of database (POSTGRESQL, MYSQL, SQLSERVER, SQLITE)
|
|
47
|
+
**kwargs: Additional connection parameters
|
|
48
|
+
"""
|
|
49
|
+
self.connection_string = connection_string or os.getenv("DATABASE_URL")
|
|
50
|
+
self.db_type = db_type
|
|
51
|
+
self.connection_params = kwargs
|
|
52
|
+
self._pool = None
|
|
53
|
+
self._driver = None
|
|
54
|
+
|
|
55
|
+
if not self.connection_string and db_type != DatabaseType.SQLLITE:
|
|
56
|
+
raise ValueError(
|
|
57
|
+
"DATABASE_URL not found. Set it as environment variable "
|
|
58
|
+
"or pass connection_string to the constructor."
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
self._initialize_driver()
|
|
62
|
+
|
|
63
|
+
def _initialize_driver(self):
|
|
64
|
+
"""Initialize the appropriate database driver"""
|
|
65
|
+
if self.db_type == DatabaseType.POSTGRESQL:
|
|
66
|
+
try:
|
|
67
|
+
import asyncpg
|
|
68
|
+
|
|
69
|
+
self._driver = asyncpg
|
|
70
|
+
except ImportError:
|
|
71
|
+
raise ImportError(
|
|
72
|
+
"asyncpg is not installed. Install it with: pip install asyncpg"
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
elif self.db_type == DatabaseType.MYSQL:
|
|
76
|
+
try:
|
|
77
|
+
import aiomysql
|
|
78
|
+
|
|
79
|
+
self._driver = aiomysql
|
|
80
|
+
except ImportError:
|
|
81
|
+
raise ImportError(
|
|
82
|
+
"aiomysql is not installed. Install it with: pip install aiomysql"
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
elif self.db_type == DatabaseType.SQLSERVER:
|
|
86
|
+
try:
|
|
87
|
+
import aioodbc
|
|
88
|
+
|
|
89
|
+
self._driver = aioodbc
|
|
90
|
+
except ImportError:
|
|
91
|
+
raise ImportError(
|
|
92
|
+
"aioodbc is not installed. Install it with: pip install aioodbc"
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
elif self.db_type == DatabaseType.SQLLITE:
|
|
96
|
+
try:
|
|
97
|
+
import aiosqlite
|
|
98
|
+
|
|
99
|
+
self._driver = aiosqlite
|
|
100
|
+
except ImportError:
|
|
101
|
+
raise ImportError(
|
|
102
|
+
"aiosqlite is not installed. Install it with: pip install aiosqlite"
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
async def connect(self):
|
|
106
|
+
"""Establish database connection pool"""
|
|
107
|
+
if self._pool:
|
|
108
|
+
return
|
|
109
|
+
|
|
110
|
+
if self.db_type == DatabaseType.POSTGRESQL:
|
|
111
|
+
self._pool = await self._driver.create_pool(
|
|
112
|
+
self.connection_string, **self.connection_params
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
elif self.db_type == DatabaseType.MYSQL:
|
|
116
|
+
self._pool = await self._driver.create_pool(
|
|
117
|
+
host=self.connection_params.get("host", "localhost"),
|
|
118
|
+
port=self.connection_params.get("port", 3306),
|
|
119
|
+
user=self.connection_params.get("user"),
|
|
120
|
+
password=self.connection_params.get("password"),
|
|
121
|
+
db=self.connection_params.get("database"),
|
|
122
|
+
**self.connection_params,
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
elif self.db_type == DatabaseType.SQLSERVER:
|
|
126
|
+
self._pool = await self._driver.create_pool(
|
|
127
|
+
dsn=self.connection_string, **self.connection_params
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
elif self.db_type == DatabaseType.SQLLITE:
|
|
131
|
+
self._pool = await self._driver.connect(
|
|
132
|
+
self.connection_string or ":memory:"
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
async def disconnect(self):
|
|
136
|
+
"""Close database connection pool"""
|
|
137
|
+
if self._pool:
|
|
138
|
+
if self.db_type == DatabaseType.SQLLITE:
|
|
139
|
+
await self._pool.close()
|
|
140
|
+
else:
|
|
141
|
+
self._pool.close()
|
|
142
|
+
await self._pool.wait_closed()
|
|
143
|
+
self._pool = None
|
|
144
|
+
|
|
145
|
+
async def execute_query(
|
|
146
|
+
self, query: str, params: Optional[tuple] = None
|
|
147
|
+
) -> QueryResult:
|
|
148
|
+
"""
|
|
149
|
+
Execute a SELECT query and return results
|
|
150
|
+
|
|
151
|
+
Args:
|
|
152
|
+
query: SQL query string
|
|
153
|
+
params: Query parameters
|
|
154
|
+
|
|
155
|
+
Returns:
|
|
156
|
+
QueryResult with columns, rows, and metadata
|
|
157
|
+
"""
|
|
158
|
+
import time
|
|
159
|
+
|
|
160
|
+
start_time = time.time()
|
|
161
|
+
|
|
162
|
+
if not self._pool:
|
|
163
|
+
await self.connect()
|
|
164
|
+
|
|
165
|
+
try:
|
|
166
|
+
if self.db_type == DatabaseType.POSTGRESQL:
|
|
167
|
+
async with self._pool.acquire() as conn:
|
|
168
|
+
rows = (
|
|
169
|
+
await conn.fetch(query, *params)
|
|
170
|
+
if params
|
|
171
|
+
else await conn.fetch(query)
|
|
172
|
+
)
|
|
173
|
+
columns = list(rows[0].keys()) if rows else []
|
|
174
|
+
row_dicts = [dict(row) for row in rows]
|
|
175
|
+
|
|
176
|
+
elif self.db_type == DatabaseType.MYSQL:
|
|
177
|
+
async with self._pool.acquire() as conn:
|
|
178
|
+
async with conn.cursor() as cursor:
|
|
179
|
+
await cursor.execute(query, params or ())
|
|
180
|
+
rows = await cursor.fetchall()
|
|
181
|
+
columns = (
|
|
182
|
+
[desc[0] for desc in cursor.description]
|
|
183
|
+
if cursor.description
|
|
184
|
+
else []
|
|
185
|
+
)
|
|
186
|
+
row_dicts = [dict(zip(columns, row)) for row in rows]
|
|
187
|
+
|
|
188
|
+
elif self.db_type == DatabaseType.SQLSERVER:
|
|
189
|
+
async with self._pool.acquire() as conn:
|
|
190
|
+
async with conn.cursor() as cursor:
|
|
191
|
+
await cursor.execute(query, params or ())
|
|
192
|
+
rows = await cursor.fetchall()
|
|
193
|
+
columns = (
|
|
194
|
+
[column[0] for column in cursor.description]
|
|
195
|
+
if cursor.description
|
|
196
|
+
else []
|
|
197
|
+
)
|
|
198
|
+
row_dicts = [dict(zip(columns, row)) for row in rows]
|
|
199
|
+
|
|
200
|
+
elif self.db_type == DatabaseType.SQLLITE:
|
|
201
|
+
rows = await self._pool.execute_fetchall(query, params or ())
|
|
202
|
+
columns = (
|
|
203
|
+
[desc[0] for desc in self._pool.description]
|
|
204
|
+
if self._pool.description
|
|
205
|
+
else []
|
|
206
|
+
)
|
|
207
|
+
row_dicts = [dict(zip(columns, row)) for row in rows]
|
|
208
|
+
|
|
209
|
+
execution_time = time.time() - start_time
|
|
210
|
+
|
|
211
|
+
return QueryResult(
|
|
212
|
+
columns=columns,
|
|
213
|
+
rows=row_dicts,
|
|
214
|
+
row_count=len(row_dicts),
|
|
215
|
+
execution_time=execution_time,
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
except Exception as e:
|
|
219
|
+
logger.error(f"Query execution failed: {str(e)}")
|
|
220
|
+
raise RuntimeError(f"Query execution failed: {str(e)}") from e
|
|
221
|
+
|
|
222
|
+
async def execute_command(
|
|
223
|
+
self, command: str, params: Optional[tuple] = None
|
|
224
|
+
) -> int:
|
|
225
|
+
"""
|
|
226
|
+
Execute an INSERT, UPDATE, or DELETE command
|
|
227
|
+
|
|
228
|
+
Args:
|
|
229
|
+
command: SQL command string
|
|
230
|
+
params: Command parameters
|
|
231
|
+
|
|
232
|
+
Returns:
|
|
233
|
+
Number of affected rows
|
|
234
|
+
"""
|
|
235
|
+
if not self._pool:
|
|
236
|
+
await self.connect()
|
|
237
|
+
|
|
238
|
+
try:
|
|
239
|
+
if self.db_type == DatabaseType.POSTGRESQL:
|
|
240
|
+
async with self._pool.acquire() as conn:
|
|
241
|
+
result = (
|
|
242
|
+
await conn.execute(command, *params)
|
|
243
|
+
if params
|
|
244
|
+
else await conn.execute(command)
|
|
245
|
+
)
|
|
246
|
+
return result.split()[-1] if isinstance(result, str) else result
|
|
247
|
+
|
|
248
|
+
elif self.db_type == DatabaseType.MYSQL:
|
|
249
|
+
async with self._pool.acquire() as conn:
|
|
250
|
+
async with conn.cursor() as cursor:
|
|
251
|
+
await cursor.execute(command, params or ())
|
|
252
|
+
await conn.commit()
|
|
253
|
+
return cursor.rowcount
|
|
254
|
+
|
|
255
|
+
elif self.db_type == DatabaseType.SQLSERVER:
|
|
256
|
+
async with self._pool.acquire() as conn:
|
|
257
|
+
async with conn.cursor() as cursor:
|
|
258
|
+
await cursor.execute(command, params or ())
|
|
259
|
+
return cursor.rowcount
|
|
260
|
+
|
|
261
|
+
elif self.db_type == DatabaseType.SQLLITE:
|
|
262
|
+
await self._pool.execute(command, params or ())
|
|
263
|
+
await self._pool.commit()
|
|
264
|
+
return self._pool.total_changes
|
|
265
|
+
|
|
266
|
+
except Exception as e:
|
|
267
|
+
logger.error(f"Command execution failed: {str(e)}")
|
|
268
|
+
raise RuntimeError(f"Command execution failed: {str(e)}") from e
|
|
269
|
+
|
|
270
|
+
async def execute_transaction(self, commands: List[tuple]) -> List[int]:
|
|
271
|
+
"""
|
|
272
|
+
Execute multiple commands in a transaction
|
|
273
|
+
|
|
274
|
+
Args:
|
|
275
|
+
commands: List of (command, params) tuples
|
|
276
|
+
|
|
277
|
+
Returns:
|
|
278
|
+
List of affected row counts
|
|
279
|
+
"""
|
|
280
|
+
if not self._pool:
|
|
281
|
+
await self.connect()
|
|
282
|
+
|
|
283
|
+
results = []
|
|
284
|
+
|
|
285
|
+
try:
|
|
286
|
+
if self.db_type == DatabaseType.POSTGRESQL:
|
|
287
|
+
async with self._pool.acquire() as conn:
|
|
288
|
+
async with conn.transaction():
|
|
289
|
+
for command, params in commands:
|
|
290
|
+
result = (
|
|
291
|
+
await conn.execute(command, *params)
|
|
292
|
+
if params
|
|
293
|
+
else await conn.execute(command)
|
|
294
|
+
)
|
|
295
|
+
results.append(
|
|
296
|
+
int(result.split()[-1])
|
|
297
|
+
if isinstance(result, str)
|
|
298
|
+
else result
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
elif self.db_type in [DatabaseType.MYSQL, DatabaseType.SQLSERVER]:
|
|
302
|
+
async with self._pool.acquire() as conn:
|
|
303
|
+
async with conn.cursor() as cursor:
|
|
304
|
+
for command, params in commands:
|
|
305
|
+
await cursor.execute(command, params or ())
|
|
306
|
+
results.append(cursor.rowcount)
|
|
307
|
+
await conn.commit()
|
|
308
|
+
|
|
309
|
+
elif self.db_type == DatabaseType.SQLLITE:
|
|
310
|
+
for command, params in commands:
|
|
311
|
+
await self._pool.execute(command, params or ())
|
|
312
|
+
results.append(self._pool.total_changes)
|
|
313
|
+
await self._pool.commit()
|
|
314
|
+
|
|
315
|
+
return results
|
|
316
|
+
|
|
317
|
+
except Exception as e:
|
|
318
|
+
logger.error(f"Transaction failed: {str(e)}")
|
|
319
|
+
raise RuntimeError(f"Transaction failed: {str(e)}") from e
|
|
320
|
+
|
|
321
|
+
async def get_table_schema(self, table_name: str) -> List[Dict[str, Any]]:
|
|
322
|
+
"""Get schema information for a table"""
|
|
323
|
+
if self.db_type == DatabaseType.POSTGRESQL:
|
|
324
|
+
query = """
|
|
325
|
+
SELECT column_name, data_type, is_nullable, column_default
|
|
326
|
+
FROM information_schema.columns
|
|
327
|
+
WHERE table_name = $1
|
|
328
|
+
ORDER BY ordinal_position
|
|
329
|
+
"""
|
|
330
|
+
result = await self.execute_query(query, (table_name,))
|
|
331
|
+
return result.rows
|
|
332
|
+
|
|
333
|
+
elif self.db_type == DatabaseType.MYSQL:
|
|
334
|
+
query = """
|
|
335
|
+
SELECT column_name, data_type, is_nullable, column_default
|
|
336
|
+
FROM information_schema.columns
|
|
337
|
+
WHERE table_name = %s
|
|
338
|
+
ORDER BY ordinal_position
|
|
339
|
+
"""
|
|
340
|
+
result = await self.execute_query(query, (table_name,))
|
|
341
|
+
return result.rows
|
|
342
|
+
|
|
343
|
+
elif self.db_type == DatabaseType.SQLSERVER:
|
|
344
|
+
query = """
|
|
345
|
+
SELECT column_name, data_type, is_nullable, column_default
|
|
346
|
+
FROM information_schema.columns
|
|
347
|
+
WHERE table_name = ?
|
|
348
|
+
ORDER BY ordinal_position
|
|
349
|
+
"""
|
|
350
|
+
result = await self.execute_query(query, (table_name,))
|
|
351
|
+
return result.rows
|
|
352
|
+
|
|
353
|
+
elif self.db_type == DatabaseType.SQLLITE:
|
|
354
|
+
query = f"PRAGMA table_info({table_name})"
|
|
355
|
+
result = await self.execute_query(query)
|
|
356
|
+
return result.rows
|
|
357
|
+
|
|
358
|
+
async def list_tables(self) -> List[str]:
|
|
359
|
+
"""List all tables in the database"""
|
|
360
|
+
if self.db_type == DatabaseType.POSTGRESQL:
|
|
361
|
+
query = """
|
|
362
|
+
SELECT table_name
|
|
363
|
+
FROM information_schema.tables
|
|
364
|
+
WHERE table_schema = 'public'
|
|
365
|
+
ORDER BY table_name
|
|
366
|
+
"""
|
|
367
|
+
elif self.db_type == DatabaseType.MYSQL:
|
|
368
|
+
query = """
|
|
369
|
+
SELECT table_name
|
|
370
|
+
FROM information_schema.tables
|
|
371
|
+
WHERE table_schema = DATABASE()
|
|
372
|
+
ORDER BY table_name
|
|
373
|
+
"""
|
|
374
|
+
elif self.db_type == DatabaseType.SQLSERVER:
|
|
375
|
+
query = """
|
|
376
|
+
SELECT table_name
|
|
377
|
+
FROM information_schema.tables
|
|
378
|
+
WHERE table_type = 'BASE TABLE'
|
|
379
|
+
ORDER BY table_name
|
|
380
|
+
"""
|
|
381
|
+
elif self.db_type == DatabaseType.SQLLITE:
|
|
382
|
+
query = "SELECT name FROM sqlite_master WHERE type='table' ORDER BY name"
|
|
383
|
+
|
|
384
|
+
result = await self.execute_query(query)
|
|
385
|
+
return [
|
|
386
|
+
row["table_name"] if "table_name" in row else row["name"]
|
|
387
|
+
for row in result.rows
|
|
388
|
+
]
|
|
389
|
+
|
|
390
|
+
def format_results(self, result: QueryResult) -> str:
|
|
391
|
+
"""Format query results as readable text"""
|
|
392
|
+
lines = [
|
|
393
|
+
f"Query returned {result.row_count} rows in {result.execution_time:.3f}s",
|
|
394
|
+
"",
|
|
395
|
+
]
|
|
396
|
+
|
|
397
|
+
if result.columns:
|
|
398
|
+
header = " | ".join(result.columns)
|
|
399
|
+
lines.append(header)
|
|
400
|
+
lines.append("-" * len(header))
|
|
401
|
+
|
|
402
|
+
for row in result.rows:
|
|
403
|
+
values = " | ".join(str(v) for v in row.values())
|
|
404
|
+
lines.append(values)
|
|
405
|
+
|
|
406
|
+
return "\n".join(lines)
|
|
407
|
+
|
|
408
|
+
async def __aenter__(self):
|
|
409
|
+
"""Async context manager entry"""
|
|
410
|
+
await self.connect()
|
|
411
|
+
return self
|
|
412
|
+
|
|
413
|
+
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
|
414
|
+
"""Async context manager exit"""
|
|
415
|
+
await self.disconnect()
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
def run_query_sync(query: str, params: Optional[tuple] = None, **kwargs) -> QueryResult:
|
|
419
|
+
"""
|
|
420
|
+
Synchronous wrapper for async query execution
|
|
421
|
+
|
|
422
|
+
Args:
|
|
423
|
+
query: SQL query string
|
|
424
|
+
params: Query parameters
|
|
425
|
+
**kwargs: Additional SQLTool parameters
|
|
426
|
+
|
|
427
|
+
Returns:
|
|
428
|
+
QueryResult with columns, rows, and metadata
|
|
429
|
+
"""
|
|
430
|
+
|
|
431
|
+
async def _run():
|
|
432
|
+
async with SQLTool(**kwargs) as db:
|
|
433
|
+
return await db.execute_query(query, params)
|
|
434
|
+
|
|
435
|
+
return asyncio.run(_run())
|