mirror of
https://github.com/RightNow-AI/openfang.git
synced 2026-07-30 15:01:15 +00:00
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:
Generated
+14
-14
@@ -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"
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user