Expose agent templates in web interface

- Replace hardcoded list of 6 templates with all 30+ available templates
- Add category information to templates
- Combine static and dynamic templates with static templates displayed first
- Add loading and error states for template list
- Fix showDetail method for agent configuration
This commit is contained in:
anierbeck
2026-03-21 17:03:09 +01:00
parent f66c2525cb
commit 7a2211d0f4
4 changed files with 3581 additions and 153 deletions
Generated
+14 -14
View File
@@ -3812,7 +3812,7 @@ dependencies = [
[[package]]
name = "openfang-api"
version = "0.4.9"
version = "0.5.1"
dependencies = [
"async-trait",
"axum",
@@ -3852,7 +3852,7 @@ dependencies = [
[[package]]
name = "openfang-channels"
version = "0.4.9"
version = "0.5.1"
dependencies = [
"aes",
"async-trait",
@@ -3889,7 +3889,7 @@ dependencies = [
[[package]]
name = "openfang-cli"
version = "0.4.9"
version = "0.5.1"
dependencies = [
"clap",
"clap_complete",
@@ -3916,7 +3916,7 @@ dependencies = [
[[package]]
name = "openfang-desktop"
version = "0.4.9"
version = "0.5.1"
dependencies = [
"axum",
"open",
@@ -3942,7 +3942,7 @@ dependencies = [
[[package]]
name = "openfang-extensions"
version = "0.4.9"
version = "0.5.1"
dependencies = [
"aes-gcm",
"argon2",
@@ -3970,7 +3970,7 @@ dependencies = [
[[package]]
name = "openfang-hands"
version = "0.4.9"
version = "0.5.1"
dependencies = [
"chrono",
"dashmap",
@@ -3987,7 +3987,7 @@ dependencies = [
[[package]]
name = "openfang-kernel"
version = "0.4.9"
version = "0.5.1"
dependencies = [
"async-trait",
"chrono",
@@ -4025,7 +4025,7 @@ dependencies = [
[[package]]
name = "openfang-memory"
version = "0.4.9"
version = "0.5.1"
dependencies = [
"async-trait",
"chrono",
@@ -4044,7 +4044,7 @@ dependencies = [
[[package]]
name = "openfang-migrate"
version = "0.4.9"
version = "0.5.1"
dependencies = [
"chrono",
"dirs 6.0.0",
@@ -4063,7 +4063,7 @@ dependencies = [
[[package]]
name = "openfang-runtime"
version = "0.4.9"
version = "0.5.1"
dependencies = [
"anyhow",
"async-trait",
@@ -4097,7 +4097,7 @@ dependencies = [
[[package]]
name = "openfang-skills"
version = "0.4.9"
version = "0.5.1"
dependencies = [
"chrono",
"hex",
@@ -4120,7 +4120,7 @@ dependencies = [
[[package]]
name = "openfang-types"
version = "0.4.9"
version = "0.5.1"
dependencies = [
"async-trait",
"chrono",
@@ -4139,7 +4139,7 @@ dependencies = [
[[package]]
name = "openfang-wire"
version = "0.4.9"
version = "0.5.1"
dependencies = [
"async-trait",
"chrono",
@@ -8818,7 +8818,7 @@ checksum = "b9cc00251562a284751c9973bace760d86c0276c471b4be569fe6b068ee97a56"
[[package]]
name = "xtask"
version = "0.4.9"
version = "0.5.1"
[[package]]
name = "yoke"
+22
View File
@@ -3110,9 +3110,13 @@ pub async fn list_templates() -> impl IntoResponse {
.map(|m| m.description)
.unwrap_or_default();
// Add category based on template name
let category = get_template_category(&name);
templates.push(serde_json::json!({
"name": name,
"description": description,
"category": category,
}));
}
}
@@ -3125,6 +3129,24 @@ pub async fn list_templates() -> impl IntoResponse {
}))
}
fn get_template_category(name: &str) -> &str {
match name {
"hello-world" | "assistant" => "General",
"researcher" | "analyst" => "Research",
"coder" | "debugger" | "devops-lead" => "Development",
"writer" | "doc-writer" => "Writing",
"ops" | "planner" => "Operations",
"architect" | "security-auditor" => "Development",
"code-reviewer" | "data-scientist" | "test-engineer" => "Development",
"legal-assistant" | "email-assistant" | "social-media" => "Business",
"customer-support" | "sales-assistant" | "recruiter" => "Business",
"meeting-assistant" => "Business",
"translator" | "tutor" | "health-tracker" => "General",
"personal-finance" | "travel-planner" | "home-automation" => "General",
_ => "General",
}
}
/// GET /api/templates/:name — Get template details.
pub async fn get_template(Path(name): Path<String>) -> impl IntoResponse {
let agents_dir = openfang_kernel::config::openfang_home().join("agents");
+555 -139
View File
@@ -13,7 +13,7 @@ function tomlMultilineEscape(s) {
* Backslashes, double-quotes, and common control chars are escaped.
*/
function tomlBasicEscape(s) {
return s.replace(/\\/g, '\\\\').replace(/"/g, '\"').replace(/\n/g, '\\n').replace(/\r/g, '\\r').replace(/\t/g, '\\t');
return s.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\n/g, '\\n').replace(/\r/g, '\\r').replace(/\t/g, '\\t');
}
function agentsPage() {
@@ -77,6 +77,8 @@ function agentsPage() {
// -- Model switch --
editingModel: false,
newModelValue: '',
editingProvider: false,
newProviderValue: '',
modelSaving: false,
// -- Fallback chain --
editingFallback: false,
@@ -89,144 +91,12 @@ function agentsPage() {
tplLoadError: '',
selectedCategory: 'All',
searchQuery: '',
templatesLoading: true,
templatesError: null,
builtinTemplates: [],
// Load templates from API
created() {
this.loadTemplates();
},
methods: {
loadTemplates() {
this.templatesLoading = true;
this.templatesError = null;
fetch('/api/templates')
.then(response => {
if (!response.ok) throw new Error('Failed to load templates');
return response.json();
})
.then(data => {
// Map API data to expected format
this.builtinTemplates = data.templates.map(template => ({
name: template.name,
description: template.description,
category: this.getTemplateCategory(template.name),
provider: 'groq',
model: 'llama-3.3-70b-versatile',
profile: 'full',
system_prompt: 'You are a helpful, friendly assistant. Provide clear, accurate, and concise responses. Ask clarifying questions when needed.'
}));
})
.catch(error => {
console.error('Error loading templates:', error);
this.templatesError = 'Failed to load agent templates';
// Fallback to hardcoded templates if available
this.builtinTemplates = this.getFallbackTemplates();
})
.finally(() => {
this.templatesLoading = false;
});
},
getTemplateCategory(name) {
// Map template names to categories
const categoryMap = {
'hello-world': 'General',
'assistant': 'General',
'researcher': 'Research',
'coder': 'Development',
'writer': 'Writing',
'analyst': 'Analysis',
'ops': 'Operations',
'planner': 'Business',
'debugger': 'Development',
'devops-lead': 'Development',
'code-reviewer': 'Development',
'data-scientist': 'Analysis',
'test-engineer': 'Development',
'legal-assistant': 'Business',
'doc-writer': 'Writing',
'email-assistant': 'Business',
'social-media': 'Business',
'customer-support': 'Business',
'sales-assistant': 'Business',
'recruiter': 'Business',
'meeting-assistant': 'Business',
'translator': 'General',
'tutor': 'General',
'health-tracker': 'General',
'personal-finance': 'General',
'travel-planner': 'General',
'home-automation': 'General',
'architect': 'Development',
'security-auditor': 'Development'
};
return categoryMap[name] || 'General';
},
getFallbackTemplates() {
// Return hardcoded templates as fallback
return [
{
name: 'General Assistant',
description: 'A versatile conversational agent that can help with everyday tasks, answer questions, and provide recommendations.',
category: 'General',
provider: 'groq',
model: 'llama-3.3-70b-versatile',
profile: 'full',
system_prompt: 'You are a helpful, friendly assistant. Provide clear, accurate, and concise responses. Ask clarifying questions when needed.'
},
{
name: 'Code Helper',
description: 'A programming-focused agent that writes, reviews, and debugs code across multiple languages.',
category: 'Development',
provider: 'groq',
model: 'llama-3.3-70b-versatile',
profile: 'coding',
system_prompt: 'You are an expert programmer. Help users write clean, efficient code. Explain your reasoning. Follow best practices and conventions for the language being used.'
},
{
name: 'Researcher',
description: 'An analytical agent that breaks down complex topics, synthesizes information, and provides cited summaries.',
category: 'Research',
provider: 'groq',
model: 'llama-3.3-70b-versatile',
profile: 'research',
system_prompt: 'You are a research analyst. Break down complex topics into clear explanations. Provide structured analysis with key findings. Cite sources when available.'
},
{
name: 'Writer',
description: 'A creative writing agent that helps with drafting, editing, and improving written content of all kinds.',
category: 'Writing',
provider: 'groq',
model: 'llama-3.3-70b-versatile',
profile: 'full',
system_prompt: 'You are a skilled writer and editor. Help users create polished content. Adapt your tone and style to match the intended audience. Offer constructive suggestions for improvement.'
},
{
name: 'Data Analyst',
description: 'A data-focused agent that helps analyze datasets, create queries, and interpret statistical results.',
category: 'Development',
provider: 'groq',
model: 'llama-3.3-70b-versatile',
profile: 'coding',
system_prompt: 'You are a data analysis expert. Help users understand their data, write SQL/Python queries, and interpret results. Present findings clearly with actionable insights.'
},
{
name: 'DevOps Engineer',
description: 'A systems-focused agent for CI/CD, infrastructure, Docker, and deployment troubleshooting.',
category: 'Development',
provider: 'groq',
model: 'llama-3.3-70b-versatile',
profile: 'automation',
system_prompt: 'You are a DevOps engineer. Help with CI/CD pipelines, Docker, Kubernetes, infrastructure as code, and deployment. Prioritize reliability and security.'
}
];
}
async init() {
await this.loadTemplates();
},
// ── Profile Descriptions ──
@@ -313,11 +183,557 @@ function agentsPage() {
},
isProviderConfigured(providerName) {
if (!providerName) return true;
if (!providerName) return false;
var p = this.tplProviders.find(function(pr) { return pr.id === providerName; });
return p ? p.auth_status === 'configured' : false;
},
// ... (rest of the file remains unchanged)
async init() {
var self = this;
this.loading = true;
this.loadError = '';
try {
await Alpine.store('app').refreshAgents();
await this.loadTemplates();
} catch(e) {
this.loadError = e.message || 'Could not load agents. Is the daemon running?';
}
this.loading = false;
// If a pending agent was set (e.g. from wizard or redirect), open chat inline
var store = Alpine.store('app');
if (store.pendingAgent) {
this.activeChatAgent = store.pendingAgent;
}
// Watch for future pendingAgent changes
this.$watch('$store.app.pendingAgent', function(agent) {
if (agent) {
self.activeChatAgent = agent;
}
});
},
async loadData() {
this.loading = true;
this.loadError = '';
try {
await Alpine.store('app').refreshAgents();
} catch(e) {
this.loadError = e.message || 'Could not load agents.';
}
this.loading = false;
},
async loadTemplates() {
this.tplLoading = true;
this.tplLoadError = '';
try {
var results = await Promise.all([
OpenFangAPI.get('/api/templates'),
OpenFangAPI.get('/api/providers').catch(function() { return { providers: [] }; })
]);
// Combine static and dynamic templates
this.builtinTemplates = [
{
name: 'General Assistant',
description: 'A versatile conversational agent that can help with everyday tasks, answer questions, and provide recommendations.',
category: 'General',
provider: 'groq',
model: 'llama-3.3-70b-versatile',
profile: 'full',
system_prompt: 'You are a helpful, friendly assistant. Provide clear, accurate, and concise responses. Ask clarifying questions when needed.'
},
{
name: 'Code Helper',
description: 'A programming-focused agent that writes, reviews, and debugs code across multiple languages.',
category: 'Development',
provider: 'groq',
model: 'llama-3.3-70b-versatile',
profile: 'coding',
system_prompt: 'You are an expert programmer. Help users write clean, efficient code. Explain your reasoning. Follow best practices and conventions for the language being used.'
},
{
name: 'Researcher',
description: 'An analytical agent that breaks down complex topics, synthesizes information, and provides cited summaries.',
category: 'Research',
provider: 'groq',
model: 'llama-3.3-70b-versatile',
profile: 'research',
system_prompt: 'You are a research analyst. Break down complex topics into clear explanations. Provide structured analysis with key findings. Cite sources when available.'
},
{
name: 'Writer',
description: 'A creative writing agent that helps with drafting, editing, and improving written content of all kinds.',
category: 'Writing',
provider: 'groq',
model: 'llama-3.3-70b-versatile',
profile: 'full',
system_prompt: 'You are a skilled writer and editor. Help users create polished content. Adapt your tone and style to match the intended audience. Offer constructive suggestions for improvement.'
},
{
name: 'Data Analyst',
description: 'A data-focused agent that helps analyze datasets, create queries, and interpret statistical results.',
category: 'Development',
provider: 'groq',
model: 'llama-3.3-70b-versatile',
profile: 'coding',
system_prompt: 'You are a data analysis expert. Help users understand their data, write SQL/Python queries, and interpret results. Present findings clearly with actionable insights.'
},
{
name: 'DevOps Engineer',
description: 'A systems-focused agent for CI/CD, infrastructure, Docker, and deployment troubleshooting.',
category: 'Development',
provider: 'groq',
model: 'llama-3.3-70b-versatile',
profile: 'automation',
system_prompt: 'You are a DevOps engineer. Help with CI/CD pipelines, Docker, Kubernetes, infrastructure as code, and deployment. Prioritize reliability and security.'
},
...results[0].templates || []
];
this.tplProviders = results[1].providers || [];
} catch(e) {
this.builtinTemplates = [];
this.tplLoadError = e.message || 'Could not load templates.';
}
this.tplLoading = false;
},
chatWithAgent(agent) {
Alpine.store('app').pendingAgent = agent;
this.activeChatAgent = agent;
},
closeChat() {
this.activeChatAgent = null;
OpenFangAPI.wsDisconnect();
},
async showDetail(agent) {
this.detailAgent = agent;
this.detailAgent._fallbacks = [];
this.detailTab = 'info';
this.agentFiles = [];
this.editingFile = null;
this.fileContent = '';
this.editingFallback = false;
this.newFallbackValue = '';
this.configForm = {
name: agent.name || '',
system_prompt: agent.system_prompt || '',
emoji: (agent.identity && agent.identity.emoji) || '',
color: (agent.identity && agent.identity.color) || '#FF5C00',
archetype: (agent.identity && agent.identity.archetype) || '',
vibe: (agent.identity && agent.identity.vibe) || ''
};
this.showDetailModal = true;
// Fetch full agent detail to get fallback_models
try {
var full = await OpenFangAPI.get('/api/agents/' + agent.id);
this.detailAgent._fallbacks = full.fallback_models || [];
} catch(e) { /* ignore */ }
},
killAgent(agent) {
var self = this;
OpenFangToast.confirm('Stop Agent', 'Stop agent "' + agent.name + '"? The agent will be shut down.', async function() {
try {
await OpenFangAPI.del('/api/agents/' + agent.id);
OpenFangToast.success('Agent "' + agent.name + '" stopped');
self.showDetailModal = false;
await Alpine.store('app').refreshAgents();
} catch(e) {
OpenFangToast.error('Failed to stop agent: ' + e.message);
}
});
},
killAllAgents() {
var list = this.filteredAgents;
if (!list.length) return;
OpenFangToast.confirm('Stop All Agents', 'Stop ' + list.length + ' agent(s)? All agents will be shut down.', async function() {
var errors = [];
for (var i = 0; i < list.length; i++) {
try {
await OpenFangAPI.del('/api/agents/' + list[i].id);
} catch(e) { errors.push(list[i].name + ': ' + e.message); }
}
await Alpine.store('app').refreshAgents();
if (errors.length) {
OpenFangToast.error('Some agents failed to stop: ' + errors.join(', '));
} else {
OpenFangToast.success(list.length + ' agent(s) stopped');
}
});
},
// ── Multi-step wizard navigation ──
async openSpawnWizard() {
this.showSpawnModal = true;
this.spawnStep = 1;
this.spawnMode = 'wizard';
this.spawnIdentity = { emoji: '', color: '#FF5C00', archetype: '' };
this.selectedPreset = '';
this.soulContent = '';
this.spawnForm.name = '';
this.spawnForm.provider = 'groq';
this.spawnForm.model = 'llama-3.3-70b-versatile';
this.spawnForm.systemPrompt = 'You are a helpful assistant.';
this.spawnForm.profile = 'full';
try {
var res = await fetch('/api/status');
if (res.ok) {
var status = await res.json();
if (status.default_provider) this.spawnForm.provider = status.default_provider;
if (status.default_model) this.spawnForm.model = status.default_model;
}
} catch(e) { /* keep hardcoded defaults */ }
},
nextStep() {
if (this.spawnStep === 1 && !this.spawnForm.name.trim()) {
OpenFangToast.warn('Please enter an agent name');
return;
}
if (this.spawnStep < 5) this.spawnStep++;
},
prevStep() {
if (this.spawnStep > 1) this.spawnStep--;
},
selectPreset(preset) {
this.selectedPreset = preset.id;
this.soulContent = preset.soul;
},
generateToml() {
var f = this.spawnForm;
var si = this.spawnIdentity;
var lines = [
'name = "' + tomlBasicEscape(f.name) + '"',
'module = "builtin:chat"'
];
if (f.profile && f.profile !== 'custom') {
lines.push('profile = "' + f.profile + '"');
}
lines.push('', '[model]');
lines.push('provider = "' + f.provider + '"');
lines.push('model = "' + f.model + '"');
lines.push('system_prompt = """\n' + tomlMultilineEscape(f.systemPrompt) + '\n"""');
if (f.profile === 'custom') {
lines.push('', '[capabilities]');
if (f.caps.memory_read) lines.push('memory_read = ["*"]');
if (f.caps.memory_write) lines.push('memory_write = ["self.*"]');
if (f.caps.network) lines.push('network = ["*"]');
if (f.caps.shell) lines.push('shell = ["*"]');
if (f.caps.agent_spawn) lines.push('agent_spawn = true');
}
return lines.join('\n');
},
async setMode(agent, mode) {
try {
await OpenFangAPI.put('/api/agents/' + agent.id + '/mode', { mode: mode });
agent.mode = mode;
OpenFangToast.success('Mode set to ' + mode);
await Alpine.store('app').refreshAgents();
} catch(e) {
OpenFangToast.error('Failed to set mode: ' + e.message);
}
},
async spawnAgent() {
this.spawning = true;
var toml = this.spawnMode === 'wizard' ? this.generateToml() : this.spawnToml;
if (!toml.trim()) {
this.spawning = false;
OpenFangToast.warn('Manifest is empty \u2014 enter agent config first');
return;
}
try {
var res = await OpenFangAPI.post('/api/agents', { manifest_toml: toml });
if (res.agent_id) {
// Post-spawn: update identity + write SOUL.md if personality preset selected
var patchBody = {};
if (this.spawnIdentity.emoji) patchBody.emoji = this.spawnIdentity.emoji;
if (this.spawnIdentity.color) patchBody.color = this.spawnIdentity.color;
if (this.spawnIdentity.archetype) patchBody.archetype = this.spawnIdentity.archetype;
if (this.selectedPreset) patchBody.vibe = this.selectedPreset;
if (Object.keys(patchBody).length) {
OpenFangAPI.patch('/api/agents/' + res.agent_id + '/config', patchBody).catch(function(e) { console.warn('Post-spawn config patch failed:', e.message); });
}
if (this.soulContent.trim()) {
OpenFangAPI.put('/api/agents/' + res.agent_id + '/files/SOUL.md', { content: '# Soul\n' + this.soulContent }).catch(function(e) { console.warn('SOUL.md write failed:', e.message); });
}
this.showSpawnModal = false;
this.spawnForm.name = '';
this.spawnToml = '';
this.spawnStep = 1;
OpenFangToast.success('Agent "' + (res.name || 'new') + '" spawned');
await Alpine.store('app').refreshAgents();
this.chatWithAgent({ id: res.agent_id, name: res.name, model_provider: '?', model_name: '?' });
} else {
OpenFangToast.error('Spawn failed: ' + (res.error || 'Unknown error'));
}
} catch(e) {
OpenFangToast.error('Failed to spawn agent: ' + e.message);
}
this.spawning = false;
},
// ── Detail modal: Files tab ──
async loadAgentFiles() {
if (!this.detailAgent) return;
this.filesLoading = true;
try {
var data = await OpenFangAPI.get('/api/agents/' + this.detailAgent.id + '/files');
this.agentFiles = data.files || [];
} catch(e) {
this.agentFiles = [];
OpenFangToast.error('Failed to load files: ' + e.message);
}
this.filesLoading = false;
},
async openFile(file) {
if (!file.exists) {
// Create with empty content
this.editingFile = file.name;
this.fileContent = '';
return;
}
try {
var data = await OpenFangAPI.get('/api/agents/' + this.detailAgent.id + '/files/' + encodeURIComponent(file.name));
this.editingFile = file.name;
this.fileContent = data.content || '';
} catch(e) {
OpenFangToast.error('Failed to read file: ' + e.message);
}
},
async saveFile() {
if (!this.editingFile || !this.detailAgent) return;
this.fileSaving = true;
try {
await OpenFangAPI.put('/api/agents/' + this.detailAgent.id + '/files/' + encodeURIComponent(this.editingFile), { content: this.fileContent });
OpenFangToast.success(this.editingFile + ' saved');
await this.loadAgentFiles();
} catch(e) {
OpenFangToast.error('Failed to save file: ' + e.message);
}
this.fileSaving = false;
},
closeFileEditor() {
this.editingFile = null;
this.fileContent = '';
},
// ── Detail modal: Config tab ──
async saveConfig() {
if (!this.detailAgent) return;
this.configSaving = true;
try {
await OpenFangAPI.patch('/api/agents/' + this.detailAgent.id + '/config', this.configForm);
OpenFangToast.success('Config updated');
await Alpine.store('app').refreshAgents();
} catch(e) {
OpenFangToast.error('Failed to save config: ' + e.message);
}
this.configSaving = false;
},
// ── Clone agent ──
async cloneAgent(agent) {
var newName = (agent.name || 'agent') + '-copy';
try {
var res = await OpenFangAPI.post('/api/agents/' + agent.id + '/clone', { new_name: newName });
if (res.agent_id) {
OpenFangToast.success('Cloned as "' + res.name + '"');
await Alpine.store('app').refreshAgents();
this.showDetailModal = false;
}
} catch(e) {
OpenFangToast.error('Clone failed: ' + e.message);
}
},
// -- Template methods --
async spawnFromTemplate(name) {
try {
var data = await OpenFangAPI.get('/api/templates/' + encodeURIComponent(name));
if (data.manifest_toml) {
var res = await OpenFangAPI.post('/api/agents', { manifest_toml: data.manifest_toml });
if (res.agent_id) {
OpenFangToast.success('Agent "' + (res.name || name) + '" spawned from template');
await Alpine.store('app').refreshAgents();
this.chatWithAgent({ id: res.agent_id, name: res.name || name, model_provider: '?', model_name: '?' });
}
}
} catch(e) {
OpenFangToast.error('Failed to spawn from template: ' + e.message);
}
},
// ── Clear agent history ──
async clearHistory(agent) {
var self = this;
OpenFangToast.confirm('Clear History', 'Clear all conversation history for "' + agent.name + '"? This cannot be undone.', async function() {
try {
await OpenFangAPI.del('/api/agents/' + agent.id + '/history');
OpenFangToast.success('History cleared for "' + agent.name + '"');
} catch(e) {
OpenFangToast.error('Failed to clear history: ' + e.message);
}
});
},
// ── Model switch ──
async changeModel() {
if (!this.detailAgent || !this.newModelValue.trim()) return;
this.modelSaving = true;
try {
var resp = await OpenFangAPI.put('/api/agents/' + this.detailAgent.id + '/model', { model: this.newModelValue.trim() });
var providerInfo = (resp && resp.provider) ? ' (provider: ' + resp.provider + ')' : '';
OpenFangToast.success('Model changed' + providerInfo + ' (memory reset)');
this.editingModel = false;
await Alpine.store('app').refreshAgents();
// Refresh detailAgent
var agents = Alpine.store('app').agents;
for (var i = 0; i < agents.length; i++) {
if (agents[i].id === this.detailAgent.id) { this.detailAgent = agents[i]; break; }
}
} catch(e) {
OpenFangToast.error('Failed to change model: ' + e.message);
}
this.modelSaving = false;
},
// ── Provider switch ──
async changeProvider() {
if (!this.detailAgent || !this.newProviderValue.trim()) return;
this.modelSaving = true;
try {
var combined = this.newProviderValue.trim() + '/' + this.detailAgent.model_name;
var resp = await OpenFangAPI.put('/api/agents/' + this.detailAgent.id + '/model', { model: combined });
OpenFangToast.success('Provider changed to ' + (resp && resp.provider ? resp.provider : this.newProviderValue.trim()));
this.editingProvider = false;
await Alpine.store('app').refreshAgents();
var agents = Alpine.store('app').agents;
for (var i = 0; i < agents.length; i++) {
if (agents[i].id === this.detailAgent.id) { this.detailAgent = agents[i]; break; }
}
} catch(e) {
OpenFangToast.error('Failed to change provider: ' + e.message);
}
this.modelSaving = false;
},
// ── Fallback model chain ──
async addFallback() {
if (!this.detailAgent || !this.newFallbackValue.trim()) return;
var parts = this.newFallbackValue.trim().split('/');
var provider = parts.length > 1 ? parts[0] : this.detailAgent.model_provider;
var model = parts.length > 1 ? parts.slice(1).join('/') : parts[0];
if (!this.detailAgent._fallbacks) this.detailAgent._fallbacks = [];
this.detailAgent._fallbacks.push({ provider: provider, model: model });
try {
await OpenFangAPI.patch('/api/agents/' + this.detailAgent.id + '/config', {
fallback_models: this.detailAgent._fallbacks
});
OpenFangToast.success('Fallback added: ' + provider + '/' + model);
} catch(e) {
OpenFangToast.error('Failed to save fallbacks: ' + e.message);
this.detailAgent._fallbacks.pop();
}
this.editingFallback = false;
this.newFallbackValue = '';
},
async removeFallback(idx) {
if (!this.detailAgent || !this.detailAgent._fallbacks) return;
var removed = this.detailAgent._fallbacks.splice(idx, 1);
try {
await OpenFangAPI.patch('/api/agents/' + this.detailAgent.id + '/config', {
fallback_models: this.detailAgent._fallbacks
});
OpenFangToast.success('Fallback removed');
} catch(e) {
OpenFangToast.error('Failed to save fallbacks: ' + e.message);
this.detailAgent._fallbacks.splice(idx, 0, removed[0]);
}
},
// ── Tool filters ──
async loadToolFilters() {
if (!this.detailAgent) return;
this.toolFiltersLoading = true;
try {
this.toolFilters = await OpenFangAPI.get('/api/agents/' + this.detailAgent.id + '/tools');
} catch(e) {
this.toolFilters = { tool_allowlist: [], tool_blocklist: [] };
}
this.toolFiltersLoading = false;
},
addAllowTool() {
var t = this.newAllowTool.trim();
if (t && this.toolFilters.tool_allowlist.indexOf(t) === -1) {
this.toolFilters.tool_allowlist.push(t);
this.newAllowTool = '';
this.saveToolFilters();
}
},
removeAllowTool(tool) {
this.toolFilters.tool_allowlist = this.toolFilters.tool_allowlist.filter(function(t) { return t !== tool; });
this.saveToolFilters();
},
addBlockTool() {
var t = this.newBlockTool.trim();
if (t && this.toolFilters.tool_blocklist.indexOf(t) === -1) {
this.toolFilters.tool_blocklist.push(t);
this.newBlockTool = '';
this.saveToolFilters();
}
},
removeBlockTool(tool) {
this.toolFilters.tool_blocklist = this.toolFilters.tool_blocklist.filter(function(t) { return t !== tool; });
this.saveToolFilters();
},
async saveToolFilters() {
if (!this.detailAgent) return;
try {
await OpenFangAPI.put('/api/agents/' + this.detailAgent.id + '/tools', this.toolFilters);
} catch(e) {
OpenFangToast.error('Failed to update tool filters: ' + e.message);
}
},
async spawnBuiltin(t) {
var toml = 'name = "' + tomlBasicEscape(t.name) + '"\n';
toml += 'description = "' + tomlBasicEscape(t.description) + '"\n';
toml += 'module = "builtin:chat"\n';
toml += 'profile = "' + t.profile + '"\n\n';
toml += '[model]\nprovider = "' + t.provider + '"\nmodel = "' + t.model + '"\n';
toml += 'system_prompt = """\n' + tomlMultilineEscape(t.system_prompt) + '\n"""\n';
try {
var res = await OpenFangAPI.post('/api/agents', { manifest_toml: toml });
if (res.agent_id) {
OpenFangToast.success('Agent "' + t.name + '" spawned');
await Alpine.store('app').refreshAgents();
this.chatWithAgent({ id: res.agent_id, name: t.name, model_provider: t.provider, model_name: t.model });
}
} catch(e) {
OpenFangToast.error('Failed to spawn agent: ' + e.message);
}
}
};
}
}
File diff suppressed because it is too large Load Diff