tagword-codeagent 1.1.6__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- codeagent/__init__.py +10 -0
- codeagent/__main__.py +13 -0
- codeagent/cli/__init__.py +13 -0
- codeagent/cli/entrypoint.py +161 -0
- codeagent/cli/main.py +979 -0
- codeagent/core/__init__.py +2 -0
- codeagent/core/attachments.py +342 -0
- codeagent/core/audio_models.py +50 -0
- codeagent/core/bootstrap.py +178 -0
- codeagent/core/default_presets.py +159 -0
- codeagent/core/env.py +106 -0
- codeagent/core/folder_picker.py +151 -0
- codeagent/core/image_understanding.py +90 -0
- codeagent/core/models.py +6 -0
- codeagent/core/paths.py +238 -0
- codeagent/core/pricing.py +107 -0
- codeagent/core/process_ports.py +258 -0
- codeagent/core/seed_bridge.py +21 -0
- codeagent/core/settings.py +98 -0
- codeagent/core/speech_synth.py +330 -0
- codeagent/core/team_config.py +247 -0
- codeagent/core/team_manager.py +245 -0
- codeagent/core/token_counter.py +41 -0
- codeagent/core/tts_voices.py +103 -0
- codeagent/core/usage_billing.py +72 -0
- codeagent/core/vision_models.py +93 -0
- codeagent/memory/diary.py +144 -0
- codeagent/persona_defaults/README.md +12 -0
- codeagent/persona_defaults/agent.md +352 -0
- codeagent/persona_defaults/identity.md +29 -0
- codeagent/persona_defaults/memory.md +4 -0
- codeagent/persona_defaults/skills.md +325 -0
- codeagent/persona_defaults/soul.md +38 -0
- codeagent/persona_defaults/tools.md +41 -0
- codeagent/persona_defaults/user.md +18 -0
- codeagent/runtime/__init__.py +11 -0
- codeagent/runtime/agent_context.py +6 -0
- codeagent/runtime/agent_runtime.py +21 -0
- codeagent/runtime/compact_prompt.py +9 -0
- codeagent/runtime/compact_state.py +40 -0
- codeagent/runtime/llm_worker.py +132 -0
- codeagent/runtime/orchestrator.py +161 -0
- codeagent/runtime/prompt_enrichment.py +324 -0
- codeagent/runtime/session_manager.py +30 -0
- codeagent/runtime/task_split.py +38 -0
- codeagent/runtime/turn_loop.py +13 -0
- codeagent/runtime/worker.py +48 -0
- codeagent/server/__init__.py +505 -0
- codeagent/server/app_factory.py +1292 -0
- codeagent/server/attachment_api.py +261 -0
- codeagent/server/hub.py +175 -0
- codeagent/server/icon.png +0 -0
- codeagent/server/self_healing.py +143 -0
- codeagent/server/team_engine.py +243 -0
- codeagent/server/tts_api.py +51 -0
- codeagent/server/webui_api_app.py +3180 -0
- codeagent/skills/__init__.py +2 -0
- codeagent/skills/select.py +144 -0
- codeagent/tools/__init__.py +6 -0
- codeagent/tools/agent_tools.py +10 -0
- codeagent/web/__init__.py +1 -0
- codeagent/web/__pycache__/__init__.cpython-313.pyc +0 -0
- codeagent/web/__pycache__/app.cpython-313.pyc +0 -0
- codeagent/web/__pycache__/auth_impl.cpython-313.pyc +0 -0
- codeagent/web/auth_impl.py +326 -0
- codeagent/web/static/00-marked.min.js +6 -0
- codeagent/web/static/00-mermaid.min.js +2314 -0
- codeagent/web/static/00-purify.min.js +3 -0
- codeagent/web/static/00-storage.js +83 -0
- codeagent/web/static/00-utils.js +51 -0
- codeagent/web/static/01-theme.css +96 -0
- codeagent/web/static/01a-scroll.js +86 -0
- codeagent/web/static/01b-think.js +62 -0
- codeagent/web/static/01c-session-identity.js +81 -0
- codeagent/web/static/01c-session-thinking.js +61 -0
- codeagent/web/static/01c-session-tree-menus.js +349 -0
- codeagent/web/static/01c-session-tree.js +531 -0
- codeagent/web/static/01c-session-utilities.js +83 -0
- codeagent/web/static/01c-vision-model.js +132 -0
- codeagent/web/static/01d-image-gen-model.js +84 -0
- codeagent/web/static/01e-audio-model.js +97 -0
- codeagent/web/static/01f-compose-ui.js +88 -0
- codeagent/web/static/01g-tts-settings.js +128 -0
- codeagent/web/static/01h-music-model.js +85 -0
- codeagent/web/static/01i-model-stack.js +291 -0
- codeagent/web/static/01j-video-gen-model.js +85 -0
- codeagent/web/static/01k-model-stack-bootstrap.js +66 -0
- codeagent/web/static/01k-model-stack-state.js +209 -0
- codeagent/web/static/01q-inflight.js +195 -0
- codeagent/web/static/02-reset.css +22 -0
- codeagent/web/static/02-session-ui.js +201 -0
- codeagent/web/static/03-layout-p1.css +2 -0
- codeagent/web/static/03-layout-p2.css +2 -0
- codeagent/web/static/03a-links-stream.js +88 -0
- codeagent/web/static/03b-stream-text.js +119 -0
- codeagent/web/static/03c-render-bubble.js +520 -0
- codeagent/web/static/03d-agent-bubble.js +126 -0
- codeagent/web/static/03e-tool-trace.js +182 -0
- codeagent/web/static/03f-timeline.js +54 -0
- codeagent/web/static/03g-bubble-tts.js +292 -0
- codeagent/web/static/04-components-p1.css +1 -0
- codeagent/web/static/04-components-p2b.css +1 -0
- codeagent/web/static/04-components-p3.css +2 -0
- codeagent/web/static/04-components-p4.css +1 -0
- codeagent/web/static/04-components-p5.css +1 -0
- codeagent/web/static/04-components-p6.css +58 -0
- codeagent/web/static/04-components-p7.css +4 -0
- codeagent/web/static/04-components-p8.css +1 -0
- codeagent/web/static/04-components-p9.css +1 -0
- codeagent/web/static/04-ws-connect.js +351 -0
- codeagent/web/static/04-ws-helpers.js +116 -0
- codeagent/web/static/05-chat-p1.css +1058 -0
- codeagent/web/static/05-chat-p2.css +1 -0
- codeagent/web/static/05-chat-p3.css +295 -0
- codeagent/web/static/05a-session-history.js +176 -0
- codeagent/web/static/05b-session-list.js +114 -0
- codeagent/web/static/05c-dialog.js +123 -0
- codeagent/web/static/05d-project-create.js +172 -0
- codeagent/web/static/05e-project-remote.js +143 -0
- codeagent/web/static/06-chat.js +552 -0
- codeagent/web/static/06-utilities.css +181 -0
- codeagent/web/static/06b-attachments.js +304 -0
- codeagent/web/static/06c-camera.js +488 -0
- codeagent/web/static/07-activity-bar.css +127 -0
- codeagent/web/static/09-git-p1.css +1 -0
- codeagent/web/static/09-git-p2.css +1 -0
- codeagent/web/static/09a-init-bind.js +110 -0
- codeagent/web/static/09b-panel-toggle.js +80 -0
- codeagent/web/static/09c-api-refresh.js +100 -0
- codeagent/web/static/09d-status-log.js +80 -0
- codeagent/web/static/09e-commit.js +80 -0
- codeagent/web/static/09f-remote.js +93 -0
- codeagent/web/static/11a-cron-parse.js +117 -0
- codeagent/web/static/11b-cron-panel.js +100 -0
- codeagent/web/static/11c-cron-card.js +133 -0
- codeagent/web/static/11d-cron-form.js +105 -0
- codeagent/web/static/11e-env-config.js +168 -0
- codeagent/web/static/11f-env-llm-a-state.js +136 -0
- codeagent/web/static/11f-env-llm-b-form.js +409 -0
- codeagent/web/static/11f-env-llm-c-board.js +322 -0
- codeagent/web/static/11g-env-paths-git.js +973 -0
- codeagent/web/static/11h-env-mcp-a-state.js +161 -0
- codeagent/web/static/11h-env-mcp-b-form.js +229 -0
- codeagent/web/static/11h-env-mcp-c-board.js +565 -0
- codeagent/web/static/12-plugins.js +127 -0
- codeagent/web/static/13a-skills.js +156 -0
- codeagent/web/static/13a-todo-utils.js +140 -0
- codeagent/web/static/13b-todo-fetch.js +106 -0
- codeagent/web/static/13b-tools.js +127 -0
- codeagent/web/static/13c-todo-crud.js +92 -0
- codeagent/web/static/14-activity-bar.js +177 -0
- codeagent/web/static/15-plans.css +194 -0
- codeagent/web/static/15-plans.js +219 -0
- codeagent/web/static/16-mobile.css +612 -0
- codeagent/web/static/16-mobile.js +178 -0
- codeagent/web/static/17-preset-ux.css +287 -0
- codeagent/web/static/18-agent-mgr.css +616 -0
- codeagent/web/static/18-agent-mgr.js +204 -0
- codeagent/web/static/24-health-dashboard.js +68 -0
- codeagent/web/static/99-brand-polish.css +152 -0
- codeagent/web/static/body.html +824 -0
- codeagent/web/static/setup/setup_form.html +171 -0
- codeagent/web/static/setup/setup_head.html +100 -0
- codeagent/web/static/setup/setup_tail.html +264 -0
- codeagent/web/static/setup/setup_test.html +38 -0
- codeagent/web/web_login.html +265 -0
- codeagent/web/webui.html +35 -0
- tagword_codeagent-1.1.6.dist-info/METADATA +273 -0
- tagword_codeagent-1.1.6.dist-info/RECORD +173 -0
- tagword_codeagent-1.1.6.dist-info/WHEEL +5 -0
- tagword_codeagent-1.1.6.dist-info/entry_points.txt +3 -0
- tagword_codeagent-1.1.6.dist-info/licenses/LICENSE +21 -0
- tagword_codeagent-1.1.6.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"""Web UI: plugin toggles and which config/*.md files feed the system prompt."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
import logging
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from seed.core.config_plane import CONFIG_FILENAMES, config_dir
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
PLUGINS_FILENAME = "codeagent.plugins.json"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _defaults() -> dict[str, Any]:
|
|
17
|
+
return {
|
|
18
|
+
"config_md_enabled": list(CONFIG_FILENAMES),
|
|
19
|
+
"plugins": {},
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _plugins_path(base: Path | None = None) -> Path:
|
|
24
|
+
cfg = config_dir() if base is None else base.resolve() / "config"
|
|
25
|
+
return cfg / PLUGINS_FILENAME
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def load_plugins(base: Path | None = None) -> dict[str, Any]:
|
|
29
|
+
d = _defaults()
|
|
30
|
+
path = _plugins_path(base)
|
|
31
|
+
if not path.is_file():
|
|
32
|
+
return d
|
|
33
|
+
try:
|
|
34
|
+
raw = json.loads(path.read_text(encoding="utf-8"))
|
|
35
|
+
except (json.JSONDecodeError, OSError) as e:
|
|
36
|
+
logger.warning("ignore bad %s: %s", path, e)
|
|
37
|
+
return d
|
|
38
|
+
if not isinstance(raw, dict):
|
|
39
|
+
return d
|
|
40
|
+
if isinstance(raw.get("config_md_enabled"), list):
|
|
41
|
+
sel = [
|
|
42
|
+
x
|
|
43
|
+
for x in raw["config_md_enabled"]
|
|
44
|
+
if isinstance(x, str) and x in CONFIG_FILENAMES
|
|
45
|
+
]
|
|
46
|
+
if sel:
|
|
47
|
+
d["config_md_enabled"] = sel
|
|
48
|
+
pl = raw.get("plugins")
|
|
49
|
+
if isinstance(pl, dict):
|
|
50
|
+
d["plugins"] = pl
|
|
51
|
+
return d
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def save_plugins(data: dict[str, Any], base: Path | None = None) -> None:
|
|
55
|
+
path = _plugins_path(base)
|
|
56
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
57
|
+
path.write_text(json.dumps(data, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
|
58
|
+
|
|
59
|
+
def get_system_prompt_filenames(base: Path | None = None) -> list[str]:
|
|
60
|
+
data = load_plugins(base) if base is not None else load_plugins()
|
|
61
|
+
enabled = data.get("config_md_enabled") or list(CONFIG_FILENAMES)
|
|
62
|
+
if not enabled:
|
|
63
|
+
return list(CONFIG_FILENAMES)
|
|
64
|
+
s = set(enabled)
|
|
65
|
+
ordered = [f for f in CONFIG_FILENAMES if f in s]
|
|
66
|
+
if not ordered:
|
|
67
|
+
return list(CONFIG_FILENAMES)
|
|
68
|
+
return ordered
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def get_tool_exclude_prefixes() -> tuple[str, ...]:
|
|
72
|
+
return ()
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def plugins_public_view() -> dict[str, Any]:
|
|
76
|
+
d = load_plugins()
|
|
77
|
+
en = set(d.get("config_md_enabled") or CONFIG_FILENAMES)
|
|
78
|
+
return {
|
|
79
|
+
"config_filenames": list(CONFIG_FILENAMES),
|
|
80
|
+
"config_md": [{"name": f, "enabled": f in en} for f in CONFIG_FILENAMES],
|
|
81
|
+
"plugins": d.get("plugins", _defaults()["plugins"]),
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def save_plugins_from_ui(body: dict[str, Any]) -> dict[str, Any]:
|
|
86
|
+
current = load_plugins()
|
|
87
|
+
enabled = body.get("config_md_enabled")
|
|
88
|
+
if enabled is not None:
|
|
89
|
+
if not isinstance(enabled, list):
|
|
90
|
+
raise ValueError("config_md_enabled must be a list of filenames")
|
|
91
|
+
sel = [x for x in enabled if isinstance(x, str) and x in CONFIG_FILENAMES]
|
|
92
|
+
if not sel:
|
|
93
|
+
sel = list(CONFIG_FILENAMES)
|
|
94
|
+
current["config_md_enabled"] = sel
|
|
95
|
+
pl = body.get("plugins")
|
|
96
|
+
if isinstance(pl, dict):
|
|
97
|
+
current["plugins"] = pl
|
|
98
|
+
save_plugins(current)
|
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
"""MiniMax text-to-speech (HTTP T2A) for Web UI bubble playback."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
import os
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Optional, Tuple
|
|
10
|
+
|
|
11
|
+
import requests
|
|
12
|
+
|
|
13
|
+
from seed.integrations.mcp_config import MINIMAX_MCP_SERVER_ID, get_server_config
|
|
14
|
+
|
|
15
|
+
from codeagent.core.tts_voices import (
|
|
16
|
+
default_tts_model,
|
|
17
|
+
default_tts_voice_id,
|
|
18
|
+
normalize_tts_model,
|
|
19
|
+
normalize_tts_voice_id,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
logger = logging.getLogger(__name__)
|
|
23
|
+
|
|
24
|
+
_QUOTA_STATUS_CODES = frozenset({1002, 1008, 2056})
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass(frozen=True)
|
|
28
|
+
class TtsSynthOutcome:
|
|
29
|
+
audio: Optional[bytes] = None
|
|
30
|
+
mime: str = "audio/mpeg"
|
|
31
|
+
error: str = ""
|
|
32
|
+
status_code: Optional[int] = None
|
|
33
|
+
|
|
34
|
+
@property
|
|
35
|
+
def ok(self) -> bool:
|
|
36
|
+
return bool(self.audio)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _base_resp_status_code(base_resp: object) -> tuple[int, str]:
|
|
40
|
+
if not isinstance(base_resp, dict):
|
|
41
|
+
return 0, ""
|
|
42
|
+
raw_code = base_resp.get("status_code", 0)
|
|
43
|
+
try:
|
|
44
|
+
code = int(raw_code)
|
|
45
|
+
except (TypeError, ValueError):
|
|
46
|
+
code = 0
|
|
47
|
+
msg = str(base_resp.get("status_msg") or "").strip()
|
|
48
|
+
return code, msg
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _decode_t2a_audio(raw_audio: object, *, timeout: int) -> tuple[Optional[bytes], str]:
|
|
52
|
+
if raw_audio is None:
|
|
53
|
+
return None, ""
|
|
54
|
+
audio_str = str(raw_audio).strip()
|
|
55
|
+
if not audio_str:
|
|
56
|
+
return None, ""
|
|
57
|
+
if audio_str.startswith("http://") or audio_str.startswith("https://"):
|
|
58
|
+
try:
|
|
59
|
+
r = requests.get(audio_str, timeout=max(10, timeout))
|
|
60
|
+
r.raise_for_status()
|
|
61
|
+
mime = r.headers.get("Content-Type") or "audio/mpeg"
|
|
62
|
+
return r.content, str(mime).split(";")[0].strip() or "audio/mpeg"
|
|
63
|
+
except Exception:
|
|
64
|
+
logger.debug("MiniMax TTS audio URL download failed", exc_info=True)
|
|
65
|
+
return None, ""
|
|
66
|
+
try:
|
|
67
|
+
audio_bytes = bytes.fromhex(audio_str)
|
|
68
|
+
except ValueError:
|
|
69
|
+
logger.debug("MiniMax TTS invalid hex audio", exc_info=True)
|
|
70
|
+
return None, ""
|
|
71
|
+
if not audio_bytes:
|
|
72
|
+
return None, ""
|
|
73
|
+
return audio_bytes, "audio/mpeg"
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def synthesize_minimax_speech_with_detail(
|
|
77
|
+
text: str,
|
|
78
|
+
base: Optional[Path] = None,
|
|
79
|
+
*,
|
|
80
|
+
voice_id: Optional[str] = None,
|
|
81
|
+
model: Optional[str] = None,
|
|
82
|
+
) -> TtsSynthOutcome:
|
|
83
|
+
"""Return structured TTS result with MiniMax error details when synthesis fails."""
|
|
84
|
+
key, host = get_minimax_tts_credentials(base)
|
|
85
|
+
if not key:
|
|
86
|
+
return TtsSynthOutcome(error="MiniMax TTS not configured")
|
|
87
|
+
|
|
88
|
+
raw = (text or "").strip()
|
|
89
|
+
if not raw:
|
|
90
|
+
return TtsSynthOutcome(error="text required")
|
|
91
|
+
raw = raw[: _max_tts_chars()]
|
|
92
|
+
|
|
93
|
+
model_id = normalize_tts_model(model)
|
|
94
|
+
voice = normalize_tts_voice_id(voice_id)
|
|
95
|
+
|
|
96
|
+
payload = {
|
|
97
|
+
"model": model_id,
|
|
98
|
+
"text": raw,
|
|
99
|
+
"stream": False,
|
|
100
|
+
"output_format": "hex",
|
|
101
|
+
"language_boost": "auto",
|
|
102
|
+
"voice_setting": {
|
|
103
|
+
"voice_id": voice,
|
|
104
|
+
"speed": 1,
|
|
105
|
+
"vol": 1,
|
|
106
|
+
"pitch": 0,
|
|
107
|
+
},
|
|
108
|
+
"audio_setting": {
|
|
109
|
+
"sample_rate": 32000,
|
|
110
|
+
"bitrate": 128000,
|
|
111
|
+
"format": "mp3",
|
|
112
|
+
"channel": 1,
|
|
113
|
+
},
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
try:
|
|
117
|
+
timeout = int(os.environ.get("CODEAGENT_TTS_TIMEOUT_SEC", "120") or 120)
|
|
118
|
+
except ValueError:
|
|
119
|
+
timeout = 120
|
|
120
|
+
|
|
121
|
+
try:
|
|
122
|
+
resp = requests.post(
|
|
123
|
+
_t2a_url(host),
|
|
124
|
+
json=payload,
|
|
125
|
+
headers={"Authorization": f"Bearer {key}", "Content-Type": "application/json"},
|
|
126
|
+
timeout=max(10, timeout),
|
|
127
|
+
)
|
|
128
|
+
resp.raise_for_status()
|
|
129
|
+
body = resp.json()
|
|
130
|
+
except requests.HTTPError as e:
|
|
131
|
+
detail = ""
|
|
132
|
+
try:
|
|
133
|
+
err_body = e.response.json() if e.response is not None else {}
|
|
134
|
+
br = err_body.get("base_resp") if isinstance(err_body, dict) else None
|
|
135
|
+
code, msg = _base_resp_status_code(br)
|
|
136
|
+
detail = msg or str(e)
|
|
137
|
+
if code:
|
|
138
|
+
return TtsSynthOutcome(error=detail or f"MiniMax TTS HTTP error ({code})", status_code=code)
|
|
139
|
+
except Exception:
|
|
140
|
+
detail = str(e)
|
|
141
|
+
logger.warning("MiniMax TTS HTTP error: %s", detail or e)
|
|
142
|
+
return TtsSynthOutcome(error=detail or "MiniMax TTS request failed")
|
|
143
|
+
except Exception as e:
|
|
144
|
+
logger.warning("MiniMax TTS request failed: %s", e, exc_info=True)
|
|
145
|
+
return TtsSynthOutcome(error=str(e) or "MiniMax TTS request failed")
|
|
146
|
+
|
|
147
|
+
if not isinstance(body, dict):
|
|
148
|
+
return TtsSynthOutcome(error="MiniMax TTS returned invalid JSON")
|
|
149
|
+
|
|
150
|
+
code, msg = _base_resp_status_code(body.get("base_resp"))
|
|
151
|
+
if code != 0:
|
|
152
|
+
logger.info("MiniMax TTS base_resp error: %s", body.get("base_resp"))
|
|
153
|
+
return TtsSynthOutcome(
|
|
154
|
+
error=_humanize_tts_error(code, msg, key=key),
|
|
155
|
+
status_code=code,
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
data = body.get("data")
|
|
159
|
+
if not isinstance(data, dict):
|
|
160
|
+
return TtsSynthOutcome(error="MiniMax TTS returned no data")
|
|
161
|
+
|
|
162
|
+
status = data.get("status")
|
|
163
|
+
if status is not None:
|
|
164
|
+
try:
|
|
165
|
+
if int(status) != 2:
|
|
166
|
+
return TtsSynthOutcome(
|
|
167
|
+
error=f"MiniMax TTS incomplete (status={status})",
|
|
168
|
+
status_code=code,
|
|
169
|
+
)
|
|
170
|
+
except (TypeError, ValueError):
|
|
171
|
+
pass
|
|
172
|
+
|
|
173
|
+
audio_bytes, mime = _decode_t2a_audio(data.get("audio"), timeout=timeout)
|
|
174
|
+
if not audio_bytes:
|
|
175
|
+
return TtsSynthOutcome(error=msg or "MiniMax TTS returned no audio", status_code=code)
|
|
176
|
+
return TtsSynthOutcome(audio=audio_bytes, mime=mime or "audio/mpeg")
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def synthesize_minimax_speech(
|
|
180
|
+
text: str,
|
|
181
|
+
base: Optional[Path] = None,
|
|
182
|
+
*,
|
|
183
|
+
voice_id: Optional[str] = None,
|
|
184
|
+
model: Optional[str] = None,
|
|
185
|
+
) -> Optional[Tuple[bytes, str]]:
|
|
186
|
+
"""Return (mp3 bytes, mime) or None when MiniMax TTS is not configured."""
|
|
187
|
+
out = synthesize_minimax_speech_with_detail(
|
|
188
|
+
text,
|
|
189
|
+
base,
|
|
190
|
+
voice_id=voice_id,
|
|
191
|
+
model=model,
|
|
192
|
+
)
|
|
193
|
+
if not out.ok or out.audio is None:
|
|
194
|
+
return None
|
|
195
|
+
return out.audio, out.mime
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def tts_http_status_for_outcome(out: TtsSynthOutcome) -> int:
|
|
199
|
+
code = out.status_code
|
|
200
|
+
if code in _QUOTA_STATUS_CODES:
|
|
201
|
+
return 429
|
|
202
|
+
if code in {1004, 2049}:
|
|
203
|
+
return 401
|
|
204
|
+
if code in {2013, 1042}:
|
|
205
|
+
return 422
|
|
206
|
+
return 502
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def _max_tts_chars() -> int:
|
|
210
|
+
try:
|
|
211
|
+
return max(1, min(int(os.environ.get("CODEAGENT_TTS_MAX_CHARS", "2000") or 2000), 10000))
|
|
212
|
+
except ValueError:
|
|
213
|
+
return 2000
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def _tts_api_key_from_env() -> str:
|
|
217
|
+
from codeagent.core import env as ca_env
|
|
218
|
+
|
|
219
|
+
return ca_env.pick_nonempty("CODEAGENT_TTS_API_KEY")
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def _tts_api_key_from_mcp(base: Optional[Path] = None) -> tuple[str, str]:
|
|
223
|
+
cfg = get_server_config(MINIMAX_MCP_SERVER_ID, base)
|
|
224
|
+
if not cfg:
|
|
225
|
+
return "", ""
|
|
226
|
+
env = cfg.env or {}
|
|
227
|
+
key = str(env.get("MINIMAX_TTS_API_KEY") or "").strip()
|
|
228
|
+
if not key:
|
|
229
|
+
return "", ""
|
|
230
|
+
host = str(env.get("MINIMAX_TTS_API_HOST") or env.get("MINIMAX_API_HOST") or "https://api.minimaxi.com")
|
|
231
|
+
return key, host.strip().rstrip("/")
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def _humanize_tts_error(code: Optional[int], msg: str, *, key: str = "") -> str:
|
|
235
|
+
base = (msg or "").strip() or (f"MiniMax TTS error (status_code={code})" if code else "MiniMax TTS failed")
|
|
236
|
+
if code != 2056:
|
|
237
|
+
return base
|
|
238
|
+
if "0/0" in base or "Token Plan" in base:
|
|
239
|
+
hint = (
|
|
240
|
+
"Token Plan Key(sk-cp-…)通常可用于聊天,但 TTS(Speech)有独立的每日字符额度;"
|
|
241
|
+
"当前返回 0/0 表示套餐未分配朗读额度,并非「已用完」。"
|
|
242
|
+
"请在 MiniMax 开放平台「账户管理 → 接口密钥」创建按量 API Key,"
|
|
243
|
+
"填到配置页「朗读 API Key(开放平台按量,可选)」后重试。"
|
|
244
|
+
)
|
|
245
|
+
if key.startswith("sk-cp-"):
|
|
246
|
+
return f"{base}。{hint}"
|
|
247
|
+
return f"{base}。{hint}"
|
|
248
|
+
return base
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def minimax_tts_configured(base: Optional[Path] = None) -> bool:
|
|
252
|
+
key, _host = get_minimax_tts_credentials(base)
|
|
253
|
+
return bool(key)
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def get_minimax_tts_credentials(base: Optional[Path] = None) -> Tuple[str, str]:
|
|
257
|
+
"""TTS credentials: dedicated pay-as-you-go key first, then MCP / LLM preset."""
|
|
258
|
+
tts_key = _tts_api_key_from_env()
|
|
259
|
+
if tts_key:
|
|
260
|
+
host = _host_from_preset_base_url(
|
|
261
|
+
os.environ.get("CODEAGENT_TTS_API_HOST", "https://api.minimaxi.com")
|
|
262
|
+
)
|
|
263
|
+
return tts_key, host
|
|
264
|
+
|
|
265
|
+
mcp_tts_key, mcp_tts_host = _tts_api_key_from_mcp(base)
|
|
266
|
+
if mcp_tts_key:
|
|
267
|
+
return mcp_tts_key, mcp_tts_host
|
|
268
|
+
|
|
269
|
+
cfg = get_server_config(MINIMAX_MCP_SERVER_ID, base)
|
|
270
|
+
if cfg:
|
|
271
|
+
env = cfg.env or {}
|
|
272
|
+
key = str(env.get("MINIMAX_API_KEY") or "").strip()
|
|
273
|
+
if key:
|
|
274
|
+
host = str(env.get("MINIMAX_API_HOST") or "https://api.minimaxi.com").strip().rstrip("/")
|
|
275
|
+
return key, host
|
|
276
|
+
return _minimax_credentials_from_llm_presets(base)
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def _host_from_preset_base_url(base_url: str) -> str:
|
|
280
|
+
raw = (base_url or "").strip().rstrip("/")
|
|
281
|
+
if not raw:
|
|
282
|
+
return "https://api.minimaxi.com"
|
|
283
|
+
if raw.endswith("/v1/t2a_v2"):
|
|
284
|
+
return raw[: -len("/v1/t2a_v2")]
|
|
285
|
+
if raw.endswith("/v1"):
|
|
286
|
+
return raw[: -len("/v1")]
|
|
287
|
+
return raw
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def _minimax_credentials_from_llm_presets(base: Optional[Path] = None) -> Tuple[str, str]:
|
|
291
|
+
"""Use MiniMax LLM preset api_key (e.g. MiniMax-M2.7 chat) for T2A — same platform key."""
|
|
292
|
+
try:
|
|
293
|
+
from seed.core.llm_presets import get_default_preset_id, load_presets
|
|
294
|
+
from seed_model_providers import resolve_provider_for_preset
|
|
295
|
+
except Exception:
|
|
296
|
+
return "", "https://api.minimaxi.com"
|
|
297
|
+
|
|
298
|
+
default_id = get_default_preset_id().strip()
|
|
299
|
+
ranked: list[tuple[int, str, str, str]] = []
|
|
300
|
+
for p in load_presets():
|
|
301
|
+
if resolve_provider_for_preset(p) != "minimax":
|
|
302
|
+
continue
|
|
303
|
+
key = str(p.get("api_key") or "").strip()
|
|
304
|
+
if not key:
|
|
305
|
+
continue
|
|
306
|
+
pid = str(p.get("id") or "").strip()
|
|
307
|
+
host = _host_from_preset_base_url(str(p.get("base_url") or ""))
|
|
308
|
+
use_type = str(p.get("use_type") or "").strip().lower()
|
|
309
|
+
if pid and pid == default_id:
|
|
310
|
+
rank = 0
|
|
311
|
+
elif use_type in ("", "chat") or "M2" in str(p.get("model") or ""):
|
|
312
|
+
rank = 1
|
|
313
|
+
else:
|
|
314
|
+
rank = 2
|
|
315
|
+
ranked.append((rank, pid, key, host))
|
|
316
|
+
|
|
317
|
+
if not ranked:
|
|
318
|
+
return "", "https://api.minimaxi.com"
|
|
319
|
+
ranked.sort(key=lambda x: (x[0], x[1]))
|
|
320
|
+
_rank, _pid, key, host = ranked[0]
|
|
321
|
+
return key, host
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def _t2a_url(host: str) -> str:
|
|
325
|
+
base = (host or "").strip().rstrip("/")
|
|
326
|
+
if base.endswith("/v1/t2a_v2"):
|
|
327
|
+
return base
|
|
328
|
+
if base.endswith("/v1"):
|
|
329
|
+
return f"{base}/t2a_v2"
|
|
330
|
+
return f"{base}/v1/t2a_v2"
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
"""Team configuration — parse .codeagent/config/team.json into typed data objects.
|
|
2
|
+
|
|
3
|
+
Usage::
|
|
4
|
+
config = TeamConfig.from_file(".codeagent/config/team.json")
|
|
5
|
+
lead = config.lead
|
|
6
|
+
for member in config.members:
|
|
7
|
+
print(member.id, member.name)
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import json
|
|
13
|
+
import logging
|
|
14
|
+
from dataclasses import dataclass, field
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Any, Dict, List, Optional
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
# Default team tools auto-injected into Lead
|
|
21
|
+
TEAM_TOOLS = ["call_agent", "dispatch", "parallel"]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass
|
|
25
|
+
class AgentConfig:
|
|
26
|
+
"""Configuration for a single agent (lead or member)."""
|
|
27
|
+
|
|
28
|
+
id: str
|
|
29
|
+
name: str
|
|
30
|
+
persona: str
|
|
31
|
+
tools: List[str] = field(default_factory=list)
|
|
32
|
+
metadata: Dict[str, Any] = field(default_factory=dict)
|
|
33
|
+
|
|
34
|
+
@classmethod
|
|
35
|
+
def from_dict(cls, d: dict) -> AgentConfig:
|
|
36
|
+
"""Create from parsed JSON dict."""
|
|
37
|
+
return cls(
|
|
38
|
+
id=d["id"],
|
|
39
|
+
name=d.get("name", d["id"]),
|
|
40
|
+
persona=d.get("persona", ""),
|
|
41
|
+
tools=d.get("tools", []),
|
|
42
|
+
metadata=d.get("metadata", {}),
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@dataclass
|
|
47
|
+
class TeamConfig:
|
|
48
|
+
"""Complete team configuration parsed from team.json."""
|
|
49
|
+
|
|
50
|
+
version: str = "1.0"
|
|
51
|
+
lead: Optional[AgentConfig] = None
|
|
52
|
+
members: List[AgentConfig] = field(default_factory=list)
|
|
53
|
+
metadata: Dict[str, Any] = field(default_factory=dict)
|
|
54
|
+
|
|
55
|
+
@classmethod
|
|
56
|
+
def from_file(cls, path: Path | str) -> TeamConfig:
|
|
57
|
+
"""Parse a team.json file into a TeamConfig instance.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
path: Path to team.json file.
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
TeamConfig instance.
|
|
64
|
+
|
|
65
|
+
Raises:
|
|
66
|
+
FileNotFoundError: If the file doesn't exist.
|
|
67
|
+
ValueError: If the file is invalid JSON or missing required fields.
|
|
68
|
+
"""
|
|
69
|
+
path = Path(path)
|
|
70
|
+
if not path.is_file():
|
|
71
|
+
raise FileNotFoundError(f"Team config not found: {path}")
|
|
72
|
+
|
|
73
|
+
try:
|
|
74
|
+
data = json.loads(path.read_text(encoding="utf-8"))
|
|
75
|
+
except json.JSONDecodeError as e:
|
|
76
|
+
raise ValueError(f"Invalid JSON in team config: {e}")
|
|
77
|
+
|
|
78
|
+
return cls.from_dict(data, config_dir=path.parent)
|
|
79
|
+
|
|
80
|
+
@classmethod
|
|
81
|
+
def from_dict(cls, data: dict, config_dir: Optional[Path] = None) -> TeamConfig:
|
|
82
|
+
"""Create from parsed JSON dict, with optional base dir for persona paths."""
|
|
83
|
+
version = data.get("version", "1.0")
|
|
84
|
+
metadata = data.get("metadata", {})
|
|
85
|
+
|
|
86
|
+
lead_data = data.get("lead")
|
|
87
|
+
if not lead_data:
|
|
88
|
+
raise ValueError("team.json: 'lead' is required")
|
|
89
|
+
|
|
90
|
+
lead = AgentConfig.from_dict(lead_data)
|
|
91
|
+
# Auto-inject team tools to Lead
|
|
92
|
+
for tool in TEAM_TOOLS:
|
|
93
|
+
if tool not in lead.tools:
|
|
94
|
+
lead.tools.append(tool)
|
|
95
|
+
|
|
96
|
+
# Resolve lead persona path
|
|
97
|
+
lead.persona = _resolve_path(lead.persona, config_dir)
|
|
98
|
+
|
|
99
|
+
members = []
|
|
100
|
+
for m in data.get("members", []):
|
|
101
|
+
agent = AgentConfig.from_dict(m)
|
|
102
|
+
agent.persona = _resolve_path(agent.persona, config_dir)
|
|
103
|
+
members.append(agent)
|
|
104
|
+
|
|
105
|
+
config = cls(version=version, lead=lead, members=members, metadata=metadata)
|
|
106
|
+
config._validate()
|
|
107
|
+
return config
|
|
108
|
+
|
|
109
|
+
def _validate(self) -> None:
|
|
110
|
+
"""Validate config for required fields and consistency."""
|
|
111
|
+
errors: List[str] = []
|
|
112
|
+
|
|
113
|
+
if not self.lead:
|
|
114
|
+
errors.append("Lead agent is required")
|
|
115
|
+
|
|
116
|
+
# Check for duplicate IDs
|
|
117
|
+
ids = [self.lead.id] if self.lead else []
|
|
118
|
+
for m in self.members:
|
|
119
|
+
if m.id in ids:
|
|
120
|
+
errors.append(f"Duplicate agent id: {m.id!r}")
|
|
121
|
+
ids.append(m.id)
|
|
122
|
+
|
|
123
|
+
# Check persona files exist (warn, not error — they'll use defaults)
|
|
124
|
+
all_agents = [self.lead] + self.members if self.lead else self.members
|
|
125
|
+
for agent in all_agents:
|
|
126
|
+
if agent.persona:
|
|
127
|
+
p = Path(agent.persona)
|
|
128
|
+
if not p.is_file():
|
|
129
|
+
logger.warning(f"Persona file not found for '{agent.id}': {agent.persona} — will use defaults")
|
|
130
|
+
|
|
131
|
+
if errors:
|
|
132
|
+
raise ValueError("Invalid team config:\n" + "\n".join(errors))
|
|
133
|
+
|
|
134
|
+
def as_dict(self) -> dict:
|
|
135
|
+
"""Serialize back to dict (for debugging / display)."""
|
|
136
|
+
return {
|
|
137
|
+
"version": self.version,
|
|
138
|
+
"lead": {
|
|
139
|
+
"id": self.lead.id,
|
|
140
|
+
"name": self.lead.name,
|
|
141
|
+
"persona": self.lead.persona,
|
|
142
|
+
"tools": self.lead.tools,
|
|
143
|
+
} if self.lead else None,
|
|
144
|
+
"members": [
|
|
145
|
+
{
|
|
146
|
+
"id": m.id,
|
|
147
|
+
"name": m.name,
|
|
148
|
+
"persona": m.persona,
|
|
149
|
+
"tools": m.tools,
|
|
150
|
+
}
|
|
151
|
+
for m in self.members
|
|
152
|
+
],
|
|
153
|
+
"metadata": self.metadata,
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
def to_json(self, indent: int = 2) -> str:
|
|
157
|
+
"""Serialize to JSON string."""
|
|
158
|
+
return json.dumps(self.as_dict(), indent=indent, ensure_ascii=False)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _resolve_path(persona_path: str, config_dir: Optional[Path]) -> str:
|
|
162
|
+
"""Resolve a persona path relative to config dir if it's a relative path."""
|
|
163
|
+
if not persona_path or not config_dir:
|
|
164
|
+
return persona_path
|
|
165
|
+
p = Path(persona_path)
|
|
166
|
+
if not p.is_absolute():
|
|
167
|
+
resolved = (config_dir / p).resolve()
|
|
168
|
+
return str(resolved)
|
|
169
|
+
return persona_path
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def create_default_team_config(config_dir: Path) -> TeamConfig:
|
|
173
|
+
"""Create a default team.json with standard 8-member roles.
|
|
174
|
+
|
|
175
|
+
Args:
|
|
176
|
+
config_dir: The .codeagent/config directory path.
|
|
177
|
+
|
|
178
|
+
Returns:
|
|
179
|
+
A TeamConfig with default roles.
|
|
180
|
+
"""
|
|
181
|
+
# Persona paths relative to config dir
|
|
182
|
+
def persona(name: str) -> str:
|
|
183
|
+
return str(config_dir / ".." / "personas" / f"{name}.md")
|
|
184
|
+
|
|
185
|
+
data = {
|
|
186
|
+
"version": "1.0",
|
|
187
|
+
"lead": {
|
|
188
|
+
"id": "lead",
|
|
189
|
+
"name": "Lead Developer",
|
|
190
|
+
"persona": persona("lead"),
|
|
191
|
+
"tools": [
|
|
192
|
+
"file_read", "file_write", "file_edit",
|
|
193
|
+
"bash", "git", "grep", "glob",
|
|
194
|
+
],
|
|
195
|
+
},
|
|
196
|
+
"members": [
|
|
197
|
+
{
|
|
198
|
+
"id": "pm",
|
|
199
|
+
"name": "产品经理",
|
|
200
|
+
"persona": persona("pm"),
|
|
201
|
+
"tools": ["file_read", "file_write", "web_search"],
|
|
202
|
+
},
|
|
203
|
+
{
|
|
204
|
+
"id": "ux_designer",
|
|
205
|
+
"name": "UX 设计师",
|
|
206
|
+
"persona": persona("ux-designer"),
|
|
207
|
+
"tools": ["file_read", "file_write", "browser_screenshot"],
|
|
208
|
+
},
|
|
209
|
+
{
|
|
210
|
+
"id": "frontend_dev",
|
|
211
|
+
"name": "前端工程师",
|
|
212
|
+
"persona": persona("frontend"),
|
|
213
|
+
"tools": ["file_read", "file_write", "bash", "browser_screenshot"],
|
|
214
|
+
},
|
|
215
|
+
{
|
|
216
|
+
"id": "backend_dev",
|
|
217
|
+
"name": "后端工程师",
|
|
218
|
+
"persona": persona("backend"),
|
|
219
|
+
"tools": ["file_read", "file_write", "bash", "db"],
|
|
220
|
+
},
|
|
221
|
+
{
|
|
222
|
+
"id": "test_dev",
|
|
223
|
+
"name": "测试工程师",
|
|
224
|
+
"persona": persona("test"),
|
|
225
|
+
"tools": ["file_read", "bash", "test_run"],
|
|
226
|
+
},
|
|
227
|
+
{
|
|
228
|
+
"id": "reviewer",
|
|
229
|
+
"name": "代码审查员",
|
|
230
|
+
"persona": persona("reviewer"),
|
|
231
|
+
"tools": ["file_read", "code_check", "git"],
|
|
232
|
+
},
|
|
233
|
+
{
|
|
234
|
+
"id": "doc_writer",
|
|
235
|
+
"name": "文档工程师",
|
|
236
|
+
"persona": persona("doc-writer"),
|
|
237
|
+
"tools": ["file_read", "file_write", "bash"],
|
|
238
|
+
},
|
|
239
|
+
{
|
|
240
|
+
"id": "data_analyst",
|
|
241
|
+
"name": "数据分析师",
|
|
242
|
+
"persona": persona("data-analyst"),
|
|
243
|
+
"tools": ["file_read", "bash", "db"],
|
|
244
|
+
},
|
|
245
|
+
],
|
|
246
|
+
}
|
|
247
|
+
return TeamConfig.from_dict(data, config_dir=config_dir)
|