
Alibabacloud Data Agent Skill
- 43 installs
- 45 repo stars
- Updated August 4, 2026
- aliyun/data-agent-skill
alibabacloud-data-agent-skill is a Claude skill that invokes Alibaba Cloud Apsara Data Agent for Analytics via CLI to run natural-language data analysis and report generation on DMS-managed databases.
About
This skill invokes the Alibaba Cloud Apsara Data Agent for Analytics via CLI to run natural-language data analysis on enterprise databases. A developer uses it to discover DMS-managed data resources, start query or deep-analysis sessions, track progress in real time, and retrieve analysis conclusions and generated reports. It requires Python 3.10+ in a venv and data sources managed in Alibaba Cloud Apsara Database or DMS.
- Invokes Alibaba Cloud Apsara Data Agent for natural-language data analysis on enterprise databases
- Discovers DMS-managed instances/databases/tables and runs query or deep-analysis sessions
- Tracks progress in real time and retrieves conclusions and generated reports
Alibabacloud Data Agent Skill by the numbers
- 43 all-time installs (skills.sh)
- +2 installs in the week ending Jun 23, 2026 (Skillselion tracking)
- Ranked #980 of 2,064 Data Science & ML skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
alibabacloud-data-agent-skill capabilities & compatibility
Free skill; requires an Alibaba Cloud account with DMS/Apsara Data Agent, optionally a DATA_AGENT_API_KEY (billed by Alibaba Cloud).
- Capabilities
- data analysis · report generation · nl to sql · data discovery
- Use cases
- data analysis · database · research
- Runs
- Runs locally
- Pricing
- Bring your own API key
What alibabacloud-data-agent-skill says it does
Invoke Alibaba Cloud Apsara Data Agent for Analytics via CLI to perform natural language-driven data analysis on enterprise databases.
It automatically completes requirement analysis, data understanding, analysis insights, and report generation based on natural language descriptions.
npx skills add https://github.com/aliyun/data-agent-skill --skill alibabacloud-data-agent-skillAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 43 |
|---|---|
| repo stars | ★ 45 |
| Last updated | August 4, 2026 |
| Repository | aliyun/data-agent-skill ↗ |
What it does
Run natural-language data analysis and report generation on DMS-managed enterprise databases via Apsara Data Agent.
Who is it for?
Natural-language data analysis, insights and report generation over DMS-managed enterprise databases.
When should I use this skill?
You need to query databases, analyze data trends, or generate data reports in natural language.
What you get
Natural-language questions produce data analysis, insights and generated reports over DMS-managed databases.
By the numbers
- Requires Python 3.10+
- Ships a built-in demo database internal_data_employees
Files
metadata: author: DataAgent Team version: "1.8.6" ---
Changelog
- v1.8.6: Add explicit DMSUnit configuration via
DATA_AGENT_DMS_UNIT; DMSUnit resolution now uses env/config override beforeGetActiveRouteUnitand region fallback. - v1.8.5 — Database listing migrated to
ListTagMetaAsset(dms-enterprise 2018-11-01); workspace auto-resolution (CLI--workspace-id> envDATA_AGENT_WORKSPACE_ID>InitDataAgentPersonalWorkspace);dbsubcommand relaxed--dms-instance-id/--instance-nameto optional. - v1.8.4: Document project Python virtualenv (
venv/) setup and activation; add end-to-end regression notes for ASK_DATA / ANALYSIS (async + attach) - v1.8.3:
dbandfilesubcommands now accept--session-mode CLAW - v1.8.2:
SendChatMessagenow supports per-messageMode=CLAW(injected viaSessionConfig.Mode); dynamic DMSUnit resolution viaGetActiveRouteUnit - v1.8.1: Emphasize
attach-based session reuse as the core interaction mechanism; add golden workflow, capability matrix, and usage rules - v1.8.0: Add workspace (collaborative space) support, add custom agent support
- v1.7.2: Use Alibaba Cloud default credential chain instead of explicit AK/SK, add User-Agent header, fix RAM policy wildcard issues
- v1.7.1: Fix CLI
lscommand API response parsing (support case-insensitive field names), optimize SKILL documentation structure, separate ANALYSIS mode specification document - v1.7.0: API_KEY authentication support, native async execution mode, session isolation, enhanced attach mode, optimized log output
---
---
Installation
Python Environment (venv) — MUST READ
🚨 Hard Requirement: Python ≥ 3.10
>
The macOS system/usr/bin/python3is typically 3.8 or 3.9 and cannot run this project (it relies onmatch/case,TypeAlias,|union syntax, and other 3.10+ features).
>
Verify your version first: python3 --version. If below 3.10, install via Homebrew or pyenv:```bash
# Homebrew
brew install python@3.12
# Or pyenv
pyenv install 3.12.4 && pyenv local 3.12.4
```
⚠️ You MUST use a venv virtual environment. Never install dependencies globally. Running pip install against the system Python pollutes the environment and may fail due to permission issues.Use Existing venv (Recommended)
The project ships a pre-built venv/ directory (all dependencies pre-installed). Use it whenever possible:
cd data-agent-skill
# Option A (recommended): activate the venv
source venv/bin/activate
python3 scripts/data_agent_cli.py ls
# Option B: invoke the venv interpreter directly (no activation needed)
venv/bin/python3 scripts/data_agent_cli.py lsRebuild venv
If venv/ is missing or dependencies are corrupted, recreate with a 3.10+ Python:
python3.12 -m venv venv # explicitly use a 3.10+ interpreter
source venv/bin/activate
pip install -r scripts/requirements.txtTip: All examples in this document writepython3 scripts/data_agent_cli.py .... When venv is activated,python3resolves to the venv interpreter automatically; otherwise prefix withvenv/bin/python3.
Configure Credentials
This Skill uses Alibaba Cloud default credential chain (recommended) or API_KEY authentication.
Option 1: Default Credential Chain (Recommended)
The Skill uses Alibaba Cloud SDK's default credential chain to automatically obtain credentials, supporting environment variables, configuration files, instance roles, etc.
See Alibaba Cloud Credential Chain Documentation
Option 2: API_KEY Authentication (File Analysis Only)
export DATA_AGENT_API_KEY=your-api-key
export DATA_AGENT_REGION=cn-hangzhouGet API_KEY: Data Agent Console
Permission Requirements
RAM users need AliyunDMSFullAccess or AliyunDMSDataAgentFullAccess permissions. See RAM-POLICIES.md for detailed permission information.
Debug Mode
DATA_AGENT_DEBUG_API=1 python3 scripts/data_agent_cli.py file example.csv -q "analyze"💡 Getting Started Tips
- Use the built-in demo database
internal_data_employees(DataAgent's built-in test database containing employee, department, and salary data) for first-time experience - Or use local file
assets/example_game_data.csvfor file analysis experience
Data Agent CLI — Unified Command-Line Data Analysis Tool
Overview
scripts/data_agent_cli.py helps users complete the full workflow from discover data → initiate analysis → track progress → get results.
Core Concepts
⚠️ Key Prerequisite: Data Agent can only analyze databases that have been imported into Data Agent Data Center.
>
- Data Center: Data Agent's data center, only databases here can be analyzed
- DMS: Alibaba Cloud Data Management Service, stores metadata of all databases
- Relationship: Databases registered in DMS ≠ Databases in Data Center
>
Usage Flow:
1. First use ls to check if the target database exists in Data Center2. If not found, usedmssubcommand to search for database info, then useimportsubcommand to import it
3. After successful import, you can use db subcommand for analysis---
Analysis Modes
- ASK_DATA (default): Synchronous execution, sub-second response, suitable for quick Q&A
- ANALYSIS: Deep analysis, takes 5-40 minutes, requires spawning a sub-agent for async execution or using --async-run parameter
- INSIGHT: Insight-oriented exploration, follows the same plan-confirmation flow as ANALYSIS
- CLAW: Agentic CLAW mode. Two entry points:
- CLI:
db --session-mode CLAW .../file --session-mode CLAW ...(session-level) - SDK: pass
mode="CLAW"toclient.send_message(...)/AsyncDataAgentClient.send_message(...)to override mode for a single message viaSessionConfig.Mode
End-to-End Regression Reference (v1.8.4 verified)
Both ASK_DATA and ANALYSIS modes are regression-tested against chinook database with the async + attach flow:
| Mode | Kickoff | Observed Chain | Typical Duration |
|---|---|---|---|
| ASK_DATA | db --session-mode ASK_DATA -q "..." | async worker → live SSE → result.json={"status":"completed"} | ~15s |
| ANALYSIS | db --session-mode ANALYSIS -q "..." | async worker → Plan → WAIT_INPUT → attach -q "confirm" → step-by-step execution → Excel/Chart artifacts → text report → 2nd WAIT_INPUT (webpage render) | 2-10 min (text); +10 min if rendering webpage |
Key checkpoints to look for in sessions/<SESSION_ID>/progress.log:
> User Query: ...— request received### Execution Plan (ID: ...)— ANALYSIS plan generated, useattach -q "confirm"to proceed> ⚠️ Plan confirmed, continuing analysis...— plan approved, execution starts## Step N/M: ...— per-step progress with artifacts links### Report Render+⚠️ Please review the report rendering request.— optional HTML report render confirmation
See ANALYSIS_MODE.md for details
---
Workspace (Collaborative Space)
Workspaces are collaborative spaces that enable team-based data analysis with shared sessions, data sources, and access control.
- List workspaces: Use
workspacesubcommand to discover available workspaces (personal or shared) - Bind session to workspace: Pass
--workspace-id <ID>when usingdborfileto create a session within a specific workspace context - Workspace types:
MY(default, personal spaces),ALL(all accessible spaces including shared ones)
Note: When a session is created within a workspace, all subsequent API calls (describe, send message, etc.) automatically carry the workspace context.
Workspace Resolution
The workspace ID is resolved automatically in this order: 1. CLI flag --workspace-id <id> 2. Environment variable DATA_AGENT_WORKSPACE_ID 3. Auto-create personal workspace via InitDataAgentPersonalWorkspace
Both AK/SK and API_KEY authentication modes support this resolution chain.
DMSUnit Resolution
The DMSUnit is resolved automatically in this order: 1. Environment variable DATA_AGENT_DMS_UNIT 2. Dynamic route lookup via GetActiveRouteUnit 3. Region fallback from DATA_AGENT_REGION
Set DATA_AGENT_DMS_UNIT only when the tenant must use a specific DMSUnit. Leave it unset for the automatic route lookup.
ListTagMetaAsset data discovery remains region-scoped and uses DATA_AGENT_REGION in tags such as sys::DMS-DA::<region>::space:<workspace>.
---
Custom Agent
Custom Agents are user-defined AI agents with specialized instructions, knowledge bases, and data scope configurations.
- List custom agents: Use
agentsubcommand to discover available custom agents (RELEASED status by default) - View agent details: Use
agent describe --custom-agent-id <ID>to see full agent configuration - Bind session to custom agent: Pass
--custom-agent-id <ID>when usingdborfileto create a session powered by a specific custom agent
Note: Custom Agent sessions automatically use the prod stage. The custom agent's instructions, knowledge, and data scope will be applied to the analysis session.---
Session Reuse via attach (⭐ Core Mechanism)
Best Practice:attachis the recommended way to interact with an ongoing or previously created session. Always preferattachover creating a new session for any follow-up interaction on the same data scope.
Why Use attach
After you call db / file to start a session, all subsequent interactions on that session MUST go through `attach --session-id <ID>`. A single session = a single conversation context on the server side, and attach is the only way to safely re-enter it.
| Capability | Command | Scenario |
|---|---|---|
| Follow-up questions | attach --session-id <ID> -q "..." | Continue the conversation with full context, skip data-understanding overhead |
| Plan confirmation | attach --session-id <ID> -q "confirm" | Approve the execution plan generated by ANALYSIS/INSIGHT mode |
| Plan modification | attach --session-id <ID> -q "simplify to 3 steps" | Refine the plan before execution |
| Progress monitoring | attach --session-id <ID> (no -q) | Tail live SSE progress of a long-running session |
| Resume after network drop | attach --session-id <ID> --checkpoint <N> | Precise recovery from the Nth event after interruption |
| Replay full history | attach --session-id <ID> --from-start | Re-stream the entire session from event 0 |
Golden Workflow (Async + attach)
The canonical pattern for long-running analyses is async `db` kickoff → `attach` for everything else:
# 1) Kick off async analysis, returns SESSION_ID immediately
python3 scripts/data_agent_cli.py db \
--dms-db-id <dbId> \
--db-name <schemaName> \
--tables "employees,departments" \
--workspace-id <workspace_id> \
--session-mode ANALYSIS \
-q "Analyze salary distribution"
# -> ✅ Async task started. Session ID: abc123xyz
# 2) Watch progress live (Ctrl-C is safe, server keeps running)
python3 scripts/data_agent_cli.py attach --session-id abc123xyz
# 3) Confirm or modify the plan when agent enters WAIT_INPUT
python3 scripts/data_agent_cli.py attach --session-id abc123xyz -q "confirm"
# 4) Ask follow-up questions (reuses context, no re-import, no re-profiling)
python3 scripts/data_agent_cli.py attach --session-id abc123xyz -q "Break down by job level"
# 5) Recover precisely if the stream was cut at event #219
python3 scripts/data_agent_cli.py attach --session-id abc123xyz --checkpoint 219
# 6) Fetch generated reports / charts
python3 scripts/data_agent_cli.py reports --session-id abc123xyzBenefits of Reusing a Session via attach
- Context preservation — previous SQL, table profiling, and user intent are kept, answers stay consistent.
- Cost reduction — skip re-discovering schema / re-profiling tables on every question.
- Plan governance — ANALYSIS / INSIGHT plans require explicit confirmation; only
attach -q "confirm"can unblock them. - Resilience —
--checkpoint/--from-startmake long-running tasks robust against network drops and client restarts. - Team collaboration — share the Session ID, teammates can
attachto the same session to review progress and results.
Rules of Thumb
1. Create session once with db / file; drive everything else with attach. 2. Record the Session ID printed after kickoff — it is the only handle to the session. 3. For ANALYSIS / INSIGHT mode, always use attach (not a new db) to confirm plans; creating a new session loses the plan. 4. Session artifacts (progress log, checkpoint, result, images) are persisted under sessions/<SESSION_ID>/.
See COMMANDS.md for the full attach parameter list and WORKFLOWS.md for end-to-end scenarios.---
Quick Start
# 1. List available databases
python3 scripts/data_agent_cli.py ls
# Example output:
# chinook [mysql] dbId=abc123 instanceResourceId=rm-xxx catalogName=chinook
# employees [mysql] dbId=def456 instanceResourceId=rm-yyy catalogName=employees
# 2. Create a session for initial analysis (record the returned Session ID!)
python3 scripts/data_agent_cli.py db \
--dms-db-id <dbId> \
--db-name <schemaName> \
--tables <table1,table2> \
--workspace-id <workspace_id> \
-q "Which department has the highest average salary"
# -> ✅ Async task started. Session ID: abc123xyz
# 3. ⭐ Reuse the session — follow-up questions, confirm plans, monitor progress
python3 scripts/data_agent_cli.py attach --session-id abc123xyz -q "Break down by month"
python3 scripts/data_agent_cli.py attach --session-id abc123xyz -q "confirm" # approve ANALYSIS plan
python3 scripts/data_agent_cli.py attach --session-id abc123xyz # tail live progress
python3 scripts/data_agent_cli.py attach --session-id abc123xyz --checkpoint 219 # resume after drop
# 4. List workspaces
python3 scripts/data_agent_cli.py workspace
# 5. Query in a specific workspace
python3 scripts/data_agent_cli.py db \
--workspace-id <WORKSPACE_ID> \
--dms-db-id <dbId> \
--db-name <schemaName> \
--tables <table1,table2> -q "Which department has the highest average salary"
# 6. List available custom agents
python3 scripts/data_agent_cli.py agent
# 7. Use a custom agent for analysis
python3 scripts/data_agent_cli.py db --custom-agent-id <AGENT_ID> --dms-instance-id ... -q "your question"Remember:db/filecreate the session once; all follow-ups go throughattach --session-id <ID>.
📖 See WORKFLOWS.md and COMMANDS.md for complete workflows, command reference, and best practices
---
Project Structure
# Skill root directory
├── SKILL.md # This document
├── scripts/ # Source code
│ ├── data_agent/ # SDK module
│ ├── cli/ # CLI module
│ ├── data_agent_cli.py # CLI entry point
│ └── requirements.txt # Dependencies
├── sessions/ # Session data
└── references/ # Reference documents# Alibaba Cloud Credentials
# This SDK uses the Alibaba Cloud default credential chain.
# Credentials are resolved automatically from:
# 1. Environment variables (handled by SDK)
# 2. ~/.aliyun/config.json
# 3. ECS instance role / OIDC role
# Alternative: API Key Authentication (for file analysis scenarios)
# DATA_AGENT_API_KEY=your_api_key
# Region Configuration (Optional)
DATA_AGENT_REGION=cn-hangzhou
# DMSUnit Configuration (Optional)
# Leave unset for automatic GetActiveRouteUnit resolution.
# Set when your tenant must use a specific DMSUnit instead of the region fallback.
# DATA_AGENT_DMS_UNIT=cn-hangzhou
# Custom Endpoint (Optional, auto-generated if not set)
# DATA_AGENT_ENDPOINT=dms.cn-hangzhou.aliyuncs.com
# Timeout Settings (Optional)
DATA_AGENT_TIMEOUT=300
DATA_AGENT_MAX_RETRY=3
# Polling Settings (Optional)
DATA_AGENT_POLL_INTERVAL=2
DATA_AGENT_MAX_POLL_COUNT=60
# Qoder
.qoder/
# macOS
.DS_Store
.AppleDouble
.LSOverride
Thumbs.db
# Node.js
node_modules/
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.npm
.yarn-integrity
dist/
build/
.cache/
# Python
__pycache__/
*.py[cod]
*$py.class
*.egg-info/
.eggs/
dist/
build/
.venv/
venv/
env/
.env
# Java
*.class
*.jar
*.war
*.ear
target/
.mvn/
# IDE
.idea/
.vscode/
*.swp
*.swo
*~
# Environment & Secrets
.env
.env.local
.env.*.local
*.pem
*.key
# Logs
logs/
*.log
# OS
*.tmp
*.bak
.agents/
skills-lock.json
.claude
.qwen
# 忽略所有以 validation-result- 开头、以 .json 结尾的文件
validation-result-*.json
tests/
sessions/
CLAUDE.md
evals/
.omc/
ua-fix-report.md
Data Agent 自动监控
当 HEARTBEAT 触发时,按照以下步骤 chain-of-thought 执行:
1. 扫描目录:dms-data-agent/sessions/*(或你的实际 sessions 路径)
- 使用 file_list 或 ls 列出所有子目录(每个对应一个 session_id)
2. 对每个 session 检查状态:
- 读
status.txt - 如果 "running":
- 读
progress.log把新的进展告诉用户,尤其是计划执行的步骤内容 - 如果 "completed":
- 读 reports/ 目录或 使用report子命令获取总结文件, Markdown 格式的内容给客户呈现, html可以通过浏览器打开。
- 如果 "failed":
- 读 error.log,总结原因
- 建议:"请用 attach {id} 重试或检查日志"
3. 通知规则:
- 需要及时的反馈阶段性洞察给客户,以及里程碑首次到达、完成或失败时发消息。
- 使用 send_message tool 或当前 channel(如 Telegram/WhatsApp)推送
- 避免重复:记录上次计划执行的步骤结论(可写到 SESSION-STATE.md 或专用 data-agent-state.md)
4. 如果无新通知内容:
- 安静结束 turn(回复 HEARTBEAT_OK,gateway 会自动丢弃)
优先使用 isolated agentTurn 执行检查(不干扰主对话)。
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
ANALYSIS Mode Sub-Agent Specification
ANALYSIS mode tasks are long-running (5-40 minutes), you must spawn a sub-agent to execute in background to avoid blocking the main conversation flow.
---
Main Agent Behavior
1. Inform user that deep analysis has started, provide Session ID 2. Return immediately, continue responding to other user questions (non-blocking)
---
Sub-Agent Behavior
1. Execute command in background, periodically poll status.txt (every 30-60 seconds) 2. Push progress updates to main agent/user at key checkpoints 3. Return report or guide user to console when task completes
---
Key Checkpoints for Sub-Agent to Report
| Checkpoint | Trigger Condition | Report Content | Required Action |
|---|---|---|---|
| 🚀 Task Started | After command execution | Session ID, estimated duration | No action needed |
| 📋 Execution Plan Ready | status.txt = waiting_input | Show analysis plan/SQL | Must wait for user confirmation, then `attach -q 'confirm execution'` |
| 📊 Phase Conclusion | New content in progress.log | Phase analysis summary | No action needed |
| ✅ Task Completed | status.txt = completed | Report link or file path | Guide user to view report |
| ❌ Task Failed | status.txt = failed | Error message | Prompt user to retry |
⚠️ `waiting_input` is a mandatory wait point: Worker has exited, sub-agent must present execution plan to user, execute attach -q 'confirm execution' after receiving confirmation, otherwise task will be permanently paused.---
Guide User to View Report After Task Completion
# Download report to local
python3 scripts/data_agent_cli.py reports --session-id <SESSION_ID>
# Or guide user to Data Agent console (recommended)
# https://agent.dms.aliyun.com/<region>/session/<SESSION_ID>Subcommand Reference
This document contains detailed descriptions and complete parameter lists for all subcommands.
---
ls Subcommand — Discover Data Resources
Before initiating analysis, use ls to understand what databases and tables are available.
Environment Resolution
CLI commands load .env and process environment variables through DataAgentConfig.from_env().
| Variable | Description |
|---|---|
DATA_AGENT_REGION | RegionId for Data Agent and DMS endpoints, default cn-hangzhou |
DATA_AGENT_DMS_UNIT | Optional explicit DMSUnit override for Data Agent session APIs; ListTagMetaAsset discovery tags still use DATA_AGENT_REGION |
DATA_AGENT_WORKSPACE_ID | Optional default workspace ID when --workspace-id is omitted |
⚠️ Important: The ls command only shows databases that have been imported into Data Agent Data Center. If you can't find the database you need, it means the database has not been imported from DMS yet.List All Databases
python3 scripts/data_agent_cli.py lsOutput is divided into two groups:
- Database Connections (ImportType: RDS/DMS) — Real relational databases, can be used with
dbsubcommand - File Data Sources (ImportType: FILE) — Uploaded file datasets
Output displays databases in compact format:
chinook [mysql] dbId=abc123 instanceResourceId=rm-xxx catalogName=chinook
employees [mysql] dbId=def456 instanceResourceId=rm-yyy catalogName=employeesTip: internal_data_employees is DataAgent's built-in demo database, containing employee, department, and salary test data, suitable for first-time experience.Filter by Keyword
python3 scripts/data_agent_cli.py ls --search internal_data_employeesList Tables for a Specific Database + Generate Usable Command
python3 scripts/data_agent_cli.py ls --db-id <AgentDbId>Output includes a db command template ready to copy and use.
---
db Subcommand — Database Analysis
⚠️ Important: The db subcommand requires the database to already exist in Data Agent Data Center.Data Source Parameters
| Parameter | Description |
|---|---|
--dms-instance-id | DMS Instance ID (numeric) |
--dms-db-id | DMS Database ID (numeric) |
--instance-name | RDS Instance Name |
--db-name | Database Name |
--tables | Table name list, comma-separated |
--engine | Database engine, default mysql |
Session Parameters
| Parameter | Description |
|---|---|
--session-mode | ASK_DATA (default) / ANALYSIS / INSIGHT |
--output | summary (default) / detail / raw |
--enable-search | Enable search capability (default false) |
--workspace-id | Workspace ID to bind the session to a specific workspace |
--custom-agent-id | Custom Agent ID to use for the analysis session |
Query Methods
| Parameter | Description |
|---|---|
-q / --query | Single query (runs preset query if not specified) |
---
file Subcommand — File Analysis
File analysis uses ANALYSIS mode by default.
Parameters
| Parameter | Description |
|---|---|
--session-mode | ASK_DATA / ANALYSIS (default) / INSIGHT |
--output | summary (default) / detail / raw |
--enable-search | Enable search capability (default false) |
--workspace-id | Workspace ID to bind the session to a specific workspace |
--custom-agent-id | Custom Agent ID to use for the analysis session |
-q / --query | Custom query question |
---
reports Subcommand — Get Generated Results
View and download reports and chart files generated by the session.
Parameters
| Parameter | Description |
|---|---|
--session-id | Required, Session ID |
---
Session Mode Description
| Mode | Features | Duration | Report | Use Cases |
|---|---|---|---|---|
ASK_DATA | Quick SQL query + natural language response, supports follow-up questions | 30 sec - 2 min | ❌ | Simple queries, data validation |
ANALYSIS | Deep analysis, generates complete report | 5-15 min | ✅ | Topic analysis, business reports |
INSIGHT | Multi-dimensional, in-depth data insights | 20-40 min | ✅ | Strategic analysis, trend research |
⚠️ Important: When you need to generate reports (HTML/Markdown/charts), ANALYSIS mode is recommended.
dms Subcommand — DMS Tool Integration
Direct access to DMS metadata, used for discovering instances, databases, and tables.
Tool List
| Tool | Description |
|---|---|
list-instances | Search database instance list in DMS |
search-database | Search databases by schema name |
list-tables | List tables in specified database |
workspace Subcommand — List Workspaces
List Data Agent collaboration workspaces under the current account.
Parameters
| Parameter | Description |
|---|---|
--workspace-type | Workspace type: MY (default) / ALL |
--search / -s | Filter workspaces by name |
--page-number | Page number (default: 1) |
--page-size | Page size (default: 50) |
Examples
# List my workspaces
python3 scripts/data_agent_cli.py workspace
# List all accessible workspaces
python3 scripts/data_agent_cli.py workspace --workspace-type ALL
# Search by name
python3 scripts/data_agent_cli.py workspace --search myworkspaceagent — List and Describe Custom Agents
List available custom agents (default: RELEASED status only) or view agent details.
Usage:
data-agent agent # List RELEASED custom agents
data-agent agent list --search keyword # Search agents by keyword
data-agent agent describe --custom-agent-id <ID> # View agent detailsParameters:
| Parameter | Description | Required |
|---|---|---|
action | list (default) or describe | No |
--workspace-id | Filter agents by workspace ID | No |
--custom-agent-id | Custom Agent ID (required for describe) | Conditional |
--search | Search keyword to filter agents | No |
--page-number | Page number (default: 1) | No |
--page-size | Page size (default: 20) | No |
---
import Subcommand — Import DMS Database
Import DMS database tables into Data Agent Data Center.
Parameters
| Parameter | Description |
|---|---|
--dms-instance-id | Required, DMS Instance ID (numeric) |
--dms-db-id | Required, DMS Database ID (numeric) |
--instance-name | Required, RDS Instance Name |
--db-name | Required, Database Name |
--tables | Required, Table name list to import (comma-separated) |
--engine | Database engine type (default: mysql) |
--region | Region ID (default: cn-hangzhou) |
--yes, -y | Confirm the import operation without an interactive prompt |
---
attach Subcommand — Session Reuse
Connect to an existing session to continue conversation, confirm plans, or check progress.
Parameters
| Parameter | Description |
|---|---|
--session-id | Required, Session ID to connect to |
-q / --query | Send single query |
--from-start | Replay session history from beginning (equivalent to --checkpoint 0) |
--checkpoint | Specify exact checkpoint to resume from (e.g., --checkpoint 219), used for precise recovery after network interruption |
--output | summary (default) / detail / raw |
RAM Permission Policies
This Skill requires the following Alibaba Cloud RAM permissions:
Required Permissions (Recommended)
Use a custom RAM policy with only the actions required by this Skill:
{
"Version": "1",
"Statement": [
{
"Effect": "Allow",
"Action": [
"dms:ListInstances",
"dms:ListDatabases",
"dms:ListTables",
"dms:ListColumns",
"dms:GetInstance",
"dms:GetDatabase",
"dms:GetTableTopology",
"dms:GetMetaTableDetailInfo",
"dms:DescribeInstance",
"dms:CreateDataAgentSession",
"dms:DescribeDataAgentSession",
"dms:ListDataAgentSession",
"dms:SendChatMessage",
"dms:GetChatContent",
"dms:DescribeDataAgentUsage",
"dms:UpdateDataAgentSession",
"dms:CreateDataAgentFeedback",
"dms:DescribeFileUploadSignature",
"dms:FileUploadCallback",
"dms:ListFileUpload",
"dms:DeleteFileUpload",
"dms:ListTagMetaAsset",
"dms:InitDataAgentPersonalWorkspace",
"dms:ListDataCenterDatabase",
"dms:ListDataCenterTable",
"dms:AddDataCenterTable"
],
"Resource": "*"
}
]
}Optional Managed Policies
Managed full-access policies are broader than this Skill requires. Use them only for temporary testing or accounts that are already governed by separate access controls:
| Policy | Description |
|---|---|
AliyunDMSDataAgentFullAccess | Full access to Data Agent |
AliyunDMSFullAccess | Full access to DMS Data Management Service |
Configuration Instructions
1. Log in to Alibaba Cloud RAM Console 2. Create or select a user 3. Add the custom minimal permission policy to the user 4. Create AccessKey for Skill authentication
Typical Workflows
This document contains complete operational workflow examples.
---
Method 1: Start from Existing Database in Data Center (Recommended)
Prerequisite: The database must already exist in Data Agent Data Center.
>
Built-in Demo Database: internal_data_employees is DataAgent's built-in test database, containing employee, department, and salary data, suitable for first-time experience.Step 1 ls -- List available databases
Step 2 ls --db-id -- List tables for a specific database, and print db command ready to copy
Step 3 db -q -- Initiate query/analysis session, output progress and conclusions in real-time
Step 4 attach -- Connect to existing session (confirm plan / follow-up / view latest results)Complete Example
# Step 1: Discover databases
python3 scripts/data_agent_cli.py ls
# Step 2: View tables in built-in test database internal_data_employees and get command template
python3 scripts/data_agent_cli.py ls --db-id <AgentDbId>
# Step 3: Query (copy command from previous step, replace question)
python3 scripts/data_agent_cli.py db \
--dms-instance-id <DMS_INSTANCE_ID> --dms-db-id <DMS_DB_ID> \
--instance-name <INSTANCE_NAME> --db-name internal_data_employees \
--tables "employees,departments,salaries" \
--session-mode ASK_DATA \
-q "Which position has the highest salary"
# Step 4: Connect to existing session for follow-up questions
python3 scripts/data_agent_cli.py attach --session-id <SESSION_ID> -q "Calculate average salary by department"---
Method 2: Discover and Import from DMS Instance to Data Center
Use Case: When Data Agent Data Center doesn't have the database you need.
Step 1 dms list-instances -- Query database instances in DMS
Step 2 dms search-database -- Search databases in instance
Step 3 dms list-tables -- List tables in database
Step 4 ls -- Check if Data Center already has this database
Step 5 import -- Import DMS database tables to Data Center
Step 6 db -- Initiate query/analysis session
Step 7 attach -- Connect to existing sessionComplete Example
# 1. Query DMS instance list
python3 scripts/data_agent_cli.py dms list-instances
# 2. Search for target database (get Database ID)
python3 scripts/data_agent_cli.py dms search-database --search-key employees
# 3. View tables in database
python3 scripts/data_agent_cli.py dms list-tables --database-id <DATABASE_ID>
# 4. Check if Data Center already has this database
python3 scripts/data_agent_cli.py ls --search employees
# 5. Import to Data Center (required step)
python3 scripts/data_agent_cli.py import \
--dms-instance-id <DMS_INSTANCE_ID> \
--dms-db-id <DMS_DB_ID> \
--instance-name <INSTANCE_NAME> \
--db-name employees \
--tables "departments,employees,salaries" \
--yes
# 6. Initiate analysis
python3 scripts/data_agent_cli.py db \
--dms-instance-id <DMS_INSTANCE_ID> --dms-db-id <DMS_DB_ID> \
--instance-name <INSTANCE_NAME> --db-name employees \
--tables "departments,employees,salaries" \
-q "Query the department with highest average salary"---
Background Execution Best Practices
For long-running ls and db commands, running in background is recommended:
# Start ANALYSIS task in background
nohup python3 scripts/data_agent_cli.py db \
--dms-instance-id <DMS_INSTANCE_ID> --dms-db-id <DMS_DB_ID> \
--instance-name <INSTANCE_NAME> --db-name internal_data_employees \
--tables "employees,departments,salaries" \
--session-mode ANALYSIS \
-q "Analyze salary distribution and generate report" > analysis.log 2>&1 &
# Get session ID from log
grep "Session ready" analysis.log
# Attach anytime to check progress
python3 scripts/data_agent_cli.py attach --session-id <SESSION_ID>
# If network is interrupted or you want to resume from a specific state, specify checkpoint
python3 scripts/data_agent_cli.py attach --session-id <SESSION_ID> --checkpoint <CHECKPOINT_NUM>Benefits:
- Avoid task failure due to network interruption (seamless resume with
--checkpointparameter) - Check progress anytime via
attach - Output logs for later review
---
Session Reuse Workflow (⭐ Always prefer attach for follow-ups)
Core Principle: Create a session once withdb/file, then drive all subsequent interactions (follow-up, confirm, modify plan, monitor, recover) throughattach --session-id <ID>. Never create a new session for a question that belongs to the same data scope — you will lose context and pay the data-understanding cost again.
Typical attach scenarios
| Scenario | Command |
|---|---|
| Follow-up question with full context | attach --session-id <ID> -q "..." |
| Confirm ANALYSIS / INSIGHT plan | attach --session-id <ID> -q "confirm" |
| Modify / refine the plan | attach --session-id <ID> -q "simplify to 3 steps" |
| Tail live progress (no query) | attach --session-id <ID> |
| Replay from beginning | attach --session-id <ID> --from-start |
| Resume precisely after network drop | attach --session-id <ID> --checkpoint <N> |
End-to-end example
# Analysis 1: Create new session (async, returns Session ID immediately)
python3 scripts/data_agent_cli.py db \
--dms-instance-id <DMS_INSTANCE_ID> --dms-db-id <DMS_DB_ID> \
--instance-name <INSTANCE_NAME> --db-name internal_data_employees \
--tables "employees,departments" \
--session-mode ANALYSIS \
-q "Analyze 2024 salary growth trends"
# Output: ✅ Async task started. Session ID: abc123xyz
# Analysis 2: Reuse same session, follow-up with details
python3 scripts/data_agent_cli.py attach --session-id abc123xyz -q "Break down salary structure by job level"
# Analysis 3: Modify plan
python3 scripts/data_agent_cli.py attach --session-id abc123xyz -q "Simplify to 3 steps"
# Analysis 4: Confirm execution (ANALYSIS/INSIGHT plans are blocked until confirmed)
python3 scripts/data_agent_cli.py attach --session-id abc123xyz -q "confirm"
# Analysis 5: Recover precisely if the stream was cut (e.g., at event #219)
python3 scripts/data_agent_cli.py attach --session-id abc123xyz --checkpoint 219
# Step 6: Read final results
cat sessions/abc123xyz/progress.log
# Step 7: Download generated reports
python3 scripts/data_agent_cli.py reports --session-id abc123xyzBenefits of Reuse via `attach`:
- Preserve conversation context, SQL history, and data profiling — consistent follow-up answers
- Skip repeated data-understanding phase, reducing latency and API cost
- Plan governance: ANALYSIS / INSIGHT plans must be confirmed through
attach -q "confirm" - Resilience:
--checkpoint/--from-startguarantee safe recovery after network drops or client restarts - Collaboration: share the Session ID so teammates can
attachand watch progress live
See ANALYSIS_MODE.md for detailed sub-agent implementation specifications
"""CLI package for Data Agent unified command-line tool.
Author: Tinker
Created: 2026-03-04
"""
from cli.formatters import (
_fmt_jupyter_cell,
_fmt_task_finish,
_fmt_insights,
_fmt_table_summaries,
_extract_json_objects,
_format_data_event,
_format_parsed_json,
_fmt_plan_progress,
_fmt_status_change,
_fmt_output_conclusion,
_fmt_recommended_questions,
_SKIP_DATA_CATEGORIES,
)
from cli.streaming import (
StreamState,
_print_event,
_stream_response,
_finalize_stream,
_is_user_confirmation_event,
)
from cli.cmd_db import cmd_db, _build_data_source, _db_attach, _db_batch, _db_single
from cli.cmd_ls import cmd_ls, _extract_list
from cli.cmd_file import cmd_file, _print_generated_files
from cli.cmd_attach import cmd_attach
from cli.parser import build_parser, main
__all__ = [
# Formatters
"_fmt_jupyter_cell",
"_fmt_task_finish",
"_fmt_insights",
"_fmt_table_summaries",
"_extract_json_objects",
"_format_data_event",
"_format_parsed_json",
"_fmt_plan_progress",
"_fmt_status_change",
"_fmt_output_conclusion",
"_fmt_recommended_questions",
"_SKIP_DATA_CATEGORIES",
# Streaming
"StreamState",
"_print_event",
"_stream_response",
"_finalize_stream",
"_is_user_confirmation_event",
# Commands
"cmd_db",
"_build_data_source",
"_db_attach",
"_db_batch",
"_db_single",
"cmd_ls",
"_extract_list",
"cmd_file",
"_print_generated_files",
"cmd_attach",
# Parser / entry
"build_parser",
"main",
]
"""Custom Agent management subcommand (agent).
Author: Tinker
Created: 2026-04-16
"""
import argparse
import sys
from data_agent import DataAgentConfig, DataAgentClient
def _get_field(obj: dict, *names: str, default=""):
"""Get a field value trying multiple possible key names."""
for name in names:
if name in obj:
return obj[name]
return default
def _cmd_list(client: DataAgentClient, args: argparse.Namespace) -> None:
"""List custom agents."""
workspace_id = getattr(args, "workspace_id", None)
search = getattr(args, "search", None)
page_number = getattr(args, "page_number", 1)
page_size = getattr(args, "page_size", 20)
print(f"Region: {client.config.region}")
print(f"Fetching custom agents (status=RELEASED)...")
try:
resp = client.list_custom_agents(
workspace_id=workspace_id,
search_key=search,
page_number=page_number,
page_size=page_size,
)
except Exception as e:
print(f"Error: {e}", file=sys.stderr)
sys.exit(1)
# Extract agent list from response
data = resp.get("data") or resp.get("Data") or {}
items = []
if isinstance(data, dict):
items = data.get("content") or data.get("Content") or []
elif isinstance(data, list):
items = data
total_elements = 0
total_pages = 0
if isinstance(data, dict):
total_elements = data.get("totalElements") or data.get("TotalElements") or len(items)
total_pages = data.get("totalPages") or data.get("TotalPages") or 1
if not items:
print("No custom agents found.")
return
sep = "=" * 60
print(f"\n{sep}")
print(f" Custom Agents ({len(items)}) Total: {total_elements}")
print(f"{sep}")
for agent in items:
name = _get_field(agent, "agentName", "AgentName", "name", "Name")
agent_id = _get_field(agent, "customAgentId", "CustomAgentId")
status = _get_field(agent, "status", "Status", default="unknown")
ws_id = _get_field(agent, "workspaceId", "WorkspaceId")
creator = _get_field(agent, "creator", "Creator")
desc = _get_field(agent, "description", "Description")
print(f"\n {name} [{status}]")
print(f" CustomAgentId : {agent_id}")
if ws_id:
print(f" WorkspaceId : {ws_id}")
if creator:
print(f" Creator : {creator}")
if desc:
print(f" Description : {desc}")
if total_pages > 1:
print(f"\n Page {page_number}/{total_pages} (use --page-number to navigate)")
# Print usage hint
print(f"\n{'-' * 60}")
print(" Tip: 使用 db --custom-agent-id <ID> ... 来指定自定义Agent进行分析")
print(f"{'-' * 60}")
print()
def _cmd_describe(client: DataAgentClient, args: argparse.Namespace) -> None:
"""Describe a custom agent in detail."""
custom_agent_id = getattr(args, "custom_agent_id", None)
if not custom_agent_id:
print("Error: --custom-agent-id is required for describe action", file=sys.stderr)
sys.exit(1)
workspace_id = getattr(args, "workspace_id", None)
print(f"Region: {client.config.region}")
print(f"Fetching custom agent details: {custom_agent_id}...")
try:
resp = client.describe_custom_agent(
custom_agent_id=custom_agent_id,
workspace_id=workspace_id,
)
except Exception as e:
print(f"Error: {e}", file=sys.stderr)
sys.exit(1)
# Extract agent detail from response
data = resp.get("data") or resp.get("Data") or {}
if not data:
print("No agent details returned.")
return
name = _get_field(data, "agentName", "AgentName", "name", "Name")
agent_id = _get_field(data, "customAgentId", "CustomAgentId")
status = _get_field(data, "status", "Status", default="unknown")
ws_id = _get_field(data, "workspaceId", "WorkspaceId")
creator = _get_field(data, "creator", "Creator")
desc = _get_field(data, "description", "Description")
instruction = _get_field(data, "instruction", "Instruction")
knowledge = _get_field(data, "knowledge", "Knowledge")
sep = "=" * 60
print(f"\n{sep}")
print(f" Custom Agent Detail")
print(f"{sep}")
print(f" Name : {name}")
print(f" CustomAgentId : {agent_id}")
print(f" Status : {status}")
if ws_id:
print(f" WorkspaceId : {ws_id}")
if creator:
print(f" Creator : {creator}")
if desc:
print(f" Description : {desc}")
if instruction:
print(f" Instruction : {instruction}")
if knowledge:
print(f" Knowledge : {knowledge}")
# Print any additional fields not covered above
known_keys = {
"agentName", "AgentName", "name", "Name",
"customAgentId", "CustomAgentId",
"status", "Status",
"workspaceId", "WorkspaceId",
"creator", "Creator",
"description", "Description",
"instruction", "Instruction",
"knowledge", "Knowledge",
}
extra = {k: v for k, v in data.items() if k not in known_keys and v}
if extra:
print(f"\n -- Additional Fields --")
for k, v in extra.items():
print(f" {k:16s}: {v}")
print(f"\n{'-' * 60}")
print(f" Tip: db --custom-agent-id {agent_id} ... 来使用此Agent")
print(f"{'-' * 60}")
print()
def cmd_agent(args: argparse.Namespace) -> None:
"""Handle agent subcommand."""
config = DataAgentConfig.from_env()
client = DataAgentClient(config)
action = getattr(args, "action", "list")
if action == "describe":
_cmd_describe(client, args)
else:
_cmd_list(client, args)
"""Attach to existing session subcommand (attach).
Author: Tinker
Created: 2026-03-04
"""
import argparse
import json
import os
import subprocess
import sys
from pathlib import Path
from cli.streaming import _stream_response, StreamState, _print_event
from cli.cmd_db import _db_attach
from cli.log_handler import StructuredLogHandler
# from cli.notify import push_notification
from cli.worker_utils import is_worker_process, setup_async_worker, handle_worker_completion
from cli.streaming_utils import run_worker_with_handler, execute_single_query
from cli.worker_lock import check_worker_lock, write_worker_pid, acquire_worker_lock, release_worker_lock
from data_agent import (
DataAgentConfig,
DataAgentClient,
SessionManager,
MessageHandler,
FileManager,
SSEClient,
)
def cmd_attach(args: argparse.Namespace) -> None:
"""Connect to an existing session for continuing analysis or confirming plan."""
session_id = args.session_id
is_worker = os.environ.get("DATA_AGENT_ASYNC_WORKER") == "1"
async_run = getattr(args, "async_run", True)
# Initialize components
config = DataAgentConfig.from_env()
client = DataAgentClient(config)
session_manager = SessionManager(client)
message_handler = MessageHandler(client)
sse_client = SSEClient(config)
file_manager = FileManager(client)
# Async mode is determined by the --async-run/--no-async-run flag, regardless of query presence
if async_run and not is_worker:
# PARENT PROCESS: spawn background worker
print(f"Connecting to session: {session_id}")
try:
session = client.describe_session(session_id=session_id)
except Exception as e:
print(f"Error: Failed to connect to session: {e}", file=sys.stderr)
sys.exit(1)
# Manually create a temporary session object to pass to the common utility
class TempSession:
def __init__(self, sess_obj):
self.session_id = sess_obj.session_id
self.agent_id = sess_obj.agent_id
temp_session = TempSession(session)
# Use common async worker setup
setup_async_worker(args, temp_session)
elif is_worker:
# WORKER PROCESS: run the attach operation in background - using common utility
def attach_query_executor(message_handler, session, args):
output_mode = getattr(args, "output", "summary")
session_dir = Path(f"sessions/{session.session_id}")
query = args.query
if query:
print(f"\n> User Query: {query}\n", flush=True)
got_content, need_confirm, output_text = _stream_response(
message_handler, session, query,
output_mode=output_mode, output_dir=session_dir,
process_log_handler=None, # In worker process, no process log handler needed for this path
is_attach=True # Indicate this is an attach operation
)
# Check if the query was for confirmation and if there are more steps to process
is_confirmation_query = query.strip() in ["确认执行", "confirm", "execute", "同意后续所有SQL执行"]
# If the query was a confirmation, there might be more steps to execute
if is_confirmation_query:
# After confirmation, the process has handled the confirmation
# The subsequent steps will be tracked by the server and can be viewed
# by attaching to the session again
print(f"\n[Worker] Confirmation processed. Subsequent steps will continue in the background.", flush=True)
# Determine final status
final_status = "waiting_input" if need_confirm else "completed"
with open(session_dir / "status.txt", "w", encoding="utf-8") as f:
f.write(final_status)
with open(session_dir / "result.json", "w", encoding="utf-8") as f:
json.dump({
"status": final_status,
"session_id": session.session_id,
}, f, ensure_ascii=False, indent=2)
return got_content, need_confirm
else:
# For attach without query, just monitor or replay
from_start = getattr(args, "from_start", False)
checkpoint = getattr(args, "checkpoint", None)
# In worker process, we need to initialize components locally
_config = DataAgentConfig.from_env()
_client = DataAgentClient(_config)
_sse_client = SSEClient(_config)
_file_manager = FileManager(_client)
_db_attach(_sse_client, _file_manager, session, from_start=from_start, checkpoint=checkpoint, output_mode=output_mode)
return True, False # got_content=True, need_confirm=False
# Since the attach worker has complex setup logic that differs significantly from the other commands,
# we'll just call the run_worker_with_handler without a data_source
run_worker_with_handler(args, query_execution_func=attach_query_executor)
else:
# SYNCHRONOUS MODE (--no-async-run)
print(f"Connecting to session: {session_id}")
print(f" Region: {config.region}")
try:
# First, try to get the session info to discover the actual agent_id
# For some sessions, the API may not return the agent_id if an empty agent_id is provided
# We need to try with empty agent_id first, and if it fails, we'll try the create_or_reuse approach
session = None
try:
session = client.describe_session(agent_id="", session_id=session_id)
# Special case: If we get CREATING status with no agent ID, this means the session doesn't actually exist
# for the current user, but the API returns a default response. We should exit immediately.
if not session.agent_id and session.status.value == "CREATING":
print(f"Error: Session {session_id} does not exist or does not belong to the current user.", file=sys.stderr)
sys.exit(1)
# Only print session info if it passes the above check
print(f"Initial session check - agent: '{session.agent_id}', status: {session.status.value}")
except Exception as e:
# This could be an authentication error or the session doesn't exist
print(f"Error connecting to session: {e}", file=sys.stderr)
print(f"This could be due to invalid credentials or the session doesn't exist.", file=sys.stderr)
# If it's an authentication error, we should exit immediately
from data_agent.exceptions import AuthenticationError
if isinstance(e, AuthenticationError):
print(f"Authentication failed. Please check your credentials in .env file.", file=sys.stderr)
sys.exit(1)
# For other errors, continue with fallback approaches
print(f"Trying alternative methods to connect to session...", file=sys.stderr)
# If session retrieval failed or agent_id is still empty, try using session manager
if not session or not session.agent_id:
print("Agent ID not found in session info, this may indicate the session doesn't exist or isn't ready yet.")
# Check if the session exists but is still in CREATING state
if session and session.status.value == "CREATING":
print("Session exists but is still being created. Waiting for it to be ready...")
# Since session is in CREATING state but no agent_id was returned,
# we need to wait until the agent_id becomes available
import time
max_retries = 10 # Reduce wait time (10*30 = 5 minutes max)
retry_count = 0
while retry_count < max_retries:
time.sleep(30) # Wait 30 seconds between checks
try:
session = client.describe_session(agent_id="", session_id=session_id)
print(f"Checking session again - agent: '{session.agent_id}', status: {session.status.value}")
# If we now have an agent_id, break out of the loop
if session.agent_id:
print(f"Got agent ID: {session.agent_id}. Session is ready.")
break
# If status is no longer CREATING but we still don't have an agent_id,
# the session may have failed to create properly or doesn't belong to the user
if session.status.value != "CREATING":
print(f"Session status changed to {session.status.value} but still no agent ID.")
print(f"This may indicate the session does not belong to the current user.", file=sys.stderr)
break
except Exception as api_error:
print(f"Check session retry failed: {api_error}", file=sys.stderr)
from data_agent.exceptions import AuthenticationError
if isinstance(api_error, AuthenticationError):
print(f"Authentication failed. Please check your credentials in .env file.", file=sys.stderr)
sys.exit(1)
retry_count += 1
if not session.agent_id:
print(f"Session is still not ready after waiting. Status: {session.status.value if session else 'unknown'}", file=sys.stderr)
print(f"If status is not CREATING, this may mean the session does not belong to your account.", file=sys.stderr)
if not session or not session.agent_id:
# At this point, if we still don't have an agent_id, the session either:
# 1. Does not exist
# 2. Does not belong to the current user
# 3. Is still being created but taking too long
# 4. Has been cancelled or failed to create properly
print(f"Error: Could not retrieve agent ID for session {session_id}.", file=sys.stderr)
print(f"This may mean the session does not exist, does not belong to your account,", file=sys.stderr)
print(f"is still being created (waited too long), or was cancelled/failed to create.", file=sys.stderr)
sys.exit(1)
rid = f", request_id: {session.request_id}" if session.request_id else ""
print(f"Session connected: {session.session_id} (agent: {session.agent_id}, status: {session.status.value}{rid})")
# If status is CREATING and a query is provided, we need to wait for it to be ready
# But for IDLE state, we can proceed without waiting since it's a valid state
if session.status.value == "CREATING" and getattr(args, "query", None):
print("Waiting for session to be ready...")
session = session_manager.create_or_reuse(
session_id=session_id, agent_id=session.agent_id, wait_for_running=True,
)
rid = f", request_id: {session.request_id}" if session.request_id else ""
print(f"Session ready: {session.session_id} (status: {session.status.value}{rid})")
except Exception as e:
print(f"Error: Failed to connect to session: {e}", file=sys.stderr)
sys.exit(1)
output_mode = getattr(args, "output", "summary")
session_dir = Path(f"sessions/{session.session_id}")
# Create process log handler for attach command
with StructuredLogHandler(session_dir, log_prefix="process") as log_handler:
# Log initial connection info
log_text = f"Connected to session: {session.session_id} (agent: {session.agent_id}, status: {session.status.value})"
log_handler.write_both(log_text + "\n")
if args.query:
query = args.query
print(f"\n> User Query: {query}\n")
# Log the query
query_log = f"Processing query: {query}\n"
log_handler.write_both(query_log)
try:
got_content, need_confirm, _ = _stream_response(
message_handler, session, query,
output_mode=output_mode, output_dir=session_dir,
process_log_handler=log_handler, # Pass the process log handler
is_attach=True # Indicate this is an attach operation
)
# Check if the query was for confirmation and if there are more steps to process
is_confirmation_query = query.strip() in ["确认执行", "confirm", "execute", "同意后续所有SQL执行"]
# Log the response completion
response_log = f"Query processed, got_content: {got_content}, need_confirm: {need_confirm}, is_confirmation: {is_confirmation_query}\n"
log_handler.write_both(response_log)
if not got_content:
print("(No response received, please retry)")
elif need_confirm:
print("\n⚠️ Agent has created an execution plan. User confirmation required.")
print(f" To continue: python3 scripts/data_agent_cli.py attach --session-id {session.session_id} -q 'your input'")
elif is_confirmation_query:
# After confirmation, continue monitoring for additional steps
print(f"\n[Sync mode] Post-confirmation: Monitoring for additional steps...")
# Refresh session to get the latest status
updated_session = client.describe_session(agent_id=session.agent_id, session_id=session.session_id)
# If session is still running after confirmation, continue monitoring
if updated_session.status.value in ["RUNNING", "WAIT_INPUT"]:
print(f"Session status after confirmation: {updated_session.status.value}")
# Continue to attach and monitor the session
print(f"\nContinuing to monitor session progress...")
# Use already imported _db_attach and initialized clients
_db_attach(sse_client, file_manager, updated_session, from_start=False, checkpoint=None, output_mode=output_mode)
else:
try:
updated_session = client.describe_session(agent_id=session.agent_id, session_id=session.session_id)
if updated_session.status.value == "WAIT_INPUT":
print("\n⚠️ Agent has created an execution plan and is waiting for confirmation.")
print(" Use -q option to confirm the plan or provide feedback:")
print(f" python3 scripts/data_agent_cli.py attach --session-id {session.session_id} -q 'confirm'")
print(f" python3 scripts/data_agent_cli.py attach --session-id {session.session_id} -q 'modify the plan'")
except Exception:
pass
except Exception as e:
error_msg = f"Request failed: {e}"
print(error_msg)
log_handler.write_both(error_msg + "\n")
else:
from_start = getattr(args, "from_start", False)
checkpoint = getattr(args, "checkpoint", None)
# Log the attachment without query
attach_log = f"Attaching to session without query (from_start: {from_start}, checkpoint: {checkpoint})\n"
log_handler.write_both(attach_log)
# Additional verification: if session agent_id is still empty, try to retrieve it directly
if not session.agent_id:
print(f"No agent ID in current session object, attempting to refresh session info...", file=sys.stderr)
try:
# Try to get fresh session data directly from the API
refreshed_session = client.describe_session(agent_id="", session_id=session_id)
if refreshed_session.agent_id:
session = refreshed_session
print(f"Successfully retrieved agent ID: {refreshed_session.agent_id}")
else:
print(f"Error: Could not retrieve agent ID for session {session_id}. Session may not exist.", file=sys.stderr)
sys.exit(1)
except Exception as e:
print(f"Error retrieving session info: {e}", file=sys.stderr)
sys.exit(1)
# Final check before calling _db_attach
if not session.agent_id:
print(f"Error: Cannot attach to session {session_id} without a valid agent ID.", file=sys.stderr)
print(f"The session might not exist or may be corrupted.", file=sys.stderr)
sys.exit(1)
_db_attach(sse_client, file_manager, session, from_start=from_start, checkpoint=checkpoint, output_mode=output_mode)
"""Database analysis subcommand (db).
Author: Tinker
Created: 2026-03-04
"""
import argparse
import sys
import os
import json
import subprocess
from pathlib import Path
from typing import Optional
# from cli.notify import push_notification
from cli.worker_lock import check_worker_lock, write_worker_pid, acquire_worker_lock, release_worker_lock
from cli.worker_utils import is_worker_process, setup_async_worker, handle_worker_completion
from cli.streaming_utils import run_worker_with_handler, execute_single_query, execute_query_batch
from cli.streaming import _stream_response, _finalize_stream, StreamState, _print_event
from data_agent import (
DataAgentConfig,
DataAgentClient,
SessionManager,
MessageHandler,
FileManager,
DataSource,
SSEClient,
)
def _build_data_source(args: argparse.Namespace) -> DataSource:
"""Build DataSource from command-line arguments."""
tables = [t.strip() for t in args.tables.split(",")] if args.tables else []
table_ids = [t.strip() for t in args.table_ids.split(",")] if args.table_ids else []
return DataSource(
dms_instance_id=getattr(args, 'dms_instance_id', None),
dms_database_id=args.dms_db_id,
instance_name=getattr(args, 'instance_name', None) or "",
db_name=args.db_name,
tables=tables,
table_ids=table_ids,
engine=args.engine,
region_id=args.region,
)
def _db_attach(
sse_client: SSEClient,
file_manager: FileManager,
session,
from_start: bool = False,
checkpoint: Optional[int] = None,
output_mode: str = "summary",
) -> None:
"""Attach to an existing session's SSE stream without sending a message.
Streams all incoming events to stdout in real-time using the same
formatting as _stream_response. Useful for:
- Watching an ongoing analysis
- Replaying the last round to review a plan before confirming
After the stream ends, calls ListFileUpload to download any
agent-generated report files to sessions/<session_id>/reports/.
"""
session_id = session.session_id
session_dir = Path(f"sessions/{session_id}")
if checkpoint is None:
checkpoint = 0 if from_start else None
label = "watching live stream"
if from_start:
label = "replaying from start"
elif checkpoint is not None:
label = f"resuming from checkpoint {checkpoint}"
print(f"\nAttaching to session {session_id} ({label})...")
print(f"Session status: {session.status.value}")
# Special reminder for WAIT_INPUT status
if session.status.value == "WAIT_INPUT":
print("\n⚠️ Session is in WAIT_INPUT state.")
print(" The agent has generated SQL and is waiting for your confirmation.")
print()
print(" To view the SQL and confirm:")
print(f" python3 skill/data_agent_cli.py attach --session-id {session_id} --from-start")
print()
print(" To confirm and execute ONLY this SQL (DO NOT create a new session):")
print(f" python3 skill/data_agent_cli.py attach --session-id {session_id} -q '确认执行当前SQL'")
print()
print(" To agree to execute all subsequent SQL automatically:")
print(f" python3 skill/data_agent_cli.py attach --session-id {session_id} -q '同意后续所有SQL执行'")
print()
print(" To modify the query:")
print(f" python3 skill/data_agent_cli.py attach --session-id {session_id} -q 'your new question'")
print(f"Output directory: {session_dir.resolve()}")
print("Press Ctrl+C to detach.")
print("\n---\n")
got_content = False
last_checkpoint = checkpoint if checkpoint else 0
last_progress_time = 0
import time
state = StreamState(output_mode=output_mode)
state.output_dir = session_dir
state.session_id = session_id
state.is_attach = True
state.session_status = getattr(session, 'status', None)
if hasattr(state.session_status, 'value'):
state.session_status = state.session_status.value
try:
# Initialize structured logging for attach mode
from cli.streaming import init_structured_logging, close_structured_logging
init_structured_logging(session_dir)
for event in sse_client.stream_chat_content(
agent_id=session.agent_id,
session_id=session_id,
checkpoint=checkpoint,
):
if event.event_type == "SSE_FINISH":
break
# Track checkpoint progress (only in detail/raw mode)
if output_mode != "summary" and event.checkpoint is not None and event.checkpoint > last_checkpoint:
current_time = time.time()
# Show checkpoint progress every 30 seconds or every 50 checkpoints
if current_time - last_progress_time >= 30 or event.checkpoint - last_checkpoint >= 50:
print(f" [checkpoint: {event.checkpoint}]", flush=True)
last_progress_time = current_time
last_checkpoint = event.checkpoint
c, _ = _print_event(event, output_mode, state=state)
if c:
got_content = True
except KeyboardInterrupt:
print("\n\nDetached.")
return
except Exception as e:
# Extract request_id from HTTP error response if available
request_id = ""
resp = getattr(e, "response", None)
if resp is not None:
request_id = resp.headers.get("x-acs-request-id", "")
rid_str = f" (Request-Id: {request_id})" if request_id else ""
print(f"\nError: {e}{rid_str}", file=sys.stderr)
return
finally:
# Close structured logging
close_structured_logging()
_finalize_stream(state)
if state.got_content:
got_content = True
if got_content:
print() # trailing newline
else:
print("(No new events -- session may be waiting for your input)")
# -- Download agent-generated files via ListFileUpload --
# Server needs a few seconds to finalize report files after session completes
print("\nFetching agent-generated files (waiting for server to finalize)...")
time.sleep(5)
report_dir = session_dir / "reports"
total_reports = 0
for category in ("WebReport", "TextReport", "DefaultArtifact"):
try:
files = file_manager.list_files(session_id, file_category=category)
except Exception as e:
print(f" Warning: could not list {category}: {e}", file=sys.stderr)
continue
if not files:
continue
report_dir.mkdir(parents=True, exist_ok=True)
for rf in files:
if not rf.download_url:
print(f" Skipping {rf.filename} ({category}): no download URL")
continue
save_path = report_dir / (rf.filename or f"{rf.file_id}.bin")
try:
file_manager.download_from_url(rf.download_url, str(save_path))
print(f" [{category}] saved \u2192 {save_path.resolve()}")
total_reports += 1
except Exception as e:
print(f" Failed to download {rf.filename} ({category}): {e}", file=sys.stderr)
if total_reports == 0:
print(" No report files found for this session.")
print("\n---\n")
print(f'> \U0001f4a1 To continue conversation:')
print(f'> python3 skill/data_agent_cli.py attach --session-id {session_id} -q "your message"')
def _db_batch(
message_handler: MessageHandler,
session,
data_source: DataSource,
queries: list,
output_mode: str = "summary",
output_dir: Optional[Path] = None,
) -> tuple[bool, bool, str]:
"""Execute batch preset queries."""
got_content, need_confirm = False, False
full_text = ""
for query in queries:
print(f"\n{'=' * 60}")
print(f"Query: {query}")
print("=" * 60)
c, nc, t = _stream_response(message_handler, session, query, data_source=data_source, output_mode=output_mode, output_dir=output_dir)
if c: got_content = True
if t: full_text += f"\n### Query: {query}\n" + t + "\n"
if nc:
need_confirm = True
break
return got_content, need_confirm, full_text
def _db_single(
message_handler: MessageHandler,
session,
data_source: DataSource,
query: str,
output_mode: str = "summary",
output_dir: Optional[Path] = None,
) -> tuple[bool, bool, str]:
"""Execute a single query with streaming output."""
print(f"\nAnalyzing...\n")
got_content, need_confirm, full_text = _stream_response(
message_handler, session, query, data_source=data_source, output_mode=output_mode, output_dir=output_dir
)
if not got_content:
print("(No response received, please retry)")
elif need_confirm:
print("\n\u26a0\ufe0f \u9700\u8981\u7528\u6237\u786e\u8ba4\uff0c\u7a0b\u5e8f\u5c06\u9000\u51fa\u3002\u8bf7\u5b8c\u6210\u786e\u8ba4\u540e\u4f7f\u7528\u4f1a\u8bddID\u7ee7\u7eed\u5bf9\u8bdd\u3002")
return got_content, need_confirm, full_text
def cmd_db(args: argparse.Namespace) -> None:
"""Handle db subcommand."""
# Validate required database parameters
missing = []
for attr, name in [
("dms_db_id", "--dms-db-id"),
("db_name", "--db-name"),
("tables", "--tables"),
]:
if not getattr(args, attr, None):
missing.append(name)
if missing:
print(f"Error: Missing required parameters: {', '.join(missing)}", file=sys.stderr)
sys.exit(1)
# Initialize components
config = DataAgentConfig.from_env()
client = DataAgentClient(config)
session_manager = SessionManager(client)
message_handler = MessageHandler(client)
# Create new session
session_mode = args.session_mode.upper()
data_source = _build_data_source(args)
is_worker = is_worker_process()
if getattr(args, "async_run", False) and not is_worker:
# PARENT PROCESS LOGIC
enable_search = getattr(args, 'enable_search', False)
print(f"Creating session for async execution...")
workspace_id = getattr(args, 'workspace_id', None)
custom_agent_id = getattr(args, 'custom_agent_id', None)
session = session_manager.create_or_reuse(mode=session_mode, database_id=str(args.dms_db_id), enable_search=enable_search, workspace_id=workspace_id, custom_agent_id=custom_agent_id)
# Use common async worker setup
setup_async_worker(args, session)
elif is_worker:
# WORKER PROCESS LOGIC - using common utility
def db_query_executor(message_handler, session, args):
output_mode = getattr(args, "output", "summary")
data_source = _build_data_source(args)
if args.query:
_, need_confirm, _ = _db_single(message_handler, session, data_source, args.query, output_mode=output_mode, output_dir=Path(f"sessions/{session.session_id}"))
else:
if session_mode == "ANALYSIS":
default_queries = [
f"Analyze the overall data structure and table relationships of {data_source.db_name} database",
"Identify key metrics and distribution characteristics in the data",
]
else:
default_queries = [
f"What tables exist in {data_source.db_name} database?",
"Who has the highest sales?",
]
_, need_confirm, _ = _db_batch(message_handler, session, data_source, default_queries, output_mode=output_mode, output_dir=Path(f"sessions/{session.session_id}"))
return True, need_confirm # got_content=True, need_confirm
run_worker_with_handler(args, data_source=_build_data_source(args), query_execution_func=db_query_executor)
# NORMAL SYNCHRONOUS LOGIC
mode_desc = {
"ASK_DATA": "ASK_DATA mode (SQL query + natural language response)",
"ANALYSIS": "ANALYSIS mode (deep analysis + report generation)",
"INSIGHT": "INSIGHT mode",
"CLAW": "CLAW mode (agentic)",
}.get(session_mode, session_mode)
print(f"Creating session: {mode_desc}...")
print(f" Region: {config.region}")
enable_search = getattr(args, 'enable_search', False)
workspace_id = getattr(args, 'workspace_id', None)
custom_agent_id = getattr(args, 'custom_agent_id', None)
session = session_manager.create_or_reuse(mode=session_mode, database_id=str(args.dms_db_id), enable_search=enable_search, workspace_id=workspace_id, custom_agent_id=custom_agent_id)
print(f"Session ready: {session.session_id}")
print(f"\n💡 Tip: To continue this session later, use: python3 scripts/data_agent_cli.py attach --session-id {session.session_id}")
# Execute query
output_mode = getattr(args, "output", "summary")
session_dir = Path(f"sessions/{session.session_id}")
session_dir.mkdir(parents=True, exist_ok=True)
# Initialize structured logging for sync mode
from cli.streaming import init_structured_logging, close_structured_logging
init_structured_logging(session_dir)
try:
if args.query:
_, _, output_text = _db_single(message_handler, session, data_source, args.query, output_mode=output_mode, output_dir=session_dir)
else:
# Default batch preset queries
if session_mode == "ANALYSIS":
default_queries = [
f"Analyze the overall data structure and table relationships of {data_source.db_name} database",
"Identify key metrics and distribution characteristics in the data",
]
else:
default_queries = [
f"What tables exist in {data_source.db_name} database?",
"Who has the highest sales?",
]
print(f"\nNo query specified, running preset queries ({len(default_queries)} total)...")
_, _, output_text = _db_batch(message_handler, session, data_source, default_queries, output_mode=output_mode, output_dir=session_dir)
finally:
close_structured_logging()
# Write result status
# if output_text:
# with open(session_dir / "output.md", "w", encoding="utf-8") as f:
# f.write(output_text)
# with open(session_dir / "result.json", "w", encoding="utf-8") as f:
# json.dump({"status": "completed", "output_file": "output.md"}, f)
with open(session_dir / "result.json", "w", encoding="utf-8") as f:
json.dump({"status": "completed"}, f)
"""DMS tools subcommand (dms).
Author: Tinker
Created: 2026-03-05
"""
import argparse
import sys
from data_agent import DataAgentConfig, DmsMcpTools
def cmd_dms(args: argparse.Namespace) -> None:
"""Handle dms subcommand for DMS tools."""
config = DataAgentConfig.from_env()
mcp_tools = DmsMcpTools(config)
tool = getattr(args, "tool", None)
if tool == "list-instances":
_list_instances(mcp_tools, args)
elif tool == "search-database":
_search_database(mcp_tools, args)
elif tool == "list-tables":
_list_tables(mcp_tools, args)
else:
print("Error: Unknown tool. Use 'list-instances', 'search-database', or 'list-tables'.", file=sys.stderr)
sys.exit(1)
def _list_instances(mcp_tools: DmsMcpTools, args: argparse.Namespace) -> None:
"""List DMS instances using MCP tool."""
search_key = getattr(args, "search", None)
db_type = getattr(args, "db_type", None)
env_type = getattr(args, "env_type", None)
page_number = getattr(args, "page_number", 1)
page_size = getattr(args, "page_size", 50)
print(f"Region: {mcp_tools._config.region}")
print("Fetching instances from DMS...")
print("-" * 60)
try:
result = mcp_tools.list_instances(
search_key=search_key,
db_type=db_type,
env_type=env_type,
page_number=page_number,
page_size=page_size,
)
if not result.items:
print("No instances found.")
return
print(f"Page {result.page_number}/{result.total_pages} (Total: {result.total_count} instances)\n")
for inst in result.items:
print(f" {inst.instance_alias} [{inst.instance_type}]")
print(f" Instance ID : {inst.instance_id}")
print(f" Host:Port : {inst.host}:{inst.port}")
print(f" State : {inst.state}")
print(f" Env Type : {inst.env_type}")
print(f" Source : {inst.instance_source}")
if inst.instance_resource_id:
print(f" Resource ID : {inst.instance_resource_id}")
print()
# Show pagination hint
print("-" * 60)
if result.has_next:
print(f"📄 Next page: --page-number {result.page_number + 1}")
else:
print("📄 This is the last page.")
except Exception as e:
print(f"Error: {e}", file=sys.stderr)
sys.exit(1)
def _search_database(mcp_tools: DmsMcpTools, args: argparse.Namespace) -> None:
"""Search databases by schema name."""
search_key = getattr(args, "search_key", None)
page_number = getattr(args, "page_number", 1)
page_size = getattr(args, "page_size", 200)
if not search_key:
print("Error: --search-key is required for search-database tool.", file=sys.stderr)
sys.exit(1)
print(f"Region: {mcp_tools._config.region}")
print(f"Search Key: {search_key}")
print("Searching databases in DMS...")
print("-" * 60)
try:
databases = mcp_tools.search_database(
search_key=search_key,
page_number=page_number,
page_size=page_size,
)
if not databases:
print("No databases found.")
print("\nTroubleshooting tips:")
print(" 1. The database may not be registered in DMS")
print(" 2. The search_key is case-sensitive, try different cases")
print(" 3. Try using 'dms list-instances' to see available instances first")
return
print(f"Found {len(databases)} database(s):\n")
for db in databases:
print(f" {db.schema_name} [{db.db_type}]")
print(f" Database ID : {db.database_id}")
print(f" Host:Port : {db.host}:{db.port}")
print(f" Instance : {db.instance_alias} (ID: {db.instance_id})")
print(f" Env Type : {db.env_type}")
print()
except Exception as e:
print(f"Error: {e}", file=sys.stderr)
sys.exit(1)
def _list_tables(mcp_tools: DmsMcpTools, args: argparse.Namespace) -> None:
"""List tables in a database."""
database_id = getattr(args, "database_id", None)
search_name = getattr(args, "search_name", None)
page_number = getattr(args, "page_number", 1)
page_size = getattr(args, "page_size", 200)
if not database_id:
print("Error: --database-id is required for list-tables tool.", file=sys.stderr)
sys.exit(1)
print(f"Region: {mcp_tools._config.region}")
print(f"Database ID: {database_id}")
if search_name:
print(f"Search Name: {search_name}")
print("Fetching tables from DMS...")
print("-" * 60)
try:
tables = mcp_tools.list_tables(
database_id=database_id,
search_name=search_name,
page_number=page_number,
page_size=page_size,
)
if not tables:
print("No tables found.")
return
print(f"Found {len(tables)} table(s):\n")
for table in tables:
print(f" {table.table_name}")
print(f" Table ID : {table.table_id}")
print(f" Table GUID : {table.table_guid}")
print(f" Schema : {table.schema_name}")
if table.engine:
print(f" Engine : {table.engine}")
if table.table_comment:
print(f" Comment : {table.table_comment}")
print()
except Exception as e:
print(f"Error: {e}", file=sys.stderr)
sys.exit(1)
"""File analysis subcommand (file).
Author: Tinker
Created: 2026-03-04
"""
import argparse
import sys
import os
import json
import subprocess
from pathlib import Path
# from cli.notify import push_notification
from cli.worker_utils import is_worker_process, setup_async_worker, handle_worker_completion
from cli.streaming_utils import run_worker_with_handler, execute_single_query, execute_query_batch
from cli.worker_lock import check_worker_lock, write_worker_pid, acquire_worker_lock, release_worker_lock
from cli.dual_logger import run_with_dual_logging
from cli.streaming import _stream_response
from data_agent import (
DataAgentConfig,
DataAgentClient,
SessionManager,
MessageHandler,
FileManager,
DataSource,
)
def cmd_file(args: argparse.Namespace) -> None:
"""Handle file subcommand."""
file_id = getattr(args, 'file_id', None)
file_path = args.file_path
# Validate arguments
if not file_id and not file_path:
print("Error: Either FILE path or --file-id must be provided", file=sys.stderr)
sys.exit(1)
if file_id and file_path:
print("Error: Cannot specify both FILE path and --file-id", file=sys.stderr)
sys.exit(1)
# Initialize components
config = DataAgentConfig.from_env()
client = DataAgentClient(config)
session_manager = SessionManager(client)
message_handler = MessageHandler(client)
file_manager = FileManager(client)
is_worker = is_worker_process()
session_mode = args.session_mode.upper()
if getattr(args, "async_run", False) and not is_worker:
# PARENT PROCESS LOGIC
enable_search = getattr(args, 'enable_search', False)
print(f"Creating session for async file analysis...")
if file_id:
# Using Data Center file ID
print(f"Using Data Center file ID: {file_id}")
# Create data source with file ID for query execution
file_data_source = DataSource(
data_source_type="FILE",
file_id=file_id,
region_id=config.region
)
# Create regular session without binding to specific data source initially
workspace_id = getattr(args, 'workspace_id', None)
custom_agent_id = getattr(args, 'custom_agent_id', None)
session = session_manager.create_or_reuse(mode=session_mode, enable_search=enable_search, file_id=file_id, workspace_id=workspace_id, custom_agent_id=custom_agent_id)
else:
# Local file workflow: upload first
if not Path(file_path).exists():
print(f"Error: File not found: {file_path}", file=sys.stderr)
sys.exit(1)
# Validate file type
if not file_manager.is_supported_file(file_path):
supported = ".csv, .xlsx, .xls"
print(f"Error: Unsupported file type. Supported formats: {supported}", file=sys.stderr)
sys.exit(1)
# Upload file and get file info
print(f"Uploading file: {file_path}")
try:
file_info = file_manager.upload_file(file_path)
print(f"File uploaded successfully!")
print(f" File ID : {file_info.file_id}")
print(f" Filename: {file_info.filename}")
print(f" Size : {file_info.size} bytes")
# Create data source with uploaded file ID
file_data_source = DataSource(
data_source_type="FILE",
file_id=file_info.file_id,
)
except Exception as e:
print(f"Upload failed: {e}", file=sys.stderr)
sys.exit(1)
# Create session
workspace_id = getattr(args, 'workspace_id', None)
custom_agent_id = getattr(args, 'custom_agent_id', None)
session = session_manager.create_or_reuse(mode=session_mode, enable_search=enable_search, file_id=file_info.file_id, workspace_id=workspace_id, custom_agent_id=custom_agent_id)
# Store file_data_source as a dict for async worker to use (avoid serialization issues)
args.file_data_source = file_data_source.to_api_dict()
# Use common async worker setup
setup_async_worker(args, session)
print(f"\n✅ Async task started. Session ID: {session.session_id}")
print(f"Check progress at: sessions/{session.session_id}/progress.log")
sys.exit(0)
elif is_worker:
# WORKER PROCESS LOGIC - using common utility
def file_query_executor(message_handler, session, args):
if hasattr(args, 'file_data_source'):
# If file_data_source is a dict (async mode), reconstruct DataSource
if isinstance(args.file_data_source, dict):
# Create a new DataSource from the dict representation
# For file analysis, we need to use data_source_type="FILE" and the correct file_id
# The DataSource.to_api_dict() method expects data_source_type="FILE" to trigger FileId inclusion
file_data_source = DataSource(
data_source_type="FILE", # Use "FILE" type for file analysis
file_id=args.file_data_source.get('FileId', ''),
region_id=args.file_data_source.get('RegionId', config.region)
)
else:
# If file_data_source is already a DataSource object (sync mode)
file_data_source = args.file_data_source
else:
# Try to load from input.json if not available in args
# In worker process, we can determine the session dir and load input.json
import json
from pathlib import Path
session_dir = Path(f"sessions/{session.session_id}")
input_file = session_dir / "input.json"
if input_file.exists():
try:
with open(input_file, 'r', encoding='utf-8') as f:
saved_args = json.load(f)
if 'file_data_source' in saved_args:
file_data_source_dict = saved_args['file_data_source']
# Create DataSource for file analysis using data_source_type="FILE"
file_data_source = DataSource(
data_source_type="FILE", # Use "FILE" type for file analysis
file_id=file_data_source_dict.get('FileId', ''),
region_id=file_data_source_dict.get('RegionId', config.region)
)
except Exception as e:
print(f"Warning: Could not load file_data_source from input.json: {e}", file=sys.stderr)
if file_data_source is None:
# Fallback: reconstruct based on available info
if hasattr(args, 'file_id') and args.file_id:
file_data_source = DataSource(
data_source_type="FILE",
file_id=args.file_id,
region_id=getattr(args, 'region_id', 'cn-hangzhou')
)
else:
print("Error: No file data source available in worker", file=sys.stderr)
return False, False
output_mode = getattr(args, "output", "summary")
session_dir = Path(f"sessions/{session.session_id}")
if args.query:
queries = [args.query]
else:
queries = [
"请分析上传文件的数据结构",
"数据的关键统计指标和分布情况是什么?",
"数据中是否存在异常值或离群点?",
]
print(f"\n{'=' * 60}")
print("File Analysis")
print("=" * 60)
output_text = ""
for query in queries:
print(f"\nQuery: {query}")
print("-" * 50)
got_content, need_confirm, t = _stream_response(
message_handler, session, query,
data_source=file_data_source, output_mode=output_mode,
output_dir=session_dir,
)
if t:
output_text += f"\n### Query: {query}\n" + t + "\n"
if need_confirm:
return got_content, need_confirm # got_content, need_confirm
if args.list_generated_files:
_print_generated_files(file_manager, session.session_id)
return True, False # got_content=True, need_confirm=False
# Pass args as-is since we've attached file_data_source to it
run_worker_with_handler(args, query_execution_func=file_query_executor)
# NORMAL SYNCHRONOUS LOGIC
if file_id:
# Using Data Center file ID
print(f"Using Data Center file ID: {file_id}")
# Create data source with file ID
file_data_source = DataSource(
data_source_type="FILE",
file_id=file_id,
region_id=config.region
)
else:
# Local file workflow: validate and upload
if not Path(file_path).exists():
print(f"Error: File not found: {file_path}", file=sys.stderr)
sys.exit(1)
# Validate file type
if not file_manager.is_supported_file(file_path):
supported = ".csv, .xlsx, .xls"
print(f"Error: Unsupported file type. Supported formats: {supported}", file=sys.stderr)
sys.exit(1)
# Upload file
print(f"Uploading file: {file_path}")
try:
file_info = file_manager.upload_file(file_path)
except Exception as e:
print(f"Upload failed: {e}", file=sys.stderr)
sys.exit(1)
print(f"File uploaded successfully!")
print(f" File ID : {file_info.file_id}")
print(f" Filename: {file_info.filename}")
print(f" Size : {file_info.size} bytes")
# Create data source for uploaded file
file_data_source = DataSource(
data_source_type="FILE",
file_id=file_info.file_id,
)
# Create session
session_mode = args.session_mode.upper()
mode_desc = {
"ASK_DATA": "ASK_DATA mode",
"ANALYSIS": "ANALYSIS mode (recommended for file analysis)",
"INSIGHT": "INSIGHT mode",
"CLAW": "CLAW mode (agentic)",
}.get(session_mode, session_mode)
print(f"\nCreating session: {mode_desc}...")
print(f" Region: {config.region}")
enable_search = getattr(args, 'enable_search', False)
workspace_id = getattr(args, 'workspace_id', None)
custom_agent_id = getattr(args, 'custom_agent_id', None)
# For file analysis, pass file_id to create_or_reuse so the session is bound to the file
session = session_manager.create_or_reuse(mode=session_mode, enable_search=enable_search, file_id=file_data_source.file_id if 'file_data_source' in locals() else None, workspace_id=workspace_id, custom_agent_id=custom_agent_id)
print(f"Session ready: {session.session_id}")
print(f"\n💡 Tip: To continue this session later, use: python3 data_agent_cli.py attach --session-id {session.session_id}")
# Get output mode
output_mode = getattr(args, "output", "summary")
session_dir = Path(f"sessions/{session.session_id}")
session_dir.mkdir(parents=True, exist_ok=True)
# Initialize structured logging for sync mode
from cli.streaming import init_structured_logging, close_structured_logging
init_structured_logging(session_dir)
try:
# Determine queries to execute
if args.query:
queries = [args.query]
else:
# Default preset analysis questions
queries = [
"请分析上传文件的数据结构",
"数据的关键统计指标和分布情况是什么?",
"数据中是否存在异常值或离群点?",
]
# Execute queries with the file data source
print(f"\n{'=' * 60}")
print("File Analysis")
print("=" * 60)
output_text = ""
for query in queries:
print(f"\nQuery: {query}")
print("-" * 50)
try:
got_content, need_confirm, t = _stream_response(
message_handler, session, query,
data_source=file_data_source, output_mode=output_mode,
output_dir=session_dir,
)
if t:
output_text += f"\n### Query: {query}\n" + t + "\n"
if not got_content:
print("(No response received, please retry)")
elif need_confirm:
print("\n⚠️ Agent has created an execution plan. User confirmation required.")
print(f" To continue: python3 scripts/data_agent_cli.py attach --session-id {session.session_id} -q 'User Input' ")
if output_text:
with open(session_dir / "output.md", "w", encoding="utf-8") as f:
f.write(output_text)
with open(session_dir / "result.json", "w", encoding="utf-8") as f:
json.dump({"status": "waiting_input", "output_file": "output.md"}, f)
return
except Exception as e:
print(f"Request failed: {e}")
# List generated files if requested
if args.list_generated_files:
_print_generated_files(file_manager, session.session_id)
finally:
# Close structured logging
close_structured_logging()
# Write result status
with open(session_dir / "result.json", "w", encoding="utf-8") as f:
json.dump({"status": "completed"}, f)
def _print_generated_files(file_manager: FileManager, session_id: str) -> None:
"""Print list of files generated by the Agent."""
print(f"\n{'=' * 60}")
print("Generated Files")
print("=" * 60)
try:
generated = file_manager.list_files(session_id)
if generated:
for f in generated:
print(f" - {f.filename} ({f.file_type}, {f.size} bytes)")
if f.download_url:
print(f" Download: {f.download_url}")
else:
print(" No generated files.")
except Exception as e:
print(f" Failed to get file list: {e}")"""Import subcommand - Add DMS database tables to Data Agent Data Center.
Author: Tinker
Created: 2026-03-05
"""
import argparse
import sys
from data_agent import DataAgentConfig, DataAgentClient
def _confirm_import(args: argparse.Namespace) -> bool:
"""Confirm the high-risk import operation before calling the API."""
if getattr(args, "yes", False):
return True
if not sys.stdin.isatty():
print("Error: Import requires explicit confirmation. Re-run with --yes in non-interactive mode.", file=sys.stderr)
return False
answer = input("Confirm import these tables to Data Center? Type 'yes' to continue: ")
if answer.strip().lower() in {"yes", "y"}:
return True
print("Import cancelled.")
return False
def cmd_import(args: argparse.Namespace) -> None:
"""Handle import subcommand for adding DMS tables to Data Center."""
# Validate required parameters
missing = []
for attr, name in [
("dms_instance_id", "--dms-instance-id"),
("dms_db_id", "--dms-db-id"),
("instance_name", "--instance-name"),
("db_name", "--db-name"),
("tables", "--tables"),
]:
if not getattr(args, attr, None):
missing.append(name)
if missing:
print(f"Error: Missing required parameters: {', '.join(missing)}", file=sys.stderr)
sys.exit(1)
# Parse tables
table_list = [t.strip() for t in args.tables.split(",") if t.strip()]
if not table_list:
print("Error: --tables must contain at least one table name", file=sys.stderr)
sys.exit(1)
print("Adding database tables to Data Center...")
print(f" Region: {args.region}")
print(f" Instance: {args.instance_name}")
print(f" Database: {args.db_name}")
print(f" DMS Instance ID: {args.dms_instance_id}")
print(f" DMS DB ID: {args.dms_db_id}")
print(f" DB Type: {getattr(args, 'engine', 'mysql')}")
print(f" Tables: {', '.join(table_list)}")
print("-" * 60)
if not _confirm_import(args):
sys.exit(1)
# Initialize client only after the operation is confirmed.
config = DataAgentConfig.from_env()
client = DataAgentClient(config)
try:
result = client.add_data_center_table(
instance_name=args.instance_name,
database_name=args.db_name,
dms_instance_id=args.dms_instance_id,
dms_db_id=args.dms_db_id,
table_name_list=table_list,
db_type=getattr(args, "engine", "mysql"),
region_id=args.region,
)
print("✓ Successfully added to Data Center")
print(f" Response: {result.get('Data', result)}")
except Exception as e:
print(f"✗ Failed to add to Data Center: {e}", file=sys.stderr)
sys.exit(1)
"""List databases and tables (ls subcommand).
Author: Tinker
Created: 2026-03-04
"""
import argparse
import sys
from data_agent import DataAgentConfig, DataAgentClient
def _extract_list(resp: dict) -> list:
"""Extract a list from an API response regardless of nesting style.
Handles ``{Data: [...]}`` and ``{Data: {List/DataList/Content: [...]}}``.
Also handles lowercase ``{data: {...}}`` format from some API responses.
"""
# Try uppercase "Data" first, then lowercase "data"
data = resp.get("Data") or resp.get("data") or []
if isinstance(data, list):
return data
if isinstance(data, dict):
return (
data.get("MetaEntities")
or data.get("metaEntities")
or data.get("List")
or data.get("DataList")
or data.get("Content")
or []
)
return []
def _get_field(obj: dict, *names: str, default=""):
"""Get a field value trying multiple possible key names (case-insensitive).
Args:
obj: The dictionary to search
*names: Possible field names to try (e.g., "DatabaseName", "databaseName")
default: Default value if none found
"""
for name in names:
if name in obj:
return obj[name]
return default
def cmd_ls(args: argparse.Namespace) -> None:
"""List DMS databases and (optionally) their tables."""
config = DataAgentConfig.from_env()
client = DataAgentClient(config)
search = getattr(args, "search", None)
db_id = getattr(args, "db_id", None)
workspace_id_arg = getattr(args, "workspace_id", None)
sep = "-" * 60
# Resolve workspace
workspace_id = client._resolve_workspace_id(workspace_id_arg)
workspace_source = client._workspace_source or "unknown"
print(f"Region: {config.region}")
print(f"Workspace: {workspace_id} (source: {workspace_source})")
# -- list databases --
if db_id is None:
print("Fetching databases...")
try:
resp = client.list_databases(workspace_id=workspace_id_arg, search_key=search)
except Exception as e:
print(f"Error: {e}", file=sys.stderr)
sys.exit(1)
items = _extract_list(resp)
if not items:
print("No databases found.")
return
print(f"\n{'=' * 60}")
print(f" Databases ({len(items)})")
print(f"{'=' * 60}")
for db in items:
attrs = db.get("MetaEntityAttrs") or db.get("metaEntityAttrs") or {}
db_id_val = _get_field(attrs, "dbId", "DbId", default="")
schema_name = _get_field(attrs, "schemaName", "SchemaName", default="")
catalog_name = _get_field(attrs, "catalogName", "CatalogName", default="")
db_type = _get_field(attrs, "dbType", "DbType", default="")
instance_id = _get_field(attrs, "instanceId", "InstanceId", default="")
instance_resource_id = _get_field(attrs, "instanceResourceId", "InstanceResourceId", default="")
print(f" {schema_name} [{db_type}] dbId={db_id_val} instanceId={instance_id} instanceResourceId={instance_resource_id} catalogName={catalog_name}")
print()
return
# -- list tables for a specific db_id --
print(f"Fetching tables for AgentDbId={db_id}...")
try:
resp = client.list_tables(agent_db_id=db_id, workspace_id=workspace_id_arg)
except Exception as e:
print(f"Error: {e}", file=sys.stderr)
sys.exit(1)
items = _extract_list(resp)
if not items:
print("No tables found.")
return
# Extract table names from MetaEntityAttrs
table_names = []
for t in items:
attrs = t.get("MetaEntityAttrs") or t.get("metaEntityAttrs") or {}
name = _get_field(attrs, "tableName", "TableName", "Name", default="")
if not name:
# Fallback: try top-level keys
name = _get_field(t, "TableName", "Name", default="")
table_names.append(name)
# Also fetch db metadata from the databases list for display
schema_name = ""
db_type = ""
instance_id = ""
try:
db_resp = client.list_databases(workspace_id=workspace_id_arg)
all_dbs = _extract_list(db_resp)
for db in all_dbs:
attrs = db.get("MetaEntityAttrs") or db.get("metaEntityAttrs") or {}
if str(_get_field(attrs, "dbId", "DbId", default="")) == str(db_id):
schema_name = _get_field(attrs, "schemaName", "SchemaName", default="")
db_type = _get_field(attrs, "dbType", "DbType", default="")
instance_id = _get_field(attrs, "instanceId", "InstanceId", default="")
break
except Exception:
pass
print(f"\n{'=' * 60}")
print(f" Database : {schema_name} [{db_type}]")
print(f" AgentDbId : {db_id}")
print(f" Tables : {len(items)}")
print(f"{'=' * 60}")
for name in table_names:
print(f" {name}")
print()
# Print ready-to-use CLI command
tables_arg = ",".join(table_names)
print(sep)
print(" Ready-to-use db command:")
print(sep)
print(f" python3 data_agent_cli.py db \\")
print(f" --dms-instance-id {instance_id} \\")
print(f" --dms-db-id {db_id} \\")
print(f" --db-name {schema_name} \\")
print(f" --tables {tables_arg} \\")
print(f" --workspace-id {workspace_id} \\")
print(f" --session-mode ASK_DATA \\")
print(f" -q \"your question here\"")
print(sep)
"""Reports subcommand to list and download generated files.
Author: Tinker
Created: 2026-03-11
"""
import argparse
import sys
from pathlib import Path
import time
from data_agent import (
DataAgentConfig,
DataAgentClient,
FileManager,
)
def cmd_reports(args: argparse.Namespace) -> None:
"""Handle reports subcommand."""
session_id = args.session_id
session_dir = Path(f"sessions/{session_id}")
report_dir = session_dir / "reports"
print(f"Fetching generated files for session {session_id}...")
# Initialize components
config = DataAgentConfig.from_env()
client = DataAgentClient(config)
file_manager = FileManager(client)
total_reports = 0
categories = ("WebReport", "TextReport", "DefaultArtifact")
found_files = []
for category in categories:
try:
files = file_manager.list_files(session_id, file_category=category)
if files:
found_files.extend([(category, rf) for rf in files])
except Exception as e:
print(f" Warning: could not list {category}: {e}", file=sys.stderr)
continue
if not found_files:
print("No report files found for this session.")
print("Note: Reports are usually generated in ANALYSIS or INSIGHT modes.")
return
report_dir.mkdir(parents=True, exist_ok=True)
print(f"Downloading files to {report_dir.resolve()}...\n")
for category, rf in found_files:
if not rf.download_url:
print(f" [{category}] {rf.filename or rf.file_id}: No download URL available")
continue
save_path = report_dir / (rf.filename or f"{rf.file_id}.bin")
try:
print(f" Downloading [{category}] {rf.filename or rf.file_id}...")
file_manager.download_from_url(rf.download_url, str(save_path))
print(f" ✅ Saved to {save_path.resolve()}")
total_reports += 1
except Exception as e:
print(f" ❌ Failed to download {rf.filename or rf.file_id} ({category}): {e}", file=sys.stderr)
if total_reports > 0:
print(f"\nSuccessfully downloaded {total_reports} files.")
"""Workspace management subcommand (workspace).
Author: Tinker
Created: 2026-04-16
"""
import argparse
import sys
from datetime import datetime
from data_agent import DataAgentConfig, DataAgentClient
def _get_field(obj: dict, *names: str, default=""):
"""Get a field value trying multiple possible key names."""
for name in names:
if name in obj:
return obj[name]
return default
def _format_timestamp(ts) -> str:
"""Format a unix timestamp (seconds) to readable date string."""
if not ts:
return ""
try:
return datetime.fromtimestamp(int(ts)).strftime("%Y-%m-%d %H:%M")
except (ValueError, TypeError, OSError):
return str(ts)
def cmd_workspace(args: argparse.Namespace) -> None:
"""List Data Agent workspaces."""
config = DataAgentConfig.from_env()
client = DataAgentClient(config)
workspace_type = getattr(args, "workspace_type", "MY")
search_name = getattr(args, "search", None)
page_number = getattr(args, "page_number", 1)
page_size = getattr(args, "page_size", 50)
print(f"Region: {config.region}")
print(f"Fetching workspaces (type={workspace_type})...")
try:
resp = client.list_workspaces(
workspace_type=workspace_type,
workspace_name=search_name,
page_number=page_number,
page_size=page_size,
)
except Exception as e:
print(f"Error: {e}", file=sys.stderr)
sys.exit(1)
# Extract workspace list from response
# Response structure: { data: { content: [...], totalElements, totalPages, ... } }
data = resp.get("data") or resp.get("Data") or {}
items = []
if isinstance(data, dict):
items = data.get("content") or data.get("Content") or []
elif isinstance(data, list):
items = data
total_elements = 0
total_pages = 0
if isinstance(data, dict):
total_elements = data.get("totalElements") or data.get("TotalElements") or len(items)
total_pages = data.get("totalPages") or data.get("TotalPages") or 1
if not items:
print("No workspaces found.")
return
sep = "=" * 60
print(f"\n{sep}")
print(f" Workspaces ({len(items)}) [Type: {workspace_type}] Total: {total_elements}")
print(f"{sep}")
for ws in items:
ws_name = _get_field(ws, "workspaceName", "WorkspaceName")
ws_id = _get_field(ws, "workspaceId", "WorkspaceId")
ws_status = _get_field(ws, "workspaceStatus", "WorkspaceStatus", default="unknown")
role = _get_field(ws, "roleName", "RoleName")
creator = _get_field(ws, "creator", "Creator")
desc = _get_field(ws, "description", "Description")
members = _get_field(ws, "totalMember", "TotalMember", default=0)
create_time = _get_field(ws, "createTime", "CreateTime")
modify_time = _get_field(ws, "modifyTime", "ModifyTime")
print(f"\n {ws_name} [{ws_status}] ({role})")
print(f" WorkspaceId : {ws_id}")
if creator:
print(f" Creator : {creator}")
print(f" Members : {members}")
if desc:
print(f" Description : {desc}")
if create_time:
print(f" Created : {_format_timestamp(create_time)}")
if modify_time:
print(f" Modified : {_format_timestamp(modify_time)}")
if total_pages > 1:
print(f"\n Page {page_number}/{total_pages} (use --page-number to navigate)")
# Print usage hint
print(f"\n{'-' * 60}")
print(" To create a session in a workspace:")
print(f"{'-' * 60}")
print(" python3 scripts/data_agent_cli.py db \\")
print(" --workspace-id <WorkspaceId> \\")
print(" --dms-instance-id <ID> --dms-db-id <ID> \\")
print(" --instance-name <NAME> --db-name <DB> \\")
print(' --tables "t1,t2" -q "your question"')
print(f"{'-' * 60}")
print()
"""Simple dual logging for Data Agent sessions.
This module provides functionality to create both progress.log and progress.jsonl files.
"""
import subprocess
from pathlib import Path
from datetime import datetime
import json
from typing import Optional
def run_with_dual_logging(cmd, session_dir: Path, env=None):
"""Run a subprocess and create both progress.log and progress.jsonl files."""
session_dir.mkdir(parents=True, exist_ok=True)
# Run the subprocess with output redirected to a temporary file
temp_output_path = session_dir / "temp_output.txt"
with open(temp_output_path, 'w', encoding='utf-8') as temp_file:
proc = subprocess.Popen(
cmd,
stdout=temp_file,
stderr=subprocess.STDOUT,
start_new_session=True,
env=env
)
proc.wait()
# After subprocess completes, process the output to create both log files
process_output_to_dual_logs(temp_output_path, session_dir)
# Clean up temporary file
temp_output_path.unlink(missing_ok=True)
return proc
def process_output_to_dual_logs(temp_output_path: Path, session_dir: Path):
"""Process the temporary output file and create log format only (progress.jsonl disabled)."""
progress_log_path = session_dir / "progress.log"
# progress_jsonl_path = session_dir / "progress.jsonl" # Disabled per user request
with open(temp_output_path, 'r', encoding='utf-8') as temp_file:
content = temp_file.read()
# Write to progress.log as-is
with open(progress_log_path, 'w', encoding='utf-8') as log_file:
log_file.write(content)
# JSONL logging disabled - progress.jsonl creation is skipped
# Process content for progress.jsonl (skipped)"""Structured logging handler for Data Agent."""
import json
from pathlib import Path
from datetime import datetime
from typing import TextIO, Optional, Dict, Any
class StructuredLogHandler:
"""Handles logging to plain text and JSONL files."""
def __init__(self, session_dir: Path, log_prefix: str = "progress"):
"""Initialize the log handler.
Args:
session_dir: Directory for the session where logs will be stored.
log_prefix: Prefix for log files (e.g., "progress" for progress.log, or "process" for process.log)
"""
self.session_dir = session_dir
self.log_prefix = log_prefix
self.progress_log_path = session_dir / f"{log_prefix}.log"
self.progress_jsonl_path = session_dir / f"{log_prefix}.jsonl"
self.progress_log_file: Optional[TextIO] = None
self.progress_jsonl_file: Optional[TextIO] = None
def __enter__(self):
"""Enter context manager, opening log file."""
# Ensure directory exists
self.session_dir.mkdir(parents=True, exist_ok=True)
self.progress_log_file = open(self.progress_log_path, "w", encoding="utf-8")
self.progress_jsonl_file = open(self.progress_jsonl_path, "w", encoding="utf-8")
return self
def __exit__(self, exc_type, exc_val, exc_tb):
"""Exit context manager, closing log file."""
if self.progress_log_file:
self.progress_log_file.close()
if self.progress_jsonl_file:
self.progress_jsonl_file.close()
def write_log(self, text: str):
"""Write text to the plain log file (e.g., progress.log or process.log).
Args:
text: Text to write to the plain text log.
"""
if self.progress_log_file:
self.progress_log_file.write(text)
self.progress_log_file.flush() # Ensure immediate write
def write_jsonl(self, data: Dict[str, Any]):
"""Write structured data to the JSONL file.
Args:
data: Dictionary containing structured log data.
"""
if self.progress_jsonl_file:
log_data = dict(data)
log_data.setdefault("timestamp", datetime.now().isoformat())
self.progress_jsonl_file.write(json.dumps(log_data, ensure_ascii=False) + "\n")
self.progress_jsonl_file.flush()
def write_both(self, text: str, data: Optional[Dict[str, Any]] = None):
"""Write to both plain text and JSONL logs.
Args:
text: Text to write to the plain text log.
data: Optional structured data for the JSONL log.
"""
self.write_log(text)
self.write_jsonl(data or {"type": "log", "message": text.rstrip("\n")})
def close(self):
"""Close open log file handles."""
if self.progress_log_file:
self.progress_log_file.close()
self.progress_log_file = None
if self.progress_jsonl_file:
self.progress_jsonl_file.close()
self.progress_jsonl_file = None
#!/usr/bin/env python3
"""Data Agent Unified CLI Tool - Entry Point.
See ``cli/`` package for implementation details.
Author: Tinker
Created: 2026-03-03
"""
import sys
from pathlib import Path
# Ensure local packages (data_agent, cli) are importable
_SCRIPT_DIR = Path(__file__).resolve().parent
if str(_SCRIPT_DIR) not in sys.path:
sys.path.insert(0, str(_SCRIPT_DIR))
# Re-export public API for backward compatibility (tests patch "data_agent_cli.XXX")
from cli import * # noqa: F401,F403
from cli.parser import main
if __name__ == "__main__":
main()
# Data Agent CLI 依赖
# 推荐使用虚拟环境安装:
# python3 -m venv venv && source venv/bin/activate
# pip install -r requirements.txt
# 阿里云 SDK
alibabacloud-tea-openapi==0.4.3
alibabacloud-dms20250414==1.10.0
alibabacloud-dms-enterprise20181101==2.1.6
alibabacloud-openapi-util==0.2.4
# HTTP 客户端
requests==2.33.0
aiohttp==3.13.5
# 环境变量
python-dotenv==1.2.2