@orchagent/cli 0.3.66 → 0.3.68
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/commands/init.js
CHANGED
|
@@ -8,6 +8,7 @@ const promises_1 = __importDefault(require("fs/promises"));
|
|
|
8
8
|
const path_1 = __importDefault(require("path"));
|
|
9
9
|
const errors_1 = require("../lib/errors");
|
|
10
10
|
const github_weekly_summary_1 = require("./templates/github-weekly-summary");
|
|
11
|
+
const support_agent_1 = require("./templates/support-agent");
|
|
11
12
|
const MANIFEST_TEMPLATE = `{
|
|
12
13
|
"name": "my-agent",
|
|
13
14
|
"description": "A simple AI agent",
|
|
@@ -87,6 +88,51 @@ if __name__ == "__main__":
|
|
|
87
88
|
main()
|
|
88
89
|
`;
|
|
89
90
|
function readmeTemplate(agentName, flavor) {
|
|
91
|
+
if (flavor === 'support_agent') {
|
|
92
|
+
return `# ${agentName}
|
|
93
|
+
|
|
94
|
+
A multi-platform support agent powered by Claude. Connects to Discord, Telegram, and/or Slack.
|
|
95
|
+
|
|
96
|
+
## Setup
|
|
97
|
+
|
|
98
|
+
### 1. Customize
|
|
99
|
+
|
|
100
|
+
Edit \`config.py\` — set your product name, description, and bot name.
|
|
101
|
+
|
|
102
|
+
### 2. Add Knowledge
|
|
103
|
+
|
|
104
|
+
Replace the example files in \`knowledge/\` with your own docs.
|
|
105
|
+
Files named \`NN-topic-name.md\` auto-discover as topics.
|
|
106
|
+
|
|
107
|
+
### 3. Set Platform Tokens
|
|
108
|
+
|
|
109
|
+
Copy \`.env.example\` to \`.env\` and fill in tokens for the platforms you want.
|
|
110
|
+
At least one platform token is required.
|
|
111
|
+
|
|
112
|
+
### 4. Run Locally
|
|
113
|
+
|
|
114
|
+
\`\`\`sh
|
|
115
|
+
pip install -r requirements.txt
|
|
116
|
+
python main.py
|
|
117
|
+
\`\`\`
|
|
118
|
+
|
|
119
|
+
### 5. Deploy
|
|
120
|
+
|
|
121
|
+
\`\`\`sh
|
|
122
|
+
orch publish
|
|
123
|
+
# Add secrets in your workspace (web dashboard > Settings > Secrets)
|
|
124
|
+
orch service deploy
|
|
125
|
+
\`\`\`
|
|
126
|
+
|
|
127
|
+
## Platforms
|
|
128
|
+
|
|
129
|
+
| Platform | Token Required | Extra Config |
|
|
130
|
+
|----------|---------------|--------------|
|
|
131
|
+
| Discord | \`DISCORD_BOT_TOKEN\` | \`DISCORD_CHANNEL_IDS\` |
|
|
132
|
+
| Telegram | \`TELEGRAM_BOT_TOKEN\` | — |
|
|
133
|
+
| Slack | \`SLACK_BOT_TOKEN\` | \`SLACK_APP_TOKEN\` |
|
|
134
|
+
`;
|
|
135
|
+
}
|
|
90
136
|
if (flavor === 'discord') {
|
|
91
137
|
return `# ${agentName}
|
|
92
138
|
|
|
@@ -479,7 +525,7 @@ function registerInitCommand(program) {
|
|
|
479
525
|
.option('--type <type>', 'Type: prompt, tool, agent, or skill (legacy aliases: agentic, code)', 'prompt')
|
|
480
526
|
.option('--orchestrator', 'Create an orchestrator agent with dependency scaffolding and SDK boilerplate')
|
|
481
527
|
.option('--run-mode <mode>', 'Run mode for agents: on_demand or always_on', 'on_demand')
|
|
482
|
-
.option('--template <name>', 'Start from a template (available: github-weekly-summary
|
|
528
|
+
.option('--template <name>', 'Start from a template (available: support-agent, discord, github-weekly-summary)')
|
|
483
529
|
.action(async (name, options) => {
|
|
484
530
|
const cwd = process.cwd();
|
|
485
531
|
let runMode = (options.runMode || 'on_demand').trim().toLowerCase();
|
|
@@ -495,7 +541,7 @@ function registerInitCommand(program) {
|
|
|
495
541
|
}
|
|
496
542
|
if (options.template) {
|
|
497
543
|
const template = options.template.trim().toLowerCase();
|
|
498
|
-
const validTemplates = ['discord', 'github-weekly-summary'];
|
|
544
|
+
const validTemplates = ['support-agent', 'discord', 'github-weekly-summary'];
|
|
499
545
|
if (!validTemplates.includes(template)) {
|
|
500
546
|
throw new errors_1.CliError(`Unknown --template '${template}'. Available templates: ${validTemplates.join(', ')}`);
|
|
501
547
|
}
|
|
@@ -505,7 +551,11 @@ function registerInitCommand(program) {
|
|
|
505
551
|
if (initMode.type === 'skill') {
|
|
506
552
|
throw new errors_1.CliError('Cannot use --template with --type skill.');
|
|
507
553
|
}
|
|
508
|
-
if (template === '
|
|
554
|
+
if (template === 'support-agent') {
|
|
555
|
+
initMode = { type: 'agent', flavor: 'support_agent' };
|
|
556
|
+
runMode = 'always_on';
|
|
557
|
+
}
|
|
558
|
+
else if (template === 'discord') {
|
|
509
559
|
initMode = { type: 'agent', flavor: 'discord' };
|
|
510
560
|
runMode = 'always_on';
|
|
511
561
|
}
|
|
@@ -550,6 +600,80 @@ function registerInitCommand(program) {
|
|
|
550
600
|
}
|
|
551
601
|
return;
|
|
552
602
|
}
|
|
603
|
+
// Handle support-agent template separately (multi-file structure)
|
|
604
|
+
if (initMode.flavor === 'support_agent') {
|
|
605
|
+
const manifestPath = path_1.default.join(targetDir, 'orchagent.json');
|
|
606
|
+
try {
|
|
607
|
+
await promises_1.default.access(manifestPath);
|
|
608
|
+
throw new errors_1.CliError(`Already initialized (orchagent.json exists in ${name ? name + '/' : 'current directory'})`);
|
|
609
|
+
}
|
|
610
|
+
catch (err) {
|
|
611
|
+
if (err.code !== 'ENOENT') {
|
|
612
|
+
throw err;
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
// Create subdirectories
|
|
616
|
+
await promises_1.default.mkdir(path_1.default.join(targetDir, 'connectors'), { recursive: true });
|
|
617
|
+
await promises_1.default.mkdir(path_1.default.join(targetDir, 'knowledge'), { recursive: true });
|
|
618
|
+
// Write manifest
|
|
619
|
+
const manifest = {
|
|
620
|
+
name: agentName,
|
|
621
|
+
type: 'agent',
|
|
622
|
+
description: 'Multi-platform support agent powered by Claude. Connects to Discord, Telegram, and/or Slack.',
|
|
623
|
+
run_mode: 'always_on',
|
|
624
|
+
runtime: { command: 'python main.py' },
|
|
625
|
+
entrypoint: 'main.py',
|
|
626
|
+
supported_providers: ['anthropic'],
|
|
627
|
+
default_models: { anthropic: 'claude-sonnet-4-5-20250929' },
|
|
628
|
+
required_secrets: [],
|
|
629
|
+
optional_secrets: ['DISCORD_BOT_TOKEN', 'DISCORD_CHANNEL_IDS', 'TELEGRAM_BOT_TOKEN', 'SLACK_BOT_TOKEN', 'SLACK_APP_TOKEN'],
|
|
630
|
+
tags: ['support', 'discord', 'telegram', 'slack', 'always-on', 'multi-platform'],
|
|
631
|
+
bundle: {
|
|
632
|
+
include: ['*.py', 'connectors/*.py', 'knowledge/*.md', 'requirements.txt'],
|
|
633
|
+
exclude: ['tests/', '__pycache__', '*.pyc', '.pytest_cache'],
|
|
634
|
+
},
|
|
635
|
+
};
|
|
636
|
+
await promises_1.default.writeFile(manifestPath, JSON.stringify(manifest, null, 2) + '\n');
|
|
637
|
+
// Write all source files
|
|
638
|
+
await promises_1.default.writeFile(path_1.default.join(targetDir, 'config.py'), support_agent_1.SA_CONFIG_PY);
|
|
639
|
+
await promises_1.default.writeFile(path_1.default.join(targetDir, 'brain.py'), support_agent_1.SA_BRAIN_PY);
|
|
640
|
+
await promises_1.default.writeFile(path_1.default.join(targetDir, 'main.py'), support_agent_1.SA_MAIN_PY);
|
|
641
|
+
await promises_1.default.writeFile(path_1.default.join(targetDir, 'connectors', '__init__.py'), '');
|
|
642
|
+
await promises_1.default.writeFile(path_1.default.join(targetDir, 'connectors', 'discord_connector.py'), support_agent_1.SA_DISCORD_CONNECTOR_PY);
|
|
643
|
+
await promises_1.default.writeFile(path_1.default.join(targetDir, 'connectors', 'telegram_connector.py'), support_agent_1.SA_TELEGRAM_CONNECTOR_PY);
|
|
644
|
+
await promises_1.default.writeFile(path_1.default.join(targetDir, 'connectors', 'slack_connector.py'), support_agent_1.SA_SLACK_CONNECTOR_PY);
|
|
645
|
+
await promises_1.default.writeFile(path_1.default.join(targetDir, 'requirements.txt'), support_agent_1.SA_REQUIREMENTS);
|
|
646
|
+
await promises_1.default.writeFile(path_1.default.join(targetDir, 'knowledge', '00-overview.md'), support_agent_1.SA_OVERVIEW_MD);
|
|
647
|
+
await promises_1.default.writeFile(path_1.default.join(targetDir, 'knowledge', '99-faq.md'), support_agent_1.SA_FAQ_MD);
|
|
648
|
+
await promises_1.default.writeFile(path_1.default.join(targetDir, '.env.example'), support_agent_1.SA_ENV_EXAMPLE);
|
|
649
|
+
await promises_1.default.writeFile(path_1.default.join(targetDir, 'README.md'), readmeTemplate(agentName, 'support_agent'));
|
|
650
|
+
const prefix = name ? name + '/' : '';
|
|
651
|
+
process.stdout.write(`\nInitialized support-agent "${agentName}" in ${targetDir}\n`);
|
|
652
|
+
process.stdout.write(`\nFiles created:\n`);
|
|
653
|
+
process.stdout.write(` ${prefix}orchagent.json Agent manifest\n`);
|
|
654
|
+
process.stdout.write(` ${prefix}config.py Customize here (4 fields)\n`);
|
|
655
|
+
process.stdout.write(` ${prefix}brain.py Three-tier classifier + responder\n`);
|
|
656
|
+
process.stdout.write(` ${prefix}main.py Startup orchestrator\n`);
|
|
657
|
+
process.stdout.write(` ${prefix}connectors/discord_connector.py Discord connector\n`);
|
|
658
|
+
process.stdout.write(` ${prefix}connectors/telegram_connector.py Telegram connector\n`);
|
|
659
|
+
process.stdout.write(` ${prefix}connectors/slack_connector.py Slack connector\n`);
|
|
660
|
+
process.stdout.write(` ${prefix}knowledge/00-overview.md Example FAQ knowledge\n`);
|
|
661
|
+
process.stdout.write(` ${prefix}knowledge/99-faq.md Example FAQ knowledge\n`);
|
|
662
|
+
process.stdout.write(` ${prefix}requirements.txt Python dependencies\n`);
|
|
663
|
+
process.stdout.write(` ${prefix}.env.example Environment variables\n`);
|
|
664
|
+
process.stdout.write(` ${prefix}README.md Setup guide\n`);
|
|
665
|
+
process.stdout.write(`\nNext steps:\n`);
|
|
666
|
+
const s = name ? 2 : 1;
|
|
667
|
+
if (name) {
|
|
668
|
+
process.stdout.write(` 1. cd ${name}\n`);
|
|
669
|
+
}
|
|
670
|
+
process.stdout.write(` ${s}. Edit config.py with your product name and description\n`);
|
|
671
|
+
process.stdout.write(` ${s + 1}. Replace knowledge/ files with your own docs\n`);
|
|
672
|
+
process.stdout.write(` ${s + 2}. Copy .env.example to .env and add platform tokens\n`);
|
|
673
|
+
process.stdout.write(` ${s + 3}. Test locally: pip install -r requirements.txt && python main.py\n`);
|
|
674
|
+
process.stdout.write(` ${s + 4}. Deploy: orch publish && orch service deploy\n`);
|
|
675
|
+
return;
|
|
676
|
+
}
|
|
553
677
|
// Handle github-weekly-summary template separately (own file set + output)
|
|
554
678
|
if (initMode.flavor === 'github_weekly_summary') {
|
|
555
679
|
const manifestPath = path_1.default.join(targetDir, 'orchagent.json');
|
package/dist/commands/publish.js
CHANGED
|
@@ -339,9 +339,11 @@ function registerPublishCommand(program) {
|
|
|
339
339
|
const cwd = process.cwd();
|
|
340
340
|
// Resolve workspace context — if `orch workspace use` was called, publish
|
|
341
341
|
// to that workspace instead of the personal org (F-5)
|
|
342
|
+
// Skip workspace resolution when using a named profile — the global
|
|
343
|
+
// workspace belongs to the default profile's context, not the named one.
|
|
342
344
|
const configFile = await (0, config_1.loadConfig)();
|
|
343
345
|
let workspaceId;
|
|
344
|
-
if (configFile.workspace) {
|
|
346
|
+
if (configFile.workspace && !options.profile) {
|
|
345
347
|
const { workspaces } = await (0, api_1.request)(config, 'GET', '/workspaces');
|
|
346
348
|
const ws = workspaces.find(w => w.slug === configFile.workspace);
|
|
347
349
|
if (!ws) {
|
|
@@ -899,7 +901,7 @@ function registerPublishCommand(program) {
|
|
|
899
901
|
}
|
|
900
902
|
// Upload the bundle with entrypoint
|
|
901
903
|
process.stdout.write(` Uploading bundle...\n`);
|
|
902
|
-
const uploadResult = await (0, api_1.uploadCodeBundle)(config, agentId, bundlePath, bundleEntrypoint);
|
|
904
|
+
const uploadResult = await (0, api_1.uploadCodeBundle)(config, agentId, bundlePath, bundleEntrypoint, workspaceId);
|
|
903
905
|
process.stdout.write(` Uploaded: ${uploadResult.code_hash.substring(0, 12)}...\n`);
|
|
904
906
|
// Show environment info if applicable
|
|
905
907
|
if (uploadResult.environment_id) {
|
package/dist/commands/service.js
CHANGED
|
@@ -93,9 +93,10 @@ function registerServiceCommand(program) {
|
|
|
93
93
|
.option('--command <cmd>', 'Override entrypoint command')
|
|
94
94
|
.option('--arg <value>', 'Command argument (repeatable)', collectArray, [])
|
|
95
95
|
.option('--pin', 'Pin to deployed version (disable auto-update on publish)')
|
|
96
|
+
.option('--profile <name>', 'Use API key from named profile')
|
|
96
97
|
.option('--json', 'Output as JSON')
|
|
97
98
|
.action(async (agentArg, options) => {
|
|
98
|
-
const config = await (0, config_1.getResolvedConfig)();
|
|
99
|
+
const config = await (0, config_1.getResolvedConfig)({}, options.profile);
|
|
99
100
|
if (!config.apiKey) {
|
|
100
101
|
throw new errors_1.CliError('Missing API key. Run `orch login` first.');
|
|
101
102
|
}
|
|
@@ -0,0 +1,570 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Multi-Platform Support Agent template.
|
|
4
|
+
*
|
|
5
|
+
* Generates a three-tier support agent that connects to Discord, Telegram,
|
|
6
|
+
* and/or Slack based on which tokens are configured.
|
|
7
|
+
*/
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.SA_ENV_EXAMPLE = exports.SA_FAQ_MD = exports.SA_OVERVIEW_MD = exports.SA_REQUIREMENTS = exports.SA_MAIN_PY = exports.SA_SLACK_CONNECTOR_PY = exports.SA_TELEGRAM_CONNECTOR_PY = exports.SA_DISCORD_CONNECTOR_PY = exports.SA_BRAIN_PY = exports.SA_CONFIG_PY = void 0;
|
|
10
|
+
exports.SA_CONFIG_PY = `"""
|
|
11
|
+
Support Agent Configuration — Edit these 4 fields to customize your agent.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
# The name your bot uses when responding
|
|
15
|
+
BOT_NAME = "Support Agent"
|
|
16
|
+
|
|
17
|
+
# Your product/service name — used in prompts and classification
|
|
18
|
+
PRODUCT_NAME = "My Product"
|
|
19
|
+
|
|
20
|
+
# One-sentence description — helps the classifier understand your domain
|
|
21
|
+
PRODUCT_DESCRIPTION = "a platform for doing amazing things"
|
|
22
|
+
|
|
23
|
+
# Knowledge files used for the FAQ tier (general questions).
|
|
24
|
+
FAQ_FILES = ["00-overview.md", "99-faq.md"]
|
|
25
|
+
`;
|
|
26
|
+
exports.SA_BRAIN_PY = `"""
|
|
27
|
+
Support Agent Brain — Three-Tier Architecture
|
|
28
|
+
|
|
29
|
+
Tier 1: CLASSIFY — Haiku classifier decides if/how to respond
|
|
30
|
+
Tier 2: FAQ — General knowledge handles ~80% of questions (cached)
|
|
31
|
+
Tier 3: DEEP DOCS — Topic-specific docs for detailed questions (cached per topic)
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
import json
|
|
35
|
+
import logging
|
|
36
|
+
import os
|
|
37
|
+
import sys
|
|
38
|
+
from pathlib import Path
|
|
39
|
+
|
|
40
|
+
import anthropic
|
|
41
|
+
|
|
42
|
+
from config import BOT_NAME, FAQ_FILES, PRODUCT_DESCRIPTION, PRODUCT_NAME
|
|
43
|
+
|
|
44
|
+
logger = logging.getLogger("support-agent.brain")
|
|
45
|
+
|
|
46
|
+
CLASSIFY_MODEL = os.environ.get("CLASSIFY_MODEL", "claude-haiku-4-5-20251001")
|
|
47
|
+
RESPOND_MODEL = os.environ.get("RESPOND_MODEL", "claude-sonnet-4-5-20250929")
|
|
48
|
+
CLASSIFY_MAX_TOKENS = 150
|
|
49
|
+
RESPOND_MAX_TOKENS = int(os.environ.get("MAX_TOKENS", "1024"))
|
|
50
|
+
|
|
51
|
+
DOCS_DIR = Path(__file__).parent / "knowledge"
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def load_doc_files() -> dict[str, str]:
|
|
55
|
+
docs: dict[str, str] = {}
|
|
56
|
+
if not DOCS_DIR.is_dir():
|
|
57
|
+
logger.warning("knowledge/ directory not found")
|
|
58
|
+
return docs
|
|
59
|
+
for md_file in sorted(DOCS_DIR.glob("*.md")):
|
|
60
|
+
content = md_file.read_text(encoding="utf-8").strip()
|
|
61
|
+
if content:
|
|
62
|
+
docs[md_file.name] = content
|
|
63
|
+
logger.info("Loaded %d knowledge files", len(docs))
|
|
64
|
+
return docs
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def discover_topics(doc_files: dict[str, str]) -> dict[str, list[str]]:
|
|
68
|
+
topics: dict[str, list[str]] = {}
|
|
69
|
+
faq_set = set(FAQ_FILES)
|
|
70
|
+
for filename in sorted(doc_files.keys()):
|
|
71
|
+
if filename in faq_set:
|
|
72
|
+
continue
|
|
73
|
+
stem = filename.rsplit(".", 1)[0]
|
|
74
|
+
parts = stem.split("-", 1)
|
|
75
|
+
if len(parts) == 2 and parts[0].isdigit():
|
|
76
|
+
topic = parts[1]
|
|
77
|
+
else:
|
|
78
|
+
topic = stem
|
|
79
|
+
topic = topic.strip().lower().replace(" ", "-").replace("_", "-")
|
|
80
|
+
if not topic:
|
|
81
|
+
continue
|
|
82
|
+
topics.setdefault(topic, []).append(filename)
|
|
83
|
+
return topics
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def build_docs_context(doc_files: dict[str, str], filenames: list[str]) -> str:
|
|
87
|
+
parts: list[str] = []
|
|
88
|
+
for name in filenames:
|
|
89
|
+
if name in doc_files:
|
|
90
|
+
title = name.rsplit(".", 1)[0].replace("-", " ").title()
|
|
91
|
+
parts.append(f"## {title}\\n\\n{doc_files[name]}")
|
|
92
|
+
return "\\n\\n---\\n\\n".join(parts)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def get_deep_files(topics: list[str], topic_files: dict[str, list[str]]) -> list[str]:
|
|
96
|
+
files: list[str] = []
|
|
97
|
+
seen: set[str] = set()
|
|
98
|
+
for topic in topics:
|
|
99
|
+
for f in topic_files.get(topic, []):
|
|
100
|
+
if f not in seen:
|
|
101
|
+
files.append(f)
|
|
102
|
+
seen.add(f)
|
|
103
|
+
for f in FAQ_FILES:
|
|
104
|
+
if f not in seen:
|
|
105
|
+
files.append(f)
|
|
106
|
+
seen.add(f)
|
|
107
|
+
return files
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def build_response_rules() -> str:
|
|
111
|
+
return f"""\\
|
|
112
|
+
You are {BOT_NAME}, the official support assistant for {PRODUCT_NAME}.
|
|
113
|
+
|
|
114
|
+
Your job is to answer user questions about {PRODUCT_NAME} accurately and helpfully, grounded in the documentation provided below. Follow these rules:
|
|
115
|
+
|
|
116
|
+
1. Base your answers ONLY on the documentation below. Do not guess or make up features.
|
|
117
|
+
2. Be concise and direct. Messages should be readable, not walls of text.
|
|
118
|
+
3. Use code blocks for commands and code examples.
|
|
119
|
+
4. If the documentation doesn't cover the question, say: "I'm not sure about this — a human will follow up." Do NOT hallucinate an answer.
|
|
120
|
+
5. If a question is ambiguous, ask for clarification before answering.
|
|
121
|
+
6. Be friendly and professional. You represent the {PRODUCT_NAME} team.
|
|
122
|
+
7. Do not discuss pricing specifics beyond what's in the docs.
|
|
123
|
+
8. Do not share internal implementation details, architecture decisions, or roadmap items.
|
|
124
|
+
9. Keep responses under 1800 characters to stay readable.
|
|
125
|
+
|
|
126
|
+
---
|
|
127
|
+
|
|
128
|
+
# {PRODUCT_NAME} Documentation
|
|
129
|
+
|
|
130
|
+
"""
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def build_classify_system_prompt(topic_list: list[str]) -> str:
|
|
134
|
+
topic_csv = ", ".join(topic_list) if topic_list else "general"
|
|
135
|
+
return f"""\\
|
|
136
|
+
You are a message classifier for the {PRODUCT_NAME} support channel.
|
|
137
|
+
{PRODUCT_NAME} is {PRODUCT_DESCRIPTION}.
|
|
138
|
+
|
|
139
|
+
Classify the user's message. Return ONLY valid JSON, no other text.
|
|
140
|
+
|
|
141
|
+
Topics: {topic_csv}
|
|
142
|
+
|
|
143
|
+
{{"respond": bool, "tier": "faq" or "deep", "topics": ["topic1"]}}
|
|
144
|
+
|
|
145
|
+
Rules:
|
|
146
|
+
- "respond": false for greetings, thanks, off-topic chat, non-questions, emoji, memes
|
|
147
|
+
- "tier": "faq" for simple/general questions about the platform
|
|
148
|
+
- "tier": "deep" for questions needing specific technical details
|
|
149
|
+
- "topics": 1-2 relevant topics when tier is "deep", empty list for "faq" """
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def build_faq_prompt(doc_files: dict[str, str]) -> str:
|
|
153
|
+
return build_response_rules() + build_docs_context(doc_files, FAQ_FILES)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def build_deep_prompt(doc_files: dict[str, str], topics: list[str], topic_files: dict[str, list[str]]) -> str:
|
|
157
|
+
files = get_deep_files(topics, topic_files)
|
|
158
|
+
return build_response_rules() + build_docs_context(doc_files, files)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def create_anthropic_client() -> anthropic.Anthropic:
|
|
162
|
+
api_key = os.environ.get("ANTHROPIC_API_KEY", "")
|
|
163
|
+
if not api_key:
|
|
164
|
+
logger.error("ANTHROPIC_API_KEY not set")
|
|
165
|
+
sys.exit(1)
|
|
166
|
+
return anthropic.Anthropic(api_key=api_key)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def classify_message(client: anthropic.Anthropic, user_message: str, system_prompt: str) -> dict:
|
|
170
|
+
try:
|
|
171
|
+
response = client.messages.create(
|
|
172
|
+
model=CLASSIFY_MODEL, max_tokens=CLASSIFY_MAX_TOKENS,
|
|
173
|
+
system=system_prompt, messages=[{"role": "user", "content": user_message}],
|
|
174
|
+
)
|
|
175
|
+
raw = response.content[0].text.strip()
|
|
176
|
+
if raw.startswith("\\\`\\\`\\\`"):
|
|
177
|
+
raw = raw.split("\\n", 1)[1].rsplit("\\\`\\\`\\\`", 1)[0].strip()
|
|
178
|
+
result = json.loads(raw)
|
|
179
|
+
logger.info("Classify: respond=%s tier=%s topics=%s (in=%d out=%d)",
|
|
180
|
+
result.get("respond"), result.get("tier"), result.get("topics"),
|
|
181
|
+
response.usage.input_tokens, response.usage.output_tokens)
|
|
182
|
+
return result
|
|
183
|
+
except (json.JSONDecodeError, KeyError, IndexError) as exc:
|
|
184
|
+
logger.warning("Classifier returned invalid JSON: %s", exc)
|
|
185
|
+
return {"respond": True, "tier": "faq", "topics": []}
|
|
186
|
+
except anthropic.APIError as exc:
|
|
187
|
+
logger.warning("Classifier API error: %s", exc)
|
|
188
|
+
return {"respond": True, "tier": "faq", "topics": []}
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def query_llm(client: anthropic.Anthropic, system_prompt: str, user_message: str) -> tuple[str, int, int]:
|
|
192
|
+
response = client.messages.create(
|
|
193
|
+
model=RESPOND_MODEL, max_tokens=RESPOND_MAX_TOKENS,
|
|
194
|
+
system=[{"type": "text", "text": system_prompt, "cache_control": {"type": "ephemeral"}}],
|
|
195
|
+
messages=[{"role": "user", "content": user_message}],
|
|
196
|
+
)
|
|
197
|
+
return response.content[0].text, response.usage.input_tokens, response.usage.output_tokens
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
class Brain:
|
|
201
|
+
def __init__(self, anthropic_client, doc_files, topic_files, faq_prompt, classify_system_prompt):
|
|
202
|
+
self.client = anthropic_client
|
|
203
|
+
self.doc_files = doc_files
|
|
204
|
+
self.topic_files = topic_files
|
|
205
|
+
self.faq_prompt = faq_prompt
|
|
206
|
+
self.classify_system_prompt = classify_system_prompt
|
|
207
|
+
|
|
208
|
+
def classify(self, message: str) -> dict:
|
|
209
|
+
return classify_message(self.client, message, self.classify_system_prompt)
|
|
210
|
+
|
|
211
|
+
def respond_faq(self, message: str) -> tuple[str, int, int]:
|
|
212
|
+
return query_llm(self.client, self.faq_prompt, message)
|
|
213
|
+
|
|
214
|
+
def respond_deep(self, message: str, topics: list[str]) -> tuple[str, int, int]:
|
|
215
|
+
prompt = build_deep_prompt(self.doc_files, topics, self.topic_files)
|
|
216
|
+
return query_llm(self.client, prompt, message)
|
|
217
|
+
`;
|
|
218
|
+
exports.SA_DISCORD_CONNECTOR_PY = `"""Discord connector for the support agent."""
|
|
219
|
+
|
|
220
|
+
import asyncio
|
|
221
|
+
import logging
|
|
222
|
+
import os
|
|
223
|
+
import time
|
|
224
|
+
|
|
225
|
+
import anthropic
|
|
226
|
+
import discord
|
|
227
|
+
|
|
228
|
+
from brain import Brain
|
|
229
|
+
|
|
230
|
+
logger = logging.getLogger("support-agent.discord")
|
|
231
|
+
USER_COOLDOWN_SECONDS = int(os.environ.get("USER_COOLDOWN_SECONDS", "5"))
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def parse_channel_ids(raw: str) -> set[int]:
|
|
235
|
+
ids: set[int] = set()
|
|
236
|
+
for part in raw.split(","):
|
|
237
|
+
part = part.strip()
|
|
238
|
+
if part.isdigit():
|
|
239
|
+
ids.add(int(part))
|
|
240
|
+
return ids
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def should_respond(message: discord.Message, allowed_channels: set[int]) -> bool:
|
|
244
|
+
if message.author.bot:
|
|
245
|
+
return False
|
|
246
|
+
channel_id = message.channel.id
|
|
247
|
+
parent_id = getattr(message.channel, "parent_id", None)
|
|
248
|
+
if channel_id not in allowed_channels and parent_id not in allowed_channels:
|
|
249
|
+
return False
|
|
250
|
+
if not message.content or not message.content.strip():
|
|
251
|
+
return False
|
|
252
|
+
if len(message.content.strip()) < 5:
|
|
253
|
+
return False
|
|
254
|
+
return True
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
class DiscordConnector(discord.Client):
|
|
258
|
+
def __init__(self, brain: Brain, allowed_channels: set[int]):
|
|
259
|
+
intents = discord.Intents.default()
|
|
260
|
+
intents.message_content = True
|
|
261
|
+
super().__init__(intents=intents)
|
|
262
|
+
self.brain = brain
|
|
263
|
+
self.allowed_channels = allowed_channels
|
|
264
|
+
self._user_last_response: dict[int, float] = {}
|
|
265
|
+
|
|
266
|
+
async def on_ready(self) -> None:
|
|
267
|
+
logger.info("Discord connected as %s (id=%s)", self.user, self.user.id)
|
|
268
|
+
|
|
269
|
+
async def on_message(self, message: discord.Message) -> None:
|
|
270
|
+
if not should_respond(message, self.allowed_channels):
|
|
271
|
+
return
|
|
272
|
+
now = time.time()
|
|
273
|
+
if now - self._user_last_response.get(message.author.id, 0) < USER_COOLDOWN_SECONDS:
|
|
274
|
+
return
|
|
275
|
+
user_text = message.content.strip()
|
|
276
|
+
classification = await asyncio.to_thread(self.brain.classify, user_text)
|
|
277
|
+
if not classification.get("respond", True):
|
|
278
|
+
return
|
|
279
|
+
tier = classification.get("tier", "faq")
|
|
280
|
+
topics = classification.get("topics", [])
|
|
281
|
+
try:
|
|
282
|
+
thread = await message.create_thread(name="Q: " + user_text[:80], auto_archive_duration=60)
|
|
283
|
+
except discord.HTTPException:
|
|
284
|
+
thread = message.channel
|
|
285
|
+
async with thread.typing():
|
|
286
|
+
try:
|
|
287
|
+
if tier == "deep" and topics:
|
|
288
|
+
answer, _, _ = await asyncio.to_thread(self.brain.respond_deep, user_text, topics)
|
|
289
|
+
else:
|
|
290
|
+
answer, _, _ = await asyncio.to_thread(self.brain.respond_faq, user_text)
|
|
291
|
+
except anthropic.RateLimitError:
|
|
292
|
+
await thread.send("I'm getting a lot of questions right now. Please try again in a minute.")
|
|
293
|
+
return
|
|
294
|
+
except anthropic.APITimeoutError:
|
|
295
|
+
await thread.send("My response timed out. Please try again.")
|
|
296
|
+
return
|
|
297
|
+
except anthropic.APIError:
|
|
298
|
+
await thread.send("I ran into an issue generating a response. A human will follow up.")
|
|
299
|
+
return
|
|
300
|
+
if len(answer) > 1900:
|
|
301
|
+
answer = answer[:1897] + "..."
|
|
302
|
+
await thread.send(answer)
|
|
303
|
+
self._user_last_response[message.author.id] = time.time()
|
|
304
|
+
|
|
305
|
+
async def run_async(self, token: str) -> None:
|
|
306
|
+
await self.start(token)
|
|
307
|
+
`;
|
|
308
|
+
exports.SA_TELEGRAM_CONNECTOR_PY = `"""Telegram connector for the support agent."""
|
|
309
|
+
|
|
310
|
+
import asyncio
|
|
311
|
+
import logging
|
|
312
|
+
import os
|
|
313
|
+
import time
|
|
314
|
+
|
|
315
|
+
from telegram import Update
|
|
316
|
+
from telegram.ext import Application, ContextTypes, MessageHandler, filters
|
|
317
|
+
|
|
318
|
+
from brain import Brain
|
|
319
|
+
|
|
320
|
+
logger = logging.getLogger("support-agent.telegram")
|
|
321
|
+
USER_COOLDOWN_SECONDS = int(os.environ.get("USER_COOLDOWN_SECONDS", "5"))
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
class TelegramConnector:
|
|
325
|
+
def __init__(self, brain: Brain, token: str):
|
|
326
|
+
self.brain = brain
|
|
327
|
+
self.token = token
|
|
328
|
+
self._user_last_response: dict[int, float] = {}
|
|
329
|
+
|
|
330
|
+
async def _handle_message(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
|
331
|
+
message = update.message
|
|
332
|
+
if not message or not message.text:
|
|
333
|
+
return
|
|
334
|
+
user_text = message.text.strip()
|
|
335
|
+
if len(user_text) < 5:
|
|
336
|
+
return
|
|
337
|
+
user_id = message.from_user.id if message.from_user else 0
|
|
338
|
+
now = time.time()
|
|
339
|
+
if now - self._user_last_response.get(user_id, 0) < USER_COOLDOWN_SECONDS:
|
|
340
|
+
return
|
|
341
|
+
classification = await asyncio.to_thread(self.brain.classify, user_text)
|
|
342
|
+
if not classification.get("respond", True):
|
|
343
|
+
return
|
|
344
|
+
tier = classification.get("tier", "faq")
|
|
345
|
+
topics = classification.get("topics", [])
|
|
346
|
+
await message.chat.send_action("typing")
|
|
347
|
+
try:
|
|
348
|
+
if tier == "deep" and topics:
|
|
349
|
+
answer, _, _ = await asyncio.to_thread(self.brain.respond_deep, user_text, topics)
|
|
350
|
+
else:
|
|
351
|
+
answer, _, _ = await asyncio.to_thread(self.brain.respond_faq, user_text)
|
|
352
|
+
except Exception:
|
|
353
|
+
await message.reply_text("I ran into an issue generating a response. A human will follow up.")
|
|
354
|
+
return
|
|
355
|
+
if len(answer) > 4000:
|
|
356
|
+
answer = answer[:3997] + "..."
|
|
357
|
+
await message.reply_text(answer)
|
|
358
|
+
self._user_last_response[user_id] = time.time()
|
|
359
|
+
|
|
360
|
+
async def run(self) -> None:
|
|
361
|
+
app = Application.builder().token(self.token).build()
|
|
362
|
+
app.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, self._handle_message))
|
|
363
|
+
await app.initialize()
|
|
364
|
+
await app.start()
|
|
365
|
+
await app.updater.start_polling(drop_pending_updates=True)
|
|
366
|
+
try:
|
|
367
|
+
await asyncio.Event().wait()
|
|
368
|
+
finally:
|
|
369
|
+
await app.updater.stop()
|
|
370
|
+
await app.stop()
|
|
371
|
+
await app.shutdown()
|
|
372
|
+
`;
|
|
373
|
+
exports.SA_SLACK_CONNECTOR_PY = `"""Slack connector for the support agent (Socket Mode)."""
|
|
374
|
+
|
|
375
|
+
import asyncio
|
|
376
|
+
import logging
|
|
377
|
+
import os
|
|
378
|
+
import time
|
|
379
|
+
|
|
380
|
+
from slack_bolt.adapter.socket_mode.async_handler import AsyncSocketModeHandler
|
|
381
|
+
from slack_bolt.async_app import AsyncApp
|
|
382
|
+
|
|
383
|
+
from brain import Brain
|
|
384
|
+
|
|
385
|
+
logger = logging.getLogger("support-agent.slack")
|
|
386
|
+
USER_COOLDOWN_SECONDS = int(os.environ.get("USER_COOLDOWN_SECONDS", "5"))
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
class SlackConnector:
|
|
390
|
+
def __init__(self, brain: Brain, bot_token: str, app_token: str):
|
|
391
|
+
self.brain = brain
|
|
392
|
+
self.app = AsyncApp(token=bot_token)
|
|
393
|
+
self.app_token = app_token
|
|
394
|
+
self._user_last_response: dict[str, float] = {}
|
|
395
|
+
self._register_handlers()
|
|
396
|
+
|
|
397
|
+
def _register_handlers(self) -> None:
|
|
398
|
+
@self.app.event("message")
|
|
399
|
+
async def handle_message(event, say):
|
|
400
|
+
if event.get("bot_id") or event.get("subtype"):
|
|
401
|
+
return
|
|
402
|
+
text = event.get("text", "").strip()
|
|
403
|
+
if not text or len(text) < 5:
|
|
404
|
+
return
|
|
405
|
+
user_id = event.get("user", "unknown")
|
|
406
|
+
now = time.time()
|
|
407
|
+
if now - self._user_last_response.get(user_id, 0) < USER_COOLDOWN_SECONDS:
|
|
408
|
+
return
|
|
409
|
+
classification = await asyncio.to_thread(self.brain.classify, text)
|
|
410
|
+
if not classification.get("respond", True):
|
|
411
|
+
return
|
|
412
|
+
tier = classification.get("tier", "faq")
|
|
413
|
+
topics = classification.get("topics", [])
|
|
414
|
+
try:
|
|
415
|
+
if tier == "deep" and topics:
|
|
416
|
+
answer, _, _ = await asyncio.to_thread(self.brain.respond_deep, text, topics)
|
|
417
|
+
else:
|
|
418
|
+
answer, _, _ = await asyncio.to_thread(self.brain.respond_faq, text)
|
|
419
|
+
except Exception:
|
|
420
|
+
await say("I ran into an issue generating a response. A human will follow up.")
|
|
421
|
+
return
|
|
422
|
+
if len(answer) > 3000:
|
|
423
|
+
answer = answer[:2997] + "..."
|
|
424
|
+
await say(answer, thread_ts=event.get("ts"))
|
|
425
|
+
self._user_last_response[user_id] = time.time()
|
|
426
|
+
|
|
427
|
+
async def run(self) -> None:
|
|
428
|
+
handler = AsyncSocketModeHandler(self.app, self.app_token)
|
|
429
|
+
await handler.start_async()
|
|
430
|
+
`;
|
|
431
|
+
exports.SA_MAIN_PY = `"""
|
|
432
|
+
Multi-Platform Support Agent — Startup Orchestrator
|
|
433
|
+
|
|
434
|
+
Checks which platform tokens are configured and starts those connectors.
|
|
435
|
+
"""
|
|
436
|
+
|
|
437
|
+
import asyncio
|
|
438
|
+
import logging
|
|
439
|
+
import os
|
|
440
|
+
import sys
|
|
441
|
+
|
|
442
|
+
from brain import (Brain, build_classify_system_prompt, build_faq_prompt,
|
|
443
|
+
create_anthropic_client, discover_topics, load_doc_files)
|
|
444
|
+
from config import FAQ_FILES
|
|
445
|
+
|
|
446
|
+
LOG_LEVEL = os.environ.get("LOG_LEVEL", "INFO").upper()
|
|
447
|
+
logging.basicConfig(level=getattr(logging, LOG_LEVEL, logging.INFO),
|
|
448
|
+
format="%(asctime)s %(levelname)s %(name)s: %(message)s", stream=sys.stdout)
|
|
449
|
+
logger = logging.getLogger("support-agent")
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
def validate_knowledge(doc_files: dict[str, str]) -> None:
|
|
453
|
+
if not doc_files:
|
|
454
|
+
logger.error("No knowledge files found in knowledge/ directory")
|
|
455
|
+
sys.exit(1)
|
|
456
|
+
for faq_file in FAQ_FILES:
|
|
457
|
+
if faq_file not in doc_files:
|
|
458
|
+
logger.error("FAQ file '%s' not found in knowledge/", faq_file)
|
|
459
|
+
sys.exit(1)
|
|
460
|
+
|
|
461
|
+
|
|
462
|
+
async def run_connectors(brain: Brain) -> None:
|
|
463
|
+
tasks = []
|
|
464
|
+
active = []
|
|
465
|
+
|
|
466
|
+
discord_token = os.environ.get("DISCORD_BOT_TOKEN", "")
|
|
467
|
+
if discord_token:
|
|
468
|
+
from connectors.discord_connector import DiscordConnector, parse_channel_ids
|
|
469
|
+
channels_raw = os.environ.get("DISCORD_CHANNEL_IDS", "")
|
|
470
|
+
if not channels_raw:
|
|
471
|
+
logger.error("DISCORD_BOT_TOKEN set but DISCORD_CHANNEL_IDS missing")
|
|
472
|
+
sys.exit(1)
|
|
473
|
+
channels = parse_channel_ids(channels_raw)
|
|
474
|
+
if not channels:
|
|
475
|
+
logger.error("No valid channel IDs in DISCORD_CHANNEL_IDS")
|
|
476
|
+
sys.exit(1)
|
|
477
|
+
tasks.append(DiscordConnector(brain, channels).run_async(discord_token))
|
|
478
|
+
active.append("Discord")
|
|
479
|
+
|
|
480
|
+
telegram_token = os.environ.get("TELEGRAM_BOT_TOKEN", "")
|
|
481
|
+
if telegram_token:
|
|
482
|
+
from connectors.telegram_connector import TelegramConnector
|
|
483
|
+
tasks.append(TelegramConnector(brain, telegram_token).run())
|
|
484
|
+
active.append("Telegram")
|
|
485
|
+
|
|
486
|
+
slack_bot_token = os.environ.get("SLACK_BOT_TOKEN", "")
|
|
487
|
+
if slack_bot_token:
|
|
488
|
+
from connectors.slack_connector import SlackConnector
|
|
489
|
+
slack_app_token = os.environ.get("SLACK_APP_TOKEN", "")
|
|
490
|
+
if not slack_app_token:
|
|
491
|
+
logger.error("SLACK_BOT_TOKEN set but SLACK_APP_TOKEN missing")
|
|
492
|
+
sys.exit(1)
|
|
493
|
+
tasks.append(SlackConnector(brain, slack_bot_token, slack_app_token).run())
|
|
494
|
+
active.append("Slack")
|
|
495
|
+
|
|
496
|
+
if not tasks:
|
|
497
|
+
logger.error("No platform tokens configured. Set at least one of: DISCORD_BOT_TOKEN, TELEGRAM_BOT_TOKEN, SLACK_BOT_TOKEN")
|
|
498
|
+
sys.exit(1)
|
|
499
|
+
|
|
500
|
+
logger.info("Active platforms: %s", ", ".join(active))
|
|
501
|
+
await asyncio.gather(*tasks)
|
|
502
|
+
|
|
503
|
+
|
|
504
|
+
def main() -> None:
|
|
505
|
+
if not os.environ.get("ANTHROPIC_API_KEY"):
|
|
506
|
+
logger.error("ANTHROPIC_API_KEY not set")
|
|
507
|
+
sys.exit(1)
|
|
508
|
+
|
|
509
|
+
doc_files = load_doc_files()
|
|
510
|
+
validate_knowledge(doc_files)
|
|
511
|
+
|
|
512
|
+
topic_files = discover_topics(doc_files)
|
|
513
|
+
topic_list = sorted(topic_files.keys())
|
|
514
|
+
faq_prompt = build_faq_prompt(doc_files)
|
|
515
|
+
classify_prompt = build_classify_system_prompt(topic_list)
|
|
516
|
+
|
|
517
|
+
logger.info("Topics discovered: %s", topic_list)
|
|
518
|
+
|
|
519
|
+
brain = Brain(
|
|
520
|
+
anthropic_client=create_anthropic_client(),
|
|
521
|
+
doc_files=doc_files, topic_files=topic_files,
|
|
522
|
+
faq_prompt=faq_prompt, classify_system_prompt=classify_prompt,
|
|
523
|
+
)
|
|
524
|
+
|
|
525
|
+
logger.info("Starting Multi-Platform Support Agent")
|
|
526
|
+
asyncio.run(run_connectors(brain))
|
|
527
|
+
|
|
528
|
+
|
|
529
|
+
if __name__ == "__main__":
|
|
530
|
+
main()
|
|
531
|
+
`;
|
|
532
|
+
exports.SA_REQUIREMENTS = `discord.py>=2.3.0,<3.0.0
|
|
533
|
+
python-telegram-bot>=21.0,<22.0.0
|
|
534
|
+
slack-bolt>=1.18.0,<2.0.0
|
|
535
|
+
anthropic>=0.40.0,<1.0.0
|
|
536
|
+
`;
|
|
537
|
+
exports.SA_OVERVIEW_MD = `# My Product Overview
|
|
538
|
+
|
|
539
|
+
Welcome to My Product! This is a placeholder overview document.
|
|
540
|
+
|
|
541
|
+
Replace this file with your own product overview and FAQ-tier knowledge.
|
|
542
|
+
|
|
543
|
+
## Getting Started
|
|
544
|
+
|
|
545
|
+
1. Sign up at myproduct.com
|
|
546
|
+
2. Follow the quick start guide
|
|
547
|
+
3. Start building
|
|
548
|
+
`;
|
|
549
|
+
exports.SA_FAQ_MD = `# FAQ
|
|
550
|
+
|
|
551
|
+
**Q: What is My Product?**
|
|
552
|
+
A: My Product is a platform for doing amazing things. Replace this with your actual FAQ.
|
|
553
|
+
|
|
554
|
+
**Q: How do I get help?**
|
|
555
|
+
A: Ask in the support channel — our support agent will respond!
|
|
556
|
+
`;
|
|
557
|
+
exports.SA_ENV_EXAMPLE = `# Required — auto-injected in production via supported_providers
|
|
558
|
+
ANTHROPIC_API_KEY=
|
|
559
|
+
|
|
560
|
+
# --- Discord (optional) ---
|
|
561
|
+
DISCORD_BOT_TOKEN=
|
|
562
|
+
DISCORD_CHANNEL_IDS=
|
|
563
|
+
|
|
564
|
+
# --- Telegram (optional) ---
|
|
565
|
+
TELEGRAM_BOT_TOKEN=
|
|
566
|
+
|
|
567
|
+
# --- Slack (optional) ---
|
|
568
|
+
SLACK_BOT_TOKEN=
|
|
569
|
+
SLACK_APP_TOKEN=
|
|
570
|
+
`;
|
package/dist/lib/api.js
CHANGED
|
@@ -320,7 +320,7 @@ async function downloadCodeBundle(config, org, agent, version) {
|
|
|
320
320
|
/**
|
|
321
321
|
* Upload a code bundle for a hosted tool agent.
|
|
322
322
|
*/
|
|
323
|
-
async function uploadCodeBundle(config, agentId, bundlePath, entrypoint) {
|
|
323
|
+
async function uploadCodeBundle(config, agentId, bundlePath, entrypoint, workspaceId) {
|
|
324
324
|
if (!config.apiKey) {
|
|
325
325
|
throw new ApiError('Missing API key. Run `orchagent login` first.', 401);
|
|
326
326
|
}
|
|
@@ -338,6 +338,9 @@ async function uploadCodeBundle(config, agentId, bundlePath, entrypoint) {
|
|
|
338
338
|
if (entrypoint) {
|
|
339
339
|
headers['x-entrypoint'] = entrypoint;
|
|
340
340
|
}
|
|
341
|
+
if (workspaceId) {
|
|
342
|
+
headers['X-Workspace-Id'] = workspaceId;
|
|
343
|
+
}
|
|
341
344
|
const response = await safeFetch(`${config.apiUrl.replace(/\/$/, '')}/agents/${agentId}/upload`, {
|
|
342
345
|
method: 'POST',
|
|
343
346
|
headers,
|
package/package.json
CHANGED