maxc-cli 0.6.1__tar.gz → 0.8.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {maxc_cli-0.6.1 → maxc_cli-0.8.0}/PKG-INFO +16 -1
- {maxc_cli-0.6.1 → maxc_cli-0.8.0}/README.md +15 -0
- {maxc_cli-0.6.1 → maxc_cli-0.8.0}/src/maxc_cli/__init__.py +1 -1
- {maxc_cli-0.6.1 → maxc_cli-0.8.0}/src/maxc_cli/_samples.py +20 -0
- {maxc_cli-0.6.1 → maxc_cli-0.8.0}/src/maxc_cli/app.py +309 -0
- {maxc_cli-0.6.1 → maxc_cli-0.8.0}/src/maxc_cli/auth_providers.py +4 -0
- {maxc_cli-0.6.1 → maxc_cli-0.8.0}/src/maxc_cli/backend/job.py +66 -1
- maxc_cli-0.8.0/src/maxc_cli/backend/mcp.py +320 -0
- {maxc_cli-0.6.1 → maxc_cli-0.8.0}/src/maxc_cli/cli.py +285 -0
- {maxc_cli-0.6.1 → maxc_cli-0.8.0}/src/maxc_cli/config.py +45 -0
- maxc_cli-0.8.0/src/maxc_cli/enterprise_tls.py +190 -0
- maxc_cli-0.8.0/src/maxc_cli/mcp_serve.py +264 -0
- {maxc_cli-0.6.1 → maxc_cli-0.8.0}/src/maxc_cli/models.py +16 -1
- {maxc_cli-0.6.1 → maxc_cli-0.8.0}/src/maxc_cli/oauth.py +25 -2
- {maxc_cli-0.6.1 → maxc_cli-0.8.0}/src/maxc_cli/output.py +37 -0
- {maxc_cli-0.6.1 → maxc_cli-0.8.0}/src/maxc_cli/skills/SKILL.md +40 -0
- {maxc_cli-0.6.1 → maxc_cli-0.8.0}/src/maxc_cli/skills/references/command-patterns.md +29 -0
- {maxc_cli-0.6.1 → maxc_cli-0.8.0}/src/maxc_cli.egg-info/PKG-INFO +16 -1
- {maxc_cli-0.6.1 → maxc_cli-0.8.0}/src/maxc_cli.egg-info/SOURCES.txt +8 -0
- {maxc_cli-0.6.1 → maxc_cli-0.8.0}/tests/test_agent_skill_commands_context.py +25 -23
- maxc_cli-0.8.0/tests/test_backend_mcp.py +419 -0
- {maxc_cli-0.6.1 → maxc_cli-0.8.0}/tests/test_build_release_archive_compat.py +38 -0
- {maxc_cli-0.6.1 → maxc_cli-0.8.0}/tests/test_cache.py +30 -16
- {maxc_cli-0.6.1 → maxc_cli-0.8.0}/tests/test_cli_mock.py +3 -0
- {maxc_cli-0.6.1 → maxc_cli-0.8.0}/tests/test_config_atomic_write.py +2 -0
- maxc_cli-0.8.0/tests/test_enterprise_tls.py +247 -0
- {maxc_cli-0.6.1 → maxc_cli-0.8.0}/tests/test_external_auth.py +4 -4
- {maxc_cli-0.6.1 → maxc_cli-0.8.0}/tests/test_installer_contracts.py +135 -8
- maxc_cli-0.8.0/tests/test_job_inspection.py +160 -0
- {maxc_cli-0.6.1 → maxc_cli-0.8.0}/tests/test_job_store_durability.py +9 -0
- maxc_cli-0.8.0/tests/test_kb_commands.py +497 -0
- maxc_cli-0.8.0/tests/test_mcp_serve.py +352 -0
- {maxc_cli-0.6.1 → maxc_cli-0.8.0}/tests/test_oauth.py +113 -1
- {maxc_cli-0.6.1 → maxc_cli-0.8.0}/tests/test_output_format_contract.py +2 -1
- {maxc_cli-0.6.1 → maxc_cli-0.8.0}/tests/test_semantic_management.py +1 -1
- {maxc_cli-0.6.1 → maxc_cli-0.8.0}/MANIFEST.in +0 -0
- {maxc_cli-0.6.1 → maxc_cli-0.8.0}/pyproject.toml +0 -0
- {maxc_cli-0.6.1 → maxc_cli-0.8.0}/scripts/pyinstaller_entry.py +0 -0
- {maxc_cli-0.6.1 → maxc_cli-0.8.0}/scripts/regression_test.py +0 -0
- {maxc_cli-0.6.1 → maxc_cli-0.8.0}/setup.cfg +0 -0
- {maxc_cli-0.6.1 → maxc_cli-0.8.0}/setup.py +0 -0
- {maxc_cli-0.6.1 → maxc_cli-0.8.0}/src/maxc_cli/__main__.py +0 -0
- {maxc_cli-0.6.1 → maxc_cli-0.8.0}/src/maxc_cli/agent_platforms.py +0 -0
- {maxc_cli-0.6.1 → maxc_cli-0.8.0}/src/maxc_cli/audit.py +0 -0
- {maxc_cli-0.6.1 → maxc_cli-0.8.0}/src/maxc_cli/auth_continuation.py +0 -0
- {maxc_cli-0.6.1 → maxc_cli-0.8.0}/src/maxc_cli/backend/__init__.py +0 -0
- {maxc_cli-0.6.1 → maxc_cli-0.8.0}/src/maxc_cli/backend/auth.py +0 -0
- {maxc_cli-0.6.1 → maxc_cli-0.8.0}/src/maxc_cli/backend/catalog.py +0 -0
- {maxc_cli-0.6.1 → maxc_cli-0.8.0}/src/maxc_cli/backend/data.py +0 -0
- {maxc_cli-0.6.1 → maxc_cli-0.8.0}/src/maxc_cli/backend/meta.py +0 -0
- {maxc_cli-0.6.1 → maxc_cli-0.8.0}/src/maxc_cli/backend/odps.py +0 -0
- {maxc_cli-0.6.1 → maxc_cli-0.8.0}/src/maxc_cli/backend/query.py +0 -0
- {maxc_cli-0.6.1 → maxc_cli-0.8.0}/src/maxc_cli/backend/semantic.py +0 -0
- {maxc_cli-0.6.1 → maxc_cli-0.8.0}/src/maxc_cli/cache.py +0 -0
- {maxc_cli-0.6.1 → maxc_cli-0.8.0}/src/maxc_cli/catalog_bootstrap.py +0 -0
- {maxc_cli-0.6.1 → maxc_cli-0.8.0}/src/maxc_cli/exceptions.py +0 -0
- {maxc_cli-0.6.1 → maxc_cli-0.8.0}/src/maxc_cli/help_format.py +0 -0
- {maxc_cli-0.6.1 → maxc_cli-0.8.0}/src/maxc_cli/helpers.py +0 -0
- {maxc_cli-0.6.1 → maxc_cli-0.8.0}/src/maxc_cli/job_ids.py +0 -0
- {maxc_cli-0.6.1 → maxc_cli-0.8.0}/src/maxc_cli/masking.py +0 -0
- {maxc_cli-0.6.1 → maxc_cli-0.8.0}/src/maxc_cli/odps_runtime.py +0 -0
- {maxc_cli-0.6.1 → maxc_cli-0.8.0}/src/maxc_cli/proxy_auth.py +0 -0
- {maxc_cli-0.6.1 → maxc_cli-0.8.0}/src/maxc_cli/semantic.py +0 -0
- {maxc_cli-0.6.1 → maxc_cli-0.8.0}/src/maxc_cli/semantic_management.py +0 -0
- {maxc_cli-0.6.1 → maxc_cli-0.8.0}/src/maxc_cli/setting_parser.py +0 -0
- {maxc_cli-0.6.1 → maxc_cli-0.8.0}/src/maxc_cli/skills/agents/openai.yaml +0 -0
- {maxc_cli-0.6.1 → maxc_cli-0.8.0}/src/maxc_cli/skills/references/bootstrap-auth.md +0 -0
- {maxc_cli-0.6.1 → maxc_cli-0.8.0}/src/maxc_cli/skills/references/bootstrap-flow.md +0 -0
- {maxc_cli-0.6.1 → maxc_cli-0.8.0}/src/maxc_cli/skills/references/json-output-format.md +0 -0
- {maxc_cli-0.6.1 → maxc_cli-0.8.0}/src/maxc_cli/skills/references/maxcompute-select-guide.md +0 -0
- {maxc_cli-0.6.1 → maxc_cli-0.8.0}/src/maxc_cli/skills/references/maxcompute-sql-notes.md +0 -0
- {maxc_cli-0.6.1 → maxc_cli-0.8.0}/src/maxc_cli/skills/references/partition-guide.md +0 -0
- {maxc_cli-0.6.1 → maxc_cli-0.8.0}/src/maxc_cli/skills/references/red-lines.md +0 -0
- {maxc_cli-0.6.1 → maxc_cli-0.8.0}/src/maxc_cli/skills/references/semantic-packages.md +0 -0
- {maxc_cli-0.6.1 → maxc_cli-0.8.0}/src/maxc_cli/skills/references/setup-install.md +0 -0
- {maxc_cli-0.6.1 → maxc_cli-0.8.0}/src/maxc_cli/skills/references/sql-common-errors.md +0 -0
- {maxc_cli-0.6.1 → maxc_cli-0.8.0}/src/maxc_cli/skills/references/sql-query-patterns.md +0 -0
- {maxc_cli-0.6.1 → maxc_cli-0.8.0}/src/maxc_cli/skills/references/text2sql-principles.md +0 -0
- {maxc_cli-0.6.1 → maxc_cli-0.8.0}/src/maxc_cli/state_permissions.py +0 -0
- {maxc_cli-0.6.1 → maxc_cli-0.8.0}/src/maxc_cli/store.py +0 -0
- {maxc_cli-0.6.1 → maxc_cli-0.8.0}/src/maxc_cli/utils.py +0 -0
- {maxc_cli-0.6.1 → maxc_cli-0.8.0}/src/maxc_cli.egg-info/dependency_links.txt +0 -0
- {maxc_cli-0.6.1 → maxc_cli-0.8.0}/src/maxc_cli.egg-info/entry_points.txt +0 -0
- {maxc_cli-0.6.1 → maxc_cli-0.8.0}/src/maxc_cli.egg-info/requires.txt +0 -0
- {maxc_cli-0.6.1 → maxc_cli-0.8.0}/src/maxc_cli.egg-info/top_level.txt +0 -0
- {maxc_cli-0.6.1 → maxc_cli-0.8.0}/tests/test_agent_hints_and_cli.py +0 -0
- {maxc_cli-0.6.1 → maxc_cli-0.8.0}/tests/test_agent_platforms.py +0 -0
- {maxc_cli-0.6.1 → maxc_cli-0.8.0}/tests/test_agent_skill_commands.py +0 -0
- {maxc_cli-0.6.1 → maxc_cli-0.8.0}/tests/test_ai_native_contract_regressions.py +0 -0
- {maxc_cli-0.6.1 → maxc_cli-0.8.0}/tests/test_auth_logout.py +0 -0
- {maxc_cli-0.6.1 → maxc_cli-0.8.0}/tests/test_backend_auth.py +0 -0
- {maxc_cli-0.6.1 → maxc_cli-0.8.0}/tests/test_backend_data.py +0 -0
- {maxc_cli-0.6.1 → maxc_cli-0.8.0}/tests/test_backend_data_serialization.py +0 -0
- {maxc_cli-0.6.1 → maxc_cli-0.8.0}/tests/test_backend_meta.py +0 -0
- {maxc_cli-0.6.1 → maxc_cli-0.8.0}/tests/test_build_release_script.py +0 -0
- {maxc_cli-0.6.1 → maxc_cli-0.8.0}/tests/test_catalog.py +0 -0
- {maxc_cli-0.6.1 → maxc_cli-0.8.0}/tests/test_catalog_bootstrap.py +0 -0
- {maxc_cli-0.6.1 → maxc_cli-0.8.0}/tests/test_cli_arg_validation.py +0 -0
- {maxc_cli-0.6.1 → maxc_cli-0.8.0}/tests/test_cli_query_parse_and_sanitize.py +0 -0
- {maxc_cli-0.6.1 → maxc_cli-0.8.0}/tests/test_compat.py +0 -0
- {maxc_cli-0.6.1 → maxc_cli-0.8.0}/tests/test_e2e_smoke.py +0 -0
- {maxc_cli-0.6.1 → maxc_cli-0.8.0}/tests/test_effective_hints_contract.py +0 -0
- {maxc_cli-0.6.1 → maxc_cli-0.8.0}/tests/test_envelope_shape.py +0 -0
- {maxc_cli-0.6.1 → maxc_cli-0.8.0}/tests/test_error_self_correction.py +0 -0
- {maxc_cli-0.6.1 → maxc_cli-0.8.0}/tests/test_error_translation.py +0 -0
- {maxc_cli-0.6.1 → maxc_cli-0.8.0}/tests/test_exit_codes.py +0 -0
- {maxc_cli-0.6.1 → maxc_cli-0.8.0}/tests/test_flag_hoist.py +0 -0
- {maxc_cli-0.6.1 → maxc_cli-0.8.0}/tests/test_help_format.py +0 -0
- {maxc_cli-0.6.1 → maxc_cli-0.8.0}/tests/test_help_version_e2e.py +0 -0
- {maxc_cli-0.6.1 → maxc_cli-0.8.0}/tests/test_helpers.py +0 -0
- {maxc_cli-0.6.1 → maxc_cli-0.8.0}/tests/test_helpers_csv.py +0 -0
- {maxc_cli-0.6.1 → maxc_cli-0.8.0}/tests/test_integration.py +0 -0
- {maxc_cli-0.6.1 → maxc_cli-0.8.0}/tests/test_integration_real.py +0 -0
- {maxc_cli-0.6.1 → maxc_cli-0.8.0}/tests/test_job_improvements.py +0 -0
- {maxc_cli-0.6.1 → maxc_cli-0.8.0}/tests/test_manifest_runtime_contract.py +0 -0
- {maxc_cli-0.6.1 → maxc_cli-0.8.0}/tests/test_masking.py +0 -0
- {maxc_cli-0.6.1 → maxc_cli-0.8.0}/tests/test_meta_schema_and_partition_cols.py +0 -0
- {maxc_cli-0.6.1 → maxc_cli-0.8.0}/tests/test_odps_runtime.py +0 -0
- {maxc_cli-0.6.1 → maxc_cli-0.8.0}/tests/test_output_action_safety.py +0 -0
- {maxc_cli-0.6.1 → maxc_cli-0.8.0}/tests/test_packaging_metadata.py +0 -0
- {maxc_cli-0.6.1 → maxc_cli-0.8.0}/tests/test_phase1_improvements.py +0 -0
- {maxc_cli-0.6.1 → maxc_cli-0.8.0}/tests/test_proxy_auth.py +0 -0
- {maxc_cli-0.6.1 → maxc_cli-0.8.0}/tests/test_pyinstaller_bundle.py +0 -0
- {maxc_cli-0.6.1 → maxc_cli-0.8.0}/tests/test_python39_compat.py +0 -0
- {maxc_cli-0.6.1 → maxc_cli-0.8.0}/tests/test_query_auto_promote.py +0 -0
- {maxc_cli-0.6.1 → maxc_cli-0.8.0}/tests/test_query_result_csv_fallback.py +0 -0
- {maxc_cli-0.6.1 → maxc_cli-0.8.0}/tests/test_semantic_scope.py +0 -0
- {maxc_cli-0.6.1 → maxc_cli-0.8.0}/tests/test_semantic_transport.py +0 -0
- {maxc_cli-0.6.1 → maxc_cli-0.8.0}/tests/test_setting_parser.py +0 -0
- {maxc_cli-0.6.1 → maxc_cli-0.8.0}/tests/test_skill_cli_consistency.py +0 -0
- {maxc_cli-0.6.1 → maxc_cli-0.8.0}/tests/test_skill_eval.py +0 -0
- {maxc_cli-0.6.1 → maxc_cli-0.8.0}/tests/test_skill_renderer.py +0 -0
- {maxc_cli-0.6.1 → maxc_cli-0.8.0}/tests/test_startup_imports.py +0 -0
- {maxc_cli-0.6.1 → maxc_cli-0.8.0}/tests/test_state_permissions.py +0 -0
- {maxc_cli-0.6.1 → maxc_cli-0.8.0}/tests/test_state_portability.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: maxc-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.8.0
|
|
4
4
|
Summary: Agent-native MaxCompute CLI for external coding agents
|
|
5
5
|
Classifier: Programming Language :: Python :: 3
|
|
6
6
|
Classifier: Programming Language :: Python :: 3.9
|
|
@@ -311,3 +311,18 @@ Metadata edits lack server CAS; publication lacks idempotency keys. An uncertain
|
|
|
311
311
|
write must be reconciled before retrying. Publication checks are not SQL or
|
|
312
312
|
business validation. History lists the latest 20 summaries, and an immutable
|
|
313
313
|
revision read does not guarantee historical DataBridge analysis.
|
|
314
|
+
|
|
315
|
+
### Task metrics and worker logs
|
|
316
|
+
|
|
317
|
+
```bash
|
|
318
|
+
aliyun maxc job task-detail <instance_id> --task-name AnonymousSQLTask --json
|
|
319
|
+
aliyun maxc job task-summary <instance_id> --task-name AnonymousSQLTask --json
|
|
320
|
+
aliyun maxc job workers <instance_id> --task-name AnonymousSQLTask --json
|
|
321
|
+
aliyun maxc job worker-log <instance_id> <log_id> --log-type stdout --size 1048576 --json
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
These read-only commands use the same project and saved job context as `job status`.
|
|
325
|
+
Task detail preserves the service payload (including `mapReduce.jsonSummary`).
|
|
326
|
+
Select `log_id` from `data.workers`; logs are returned as `data.content` in the JSON
|
|
327
|
+
envelope. The requested size defaults to 1 MiB and must be positive; returned logs
|
|
328
|
+
may be partial. Use `--project` to select the owning project explicitly.
|
|
@@ -289,3 +289,18 @@ Metadata edits lack server CAS; publication lacks idempotency keys. An uncertain
|
|
|
289
289
|
write must be reconciled before retrying. Publication checks are not SQL or
|
|
290
290
|
business validation. History lists the latest 20 summaries, and an immutable
|
|
291
291
|
revision read does not guarantee historical DataBridge analysis.
|
|
292
|
+
|
|
293
|
+
### Task metrics and worker logs
|
|
294
|
+
|
|
295
|
+
```bash
|
|
296
|
+
aliyun maxc job task-detail <instance_id> --task-name AnonymousSQLTask --json
|
|
297
|
+
aliyun maxc job task-summary <instance_id> --task-name AnonymousSQLTask --json
|
|
298
|
+
aliyun maxc job workers <instance_id> --task-name AnonymousSQLTask --json
|
|
299
|
+
aliyun maxc job worker-log <instance_id> <log_id> --log-type stdout --size 1048576 --json
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
These read-only commands use the same project and saved job context as `job status`.
|
|
303
|
+
Task detail preserves the service payload (including `mapReduce.jsonSummary`).
|
|
304
|
+
Select `log_id` from `data.workers`; logs are returned as `data.content` in the JSON
|
|
305
|
+
envelope. The requested size defaults to 1 MiB and must be positive; returned logs
|
|
306
|
+
may be partial. Use `--project` to select the owning project explicitly.
|
|
@@ -59,6 +59,10 @@ SAMPLES: dict[str, str] = {
|
|
|
59
59
|
"maxc job wait <job_id>\n"
|
|
60
60
|
"maxc job wait <job_id> --timeout 600 --stream"
|
|
61
61
|
),
|
|
62
|
+
"job.task-detail": "maxc job task-detail <job_id> --task-name AnonymousSQLTask --json",
|
|
63
|
+
"job.task-summary": "maxc job task-summary <job_id> --task-name AnonymousSQLTask --json",
|
|
64
|
+
"job.workers": "maxc job workers <job_id> --task-name AnonymousSQLTask --json",
|
|
65
|
+
"job.worker-log": "maxc job worker-log <job_id> <log_id> --log-type stdout --size 1048576 --json",
|
|
62
66
|
"job.diagnose": "maxc job diagnose <job_id>\nmaxc job diagnose <job_id> --json",
|
|
63
67
|
"job.result": (
|
|
64
68
|
"maxc job result <job_id>\n"
|
|
@@ -84,6 +88,22 @@ SAMPLES: dict[str, str] = {
|
|
|
84
88
|
"maxc meta search orders\n"
|
|
85
89
|
"maxc meta search user --project my_proj --json"
|
|
86
90
|
),
|
|
91
|
+
# ── kb ─────────────────────────────────────────────────────────────────
|
|
92
|
+
"kb": "maxc kb ask \"How do I set a split size hint?\" --json\nmaxc kb search \"dynamic filter\" --json",
|
|
93
|
+
"kb.ask": (
|
|
94
|
+
"maxc kb ask \"How do I set a split size hint?\"\n"
|
|
95
|
+
"maxc kb ask \"What does ODPS-0123144 mean?\" --max-docs 3 --json"
|
|
96
|
+
),
|
|
97
|
+
"kb.search": (
|
|
98
|
+
"maxc kb search \"clustered table bucket\"\n"
|
|
99
|
+
"maxc kb search \"dynamic filter\" --limit 5 --context-lines 3 --json"
|
|
100
|
+
),
|
|
101
|
+
# ── mcp ────────────────────────────────────────────────────────────────
|
|
102
|
+
"mcp": 'maxc mcp serve # point an MCP client at {"command": "maxc", "args": ["mcp", "serve"]}',
|
|
103
|
+
"mcp.serve": (
|
|
104
|
+
'maxc mcp serve\n'
|
|
105
|
+
'# MCP client config: {"command": "maxc", "args": ["mcp", "serve"]}'
|
|
106
|
+
),
|
|
87
107
|
"meta.search-columns": (
|
|
88
108
|
"maxc meta search-columns user_id\n"
|
|
89
109
|
"maxc meta search-columns dt --project my_proj --json"
|
|
@@ -1811,6 +1811,30 @@ class MaxCApp:
|
|
|
1811
1811
|
self.log("job.cancel", envelope.status, envelope.metadata)
|
|
1812
1812
|
return envelope
|
|
1813
1813
|
|
|
1814
|
+
def job_inspect(
|
|
1815
|
+
self, job_id: 'str', *, section: 'str', project: 'str | None' = None,
|
|
1816
|
+
task_name: 'str | None' = None, log_id: 'str | None' = None,
|
|
1817
|
+
log_type: 'str' = "stdout", size: 'int' = 1048576,
|
|
1818
|
+
) -> 'Envelope':
|
|
1819
|
+
if not self.remote_jobs:
|
|
1820
|
+
raise FeatureUnavailableError("Task diagnostics require a MaxCompute backend.")
|
|
1821
|
+
resolved = self._resolve_remote_job_id(job_id, project=project)
|
|
1822
|
+
payload = self.backend.inspect_job(
|
|
1823
|
+
resolved.instance_id, section=section, project=resolved.project,
|
|
1824
|
+
session_context=resolved.session_context, task_name=task_name,
|
|
1825
|
+
log_id=log_id, log_type=log_type, size=size,
|
|
1826
|
+
)
|
|
1827
|
+
envelope = Envelope(
|
|
1828
|
+
command=f"job.{section}", status="success",
|
|
1829
|
+
data={"job_id": resolved.external_job_id, **payload},
|
|
1830
|
+
metadata={"project": resolved.project, "job_id": resolved.external_job_id},
|
|
1831
|
+
agent_hints=AgentHints(warnings=[
|
|
1832
|
+
"The service may return only part of the log; requested_size does not prove completeness."
|
|
1833
|
+
] if section == "worker-log" else []),
|
|
1834
|
+
)
|
|
1835
|
+
self.log(envelope.command, envelope.status, envelope.metadata)
|
|
1836
|
+
return envelope
|
|
1837
|
+
|
|
1814
1838
|
def job_diagnose(self, job_id: 'str', *, project: 'str | None' = None) -> 'Envelope':
|
|
1815
1839
|
if self.remote_jobs:
|
|
1816
1840
|
resolved = self._resolve_remote_job_id(job_id, project=project)
|
|
@@ -3836,6 +3860,288 @@ class MaxCApp:
|
|
|
3836
3860
|
self.log("meta.list-projects", envelope.status, envelope.metadata)
|
|
3837
3861
|
return envelope
|
|
3838
3862
|
|
|
3863
|
+
# --- Knowledge base (public MCP) ------------------------------------
|
|
3864
|
+
|
|
3865
|
+
def _mcp_client(self):
|
|
3866
|
+
"""Build a stateless MCP client authorized by the already-resolved credentials.
|
|
3867
|
+
|
|
3868
|
+
Raises ``FeatureUnavailableError`` rather than silently falling back: an agent
|
|
3869
|
+
that reads "no results" and "KB unreachable" as the same signal would conclude
|
|
3870
|
+
the documentation does not cover its question.
|
|
3871
|
+
"""
|
|
3872
|
+
from .backend.mcp import (
|
|
3873
|
+
CatalogMcpTokenProvider,
|
|
3874
|
+
McpError,
|
|
3875
|
+
McpHttpClient,
|
|
3876
|
+
build_catalog_mint,
|
|
3877
|
+
default_endpoint,
|
|
3878
|
+
)
|
|
3879
|
+
|
|
3880
|
+
if not self.config.mcp.enabled:
|
|
3881
|
+
raise FeatureUnavailableError(
|
|
3882
|
+
"Knowledge-base commands need the MaxCompute MCP endpoint enabled.",
|
|
3883
|
+
suggestion=(
|
|
3884
|
+
"Set `mcp.enabled: true` in the maxc config. The bearer is minted "
|
|
3885
|
+
"from your existing MaxCompute credentials, so no separate login "
|
|
3886
|
+
"is required."
|
|
3887
|
+
),
|
|
3888
|
+
)
|
|
3889
|
+
if self.backend is None or not hasattr(self.backend, "_catalog_rest"):
|
|
3890
|
+
raise FeatureUnavailableError(
|
|
3891
|
+
"Knowledge-base commands need an authenticated backend to mint an MCP token.",
|
|
3892
|
+
suggestion="Configure credentials first: `maxc auth whoami --json`.",
|
|
3893
|
+
)
|
|
3894
|
+
catalog_rest = self.backend._catalog_rest
|
|
3895
|
+
if catalog_rest is None:
|
|
3896
|
+
raise BackendConnectionError(
|
|
3897
|
+
"Could not reach CatalogAPI, which mints the MCP access token.",
|
|
3898
|
+
suggestion=(
|
|
3899
|
+
"Verify connectivity and identity: `maxc agent doctor --online --json`."
|
|
3900
|
+
),
|
|
3901
|
+
)
|
|
3902
|
+
endpoint = (self.config.mcp.endpoint or "").strip() or default_endpoint(
|
|
3903
|
+
self.config.default_region
|
|
3904
|
+
)
|
|
3905
|
+
try:
|
|
3906
|
+
tokens = CatalogMcpTokenProvider(
|
|
3907
|
+
build_catalog_mint(catalog_rest, catalog_rest.endpoint or "")
|
|
3908
|
+
)
|
|
3909
|
+
return McpHttpClient(
|
|
3910
|
+
endpoint,
|
|
3911
|
+
tokens,
|
|
3912
|
+
timeout=self.config.mcp.timeout_seconds,
|
|
3913
|
+
client_version=__version__,
|
|
3914
|
+
)
|
|
3915
|
+
except McpError as exc:
|
|
3916
|
+
raise BackendConnectionError(str(exc)) from exc
|
|
3917
|
+
|
|
3918
|
+
@staticmethod
|
|
3919
|
+
def _kb_structured(result: 'dict[str, Any]') -> 'dict[str, Any]':
|
|
3920
|
+
structured = result.get("structuredContent")
|
|
3921
|
+
if not isinstance(structured, dict):
|
|
3922
|
+
raise BackendConnectionError(
|
|
3923
|
+
"The knowledge-base tool returned no structured content.",
|
|
3924
|
+
suggestion="Retry, or fall back to `maxc meta search` for metadata lookups.",
|
|
3925
|
+
)
|
|
3926
|
+
return structured
|
|
3927
|
+
|
|
3928
|
+
@staticmethod
|
|
3929
|
+
def _kb_payload(
|
|
3930
|
+
structured: 'dict[str, Any]',
|
|
3931
|
+
*,
|
|
3932
|
+
answer_from: 'tuple[str, ...] | None' = None,
|
|
3933
|
+
nested_citation: bool = False,
|
|
3934
|
+
package: bool = False,
|
|
3935
|
+
) -> 'dict[str, Any]':
|
|
3936
|
+
"""Carry the server's fields through rather than rebuilding them.
|
|
3937
|
+
|
|
3938
|
+
Only two transformations happen here: citations are flattened to a stable
|
|
3939
|
+
``uri`` key (the server nests it under ``source`` for search and puts it
|
|
3940
|
+
top-level for ask), and an answer string is wrapped so its provenance is
|
|
3941
|
+
explicit. Everything else is passed by reference on purpose — a field the
|
|
3942
|
+
service adds starts appearing in maxc output with no CLI change, which is
|
|
3943
|
+
the behaviour worth having when the upstream schema is not ours to pin.
|
|
3944
|
+
"""
|
|
3945
|
+
data = structured.get("data") if isinstance(structured.get("data"), dict) else {}
|
|
3946
|
+
payload: dict[str, Any] = {}
|
|
3947
|
+
if answer_from is not None:
|
|
3948
|
+
text = next(
|
|
3949
|
+
(data[key] for key in answer_from if data.get(key) is not None), None
|
|
3950
|
+
)
|
|
3951
|
+
payload["answer"] = {"query": data.get("query"), "text": text}
|
|
3952
|
+
else:
|
|
3953
|
+
search: dict[str, Any] = {
|
|
3954
|
+
"query": data.get("query"),
|
|
3955
|
+
"matches": [],
|
|
3956
|
+
}
|
|
3957
|
+
if package:
|
|
3958
|
+
search["package"] = data.get("package")
|
|
3959
|
+
raw_items = data.get("results") or data.get("matches") or []
|
|
3960
|
+
matches = []
|
|
3961
|
+
for item in raw_items:
|
|
3962
|
+
if not isinstance(item, dict):
|
|
3963
|
+
continue
|
|
3964
|
+
entry = dict(item)
|
|
3965
|
+
uri = entry.get("uri") or entry.get("url")
|
|
3966
|
+
if uri is None and nested_citation:
|
|
3967
|
+
source = entry.get("source")
|
|
3968
|
+
if isinstance(source, dict):
|
|
3969
|
+
uri = source.get("uri") or source.get("url")
|
|
3970
|
+
elif isinstance(source, str):
|
|
3971
|
+
uri = source
|
|
3972
|
+
if uri is not None:
|
|
3973
|
+
entry["uri"] = uri
|
|
3974
|
+
matches.append(entry)
|
|
3975
|
+
search["matches"] = matches
|
|
3976
|
+
payload["search"] = search
|
|
3977
|
+
raw_citations = structured.get("citations")
|
|
3978
|
+
if isinstance(raw_citations, list):
|
|
3979
|
+
flattened = []
|
|
3980
|
+
for item in raw_citations:
|
|
3981
|
+
if not isinstance(item, dict):
|
|
3982
|
+
continue
|
|
3983
|
+
entry = dict(item)
|
|
3984
|
+
uri = entry.get("uri") or entry.get("url")
|
|
3985
|
+
if uri is not None:
|
|
3986
|
+
entry["uri"] = uri
|
|
3987
|
+
flattened.append(entry)
|
|
3988
|
+
payload["citations"] = flattened
|
|
3989
|
+
payload["pagination"] = {
|
|
3990
|
+
"has_more": bool(structured.get("has_more", False)),
|
|
3991
|
+
"next_cursor": structured.get("next_cursor"),
|
|
3992
|
+
}
|
|
3993
|
+
# `request_id` is the only handle for correlating a metered model call with
|
|
3994
|
+
# a service-side incident, so keep it at a predictable location.
|
|
3995
|
+
payload["request_id"] = structured.get("request_id")
|
|
3996
|
+
return payload
|
|
3997
|
+
|
|
3998
|
+
def kb_ask(
|
|
3999
|
+
self,
|
|
4000
|
+
question: 'str',
|
|
4001
|
+
*,
|
|
4002
|
+
max_docs: 'int | None' = None,
|
|
4003
|
+
region: 'str | None' = None,
|
|
4004
|
+
) -> 'Envelope':
|
|
4005
|
+
"""Answer from retrieved documentation, with citations kept as first-class output.
|
|
4006
|
+
|
|
4007
|
+
The two kb tools return different shapes (ask yields ``data.answer`` plus a
|
|
4008
|
+
top-level ``citations[]``; search yields ``data.results[]`` with the URI nested
|
|
4009
|
+
under ``source``), so each is projected explicitly instead of sharing one guess.
|
|
4010
|
+
"""
|
|
4011
|
+
started = monotonic()
|
|
4012
|
+
arguments: dict[str, Any] = {"question": question}
|
|
4013
|
+
if max_docs is not None:
|
|
4014
|
+
arguments["max_docs"] = max_docs
|
|
4015
|
+
effective_region = region or self.config.default_region
|
|
4016
|
+
if effective_region:
|
|
4017
|
+
arguments["region"] = effective_region
|
|
4018
|
+
structured = self._call_kb_tool("maxcompute_kb_ask", arguments)
|
|
4019
|
+
payload = self._kb_payload(
|
|
4020
|
+
structured,
|
|
4021
|
+
answer_from=("answer", "text"),
|
|
4022
|
+
)
|
|
4023
|
+
envelope = self._kb_envelope(
|
|
4024
|
+
"kb.ask",
|
|
4025
|
+
payload,
|
|
4026
|
+
started,
|
|
4027
|
+
effective_region,
|
|
4028
|
+
follow_up="kb.search",
|
|
4029
|
+
warnings=self._kb_warnings(structured),
|
|
4030
|
+
insights=[
|
|
4031
|
+
"The answer text is model-generated from the cited documents; attribute "
|
|
4032
|
+
"product claims to `citations[].uri` rather than restating them as fact.",
|
|
4033
|
+
"An empty or thin citation list means retrieval found little, which is "
|
|
4034
|
+
"not evidence that the behaviour is undocumented.",
|
|
4035
|
+
],
|
|
4036
|
+
)
|
|
4037
|
+
self.log("kb.ask", envelope.status, envelope.metadata)
|
|
4038
|
+
return envelope
|
|
4039
|
+
|
|
4040
|
+
def kb_search(
|
|
4041
|
+
self,
|
|
4042
|
+
query: 'str',
|
|
4043
|
+
*,
|
|
4044
|
+
limit: 'int' = 5,
|
|
4045
|
+
context_lines: 'int | None' = None,
|
|
4046
|
+
region: 'str | None' = None,
|
|
4047
|
+
) -> 'Envelope':
|
|
4048
|
+
started = monotonic()
|
|
4049
|
+
arguments: dict[str, Any] = {"query": query, "limit": limit}
|
|
4050
|
+
if context_lines is not None:
|
|
4051
|
+
arguments["before_lines"] = context_lines
|
|
4052
|
+
arguments["after_lines"] = context_lines
|
|
4053
|
+
effective_region = region or self.config.default_region
|
|
4054
|
+
if effective_region:
|
|
4055
|
+
arguments["region"] = effective_region
|
|
4056
|
+
structured = self._call_kb_tool("maxcompute_kb_search", arguments)
|
|
4057
|
+
payload = self._kb_payload(
|
|
4058
|
+
structured,
|
|
4059
|
+
nested_citation=True,
|
|
4060
|
+
package=True,
|
|
4061
|
+
)
|
|
4062
|
+
envelope = self._kb_envelope(
|
|
4063
|
+
"kb.search",
|
|
4064
|
+
payload,
|
|
4065
|
+
started,
|
|
4066
|
+
effective_region,
|
|
4067
|
+
follow_up="kb.ask",
|
|
4068
|
+
warnings=self._kb_warnings(structured),
|
|
4069
|
+
insights=[
|
|
4070
|
+
"Snippets are truncated server-side; open the cited `uri` before quoting "
|
|
4071
|
+
"a passage as complete.",
|
|
4072
|
+
],
|
|
4073
|
+
)
|
|
4074
|
+
self.log("kb.search", envelope.status, envelope.metadata)
|
|
4075
|
+
return envelope
|
|
4076
|
+
|
|
4077
|
+
@staticmethod
|
|
4078
|
+
def _kb_warnings(structured: 'dict[str, Any]') -> 'list[str]':
|
|
4079
|
+
"""Surface retrieval degradation that still answered HTTP 200 and ok=true-looking.
|
|
4080
|
+
|
|
4081
|
+
Negative inference is the specific risk here: an agent reading an empty result
|
|
4082
|
+
as "the platform cannot do this" produces a confident, wrong answer.
|
|
4083
|
+
"""
|
|
4084
|
+
warnings = [str(item) for item in (structured.get("warnings") or [])]
|
|
4085
|
+
if structured.get("ok") is False:
|
|
4086
|
+
warnings.append(
|
|
4087
|
+
"The knowledge-base tool reported ok=false; treat this response as unverified."
|
|
4088
|
+
)
|
|
4089
|
+
return warnings
|
|
4090
|
+
|
|
4091
|
+
def _kb_envelope(
|
|
4092
|
+
self,
|
|
4093
|
+
command: 'str',
|
|
4094
|
+
payload: 'dict[str, Any]',
|
|
4095
|
+
started: float,
|
|
4096
|
+
region: 'str | None',
|
|
4097
|
+
*,
|
|
4098
|
+
follow_up: 'str',
|
|
4099
|
+
warnings: 'list[str]',
|
|
4100
|
+
insights: 'list[str]',
|
|
4101
|
+
) -> 'Envelope':
|
|
4102
|
+
metadata = {
|
|
4103
|
+
"elapsed_ms": int((monotonic() - started) * 1000),
|
|
4104
|
+
"region": region or None,
|
|
4105
|
+
"backend": "mcp",
|
|
4106
|
+
}
|
|
4107
|
+
return Envelope(
|
|
4108
|
+
command=command,
|
|
4109
|
+
status="success",
|
|
4110
|
+
data=payload,
|
|
4111
|
+
metadata=metadata,
|
|
4112
|
+
agent_hints=AgentHints(
|
|
4113
|
+
actions=[action(follow_up, data=payload, metadata=metadata)],
|
|
4114
|
+
warnings=warnings,
|
|
4115
|
+
insights=insights,
|
|
4116
|
+
),
|
|
4117
|
+
)
|
|
4118
|
+
|
|
4119
|
+
def _call_kb_tool(self, tool: 'str', arguments: 'dict[str, Any]') -> 'dict[str, Any]':
|
|
4120
|
+
from .backend.mcp import McpError
|
|
4121
|
+
|
|
4122
|
+
client = self._mcp_client()
|
|
4123
|
+
try:
|
|
4124
|
+
result = client.call_tool(tool, arguments)
|
|
4125
|
+
except McpError as exc:
|
|
4126
|
+
raise BackendConnectionError(
|
|
4127
|
+
str(exc),
|
|
4128
|
+
suggestion=(
|
|
4129
|
+
"Confirm the MCP endpoint is reachable for your region, then retry. "
|
|
4130
|
+
"Do not conclude from this failure that the documentation lacks an answer."
|
|
4131
|
+
),
|
|
4132
|
+
) from exc
|
|
4133
|
+
# A tool-level error still answers HTTP 200, so it must not read as success.
|
|
4134
|
+
if result.get("isError"):
|
|
4135
|
+
content = result.get("content") or []
|
|
4136
|
+
detail = ""
|
|
4137
|
+
if content and isinstance(content[0], dict):
|
|
4138
|
+
detail = str(content[0].get("text") or "")
|
|
4139
|
+
raise BackendConnectionError(
|
|
4140
|
+
"The knowledge-base tool failed: "
|
|
4141
|
+
+ (detail[:300] or "no detail returned")
|
|
4142
|
+
)
|
|
4143
|
+
return self._kb_structured(result)
|
|
4144
|
+
|
|
3839
4145
|
def meta_list_schemas(self, *, project: 'str | None' = None) -> 'Envelope':
|
|
3840
4146
|
"""List all schemas in a project."""
|
|
3841
4147
|
target_project = project or self.config.default_project
|
|
@@ -5677,6 +5983,9 @@ class MaxCApp:
|
|
|
5677
5983
|
"remote_jobs": getattr(self.backend, "supports_remote_jobs", True) if self.backend else True,
|
|
5678
5984
|
"cost_check": getattr(self.backend, "supports_cost_check", True) if self.backend else True,
|
|
5679
5985
|
"lineage": False, # Always false for current ODPS backend
|
|
5986
|
+
# Reported from configuration only; probing the MCP endpoint would make
|
|
5987
|
+
# this local command reach the network.
|
|
5988
|
+
"knowledge_base": bool(self.config.mcp.enabled),
|
|
5680
5989
|
}
|
|
5681
5990
|
|
|
5682
5991
|
# Keep agent.context strictly local. Report Catalog search capability
|
|
@@ -66,8 +66,12 @@ class ResolvedAuthConnection:
|
|
|
66
66
|
_MINIMUM_PYODPS = "0.12.0"
|
|
67
67
|
|
|
68
68
|
def create_client(self):
|
|
69
|
+
from .enterprise_tls import configure_enterprise_tls_env
|
|
69
70
|
from .odps_runtime import configure_user_agent
|
|
70
71
|
|
|
72
|
+
# requests resolves SSL_CERT_FILE when a Session is constructed, so
|
|
73
|
+
# this must run before ODPS builds its REST and tunnel clients.
|
|
74
|
+
configure_enterprise_tls_env()
|
|
71
75
|
configure_user_agent()
|
|
72
76
|
try:
|
|
73
77
|
from odps import ODPS
|
|
@@ -5,7 +5,7 @@ from itertools import islice
|
|
|
5
5
|
from time import monotonic, sleep
|
|
6
6
|
from typing import Any
|
|
7
7
|
|
|
8
|
-
from ..exceptions import BackendConnectionError, JobTimeoutError, ValidationError
|
|
8
|
+
from ..exceptions import BackendConnectionError, JobTimeoutError, MaxCError, ValidationError
|
|
9
9
|
from ..helpers import (
|
|
10
10
|
OdpsNoSuchObject,
|
|
11
11
|
_dt_to_iso,
|
|
@@ -301,6 +301,71 @@ class JobMixin(QueryMixin):
|
|
|
301
301
|
"task_results": task_results,
|
|
302
302
|
}
|
|
303
303
|
|
|
304
|
+
def inspect_job(
|
|
305
|
+
self, job_id: 'str', *, section: 'str', project: 'str | None' = None,
|
|
306
|
+
task_name: 'str | None' = None, log_id: 'str | None' = None,
|
|
307
|
+
log_type: 'str' = "stdout", size: 'int' = 1048576,
|
|
308
|
+
session_context: 'dict[str, Any] | None' = None,
|
|
309
|
+
) -> 'dict[str, Any]':
|
|
310
|
+
"""Read task diagnostics without hiding service errors or raw metrics.
|
|
311
|
+
|
|
312
|
+
Args:
|
|
313
|
+
job_id: Instance ID resolved by the application.
|
|
314
|
+
section: task-detail, task-summary, workers, or worker-log.
|
|
315
|
+
project: Project owning the instance.
|
|
316
|
+
task_name: Explicit task name, or SDK single-task selection.
|
|
317
|
+
log_id: Worker log ID returned by worker discovery.
|
|
318
|
+
log_type: Supported PyODPS worker log type.
|
|
319
|
+
size: Positive requested log size in bytes.
|
|
320
|
+
session_context: Saved SQLRT routing context.
|
|
321
|
+
"""
|
|
322
|
+
if section not in {"task-detail", "task-summary", "workers", "worker-log"}:
|
|
323
|
+
raise ValidationError("Unknown job inspection section.")
|
|
324
|
+
if section == "worker-log":
|
|
325
|
+
from odps.models.worker import LOG_TYPES_MAPPING
|
|
326
|
+
|
|
327
|
+
if not log_id or not log_id.strip():
|
|
328
|
+
raise ValidationError("A non-empty worker log ID is required.")
|
|
329
|
+
if log_type not in LOG_TYPES_MAPPING or not isinstance(size, int) or size <= 0:
|
|
330
|
+
raise ValidationError("Choose a supported log type and a positive log size.")
|
|
331
|
+
instance = self._get_instance(job_id, project=project, session_context=session_context)
|
|
332
|
+
try:
|
|
333
|
+
if section == "worker-log":
|
|
334
|
+
return {"log_id": log_id, "log_type": log_type, "requested_size": size,
|
|
335
|
+
"content": instance.get_worker_log(log_id, log_type, size=size)}
|
|
336
|
+
if section == "task-summary":
|
|
337
|
+
if self._raw_attr(instance, "_subquery_id") is not None:
|
|
338
|
+
from ..exceptions import FeatureUnavailableError
|
|
339
|
+
|
|
340
|
+
raise FeatureUnavailableError(
|
|
341
|
+
"Task summaries are not scoped to SQLRT subqueries by PyODPS.",
|
|
342
|
+
suggestion="Use `job task-detail` for subquery-scoped metrics.",
|
|
343
|
+
)
|
|
344
|
+
summary = instance.get_task_summary(task_name)
|
|
345
|
+
return {"task_name": task_name, "available": summary is not None,
|
|
346
|
+
"summary": dict(summary) if summary is not None else None,
|
|
347
|
+
"summary_text": getattr(summary, "summary_text", None)}
|
|
348
|
+
detail = instance.get_task_detail2(task_name)
|
|
349
|
+
if section == "task-detail":
|
|
350
|
+
return {"task_name": task_name, "detail": detail}
|
|
351
|
+
if not isinstance(detail, (dict, list)):
|
|
352
|
+
from ..exceptions import FeatureUnavailableError
|
|
353
|
+
|
|
354
|
+
raise FeatureUnavailableError(
|
|
355
|
+
"Task detail is not structured JSON; worker discovery is unavailable.",
|
|
356
|
+
suggestion="Read `job task-detail` to inspect the returned detail.",
|
|
357
|
+
)
|
|
358
|
+
workers = instance.get_task_workers(task_name, json_obj=detail)
|
|
359
|
+
fields = ("id", "log_id", "type", "status", "start_time", "end_time",
|
|
360
|
+
"input_bytes", "input_records", "output_bytes", "output_records")
|
|
361
|
+
return {"task_name": task_name, "workers": [
|
|
362
|
+
{field: getattr(worker, field, None) for field in fields} for worker in workers
|
|
363
|
+
]}
|
|
364
|
+
except MaxCError:
|
|
365
|
+
raise
|
|
366
|
+
except Exception as exc:
|
|
367
|
+
raise translate_odps_error(exc, context="job") from exc
|
|
368
|
+
|
|
304
369
|
def list_jobs(
|
|
305
370
|
self, *, project: 'str | None' = None, limit: 'int' = 20
|
|
306
371
|
) -> 'tuple[list[JobInfo], bool]':
|