Add AI prompt tool for natural language graph operations

New MCP tool and portal UI for executing natural language instructions
against the memory graph via Ollama (qwen3-coder:30b). Single LLM call
generates a JSON action plan which is executed sequentially.

Supports 8 action types: add_node, update_node, remove_node, add_edge,
remove_edge, bulk_tag, reorganize, query. Actions can reference previous
results via $result[N].field interpolation. Uses /api/chat with few-shot
assistant example, format:json, and temperature:0 for reliable output.
This commit is contained in:
2026-02-03 02:40:01 +01:00
parent f2f9d729da
commit 661325a235
11 changed files with 569 additions and 3 deletions

View File

@@ -0,0 +1,137 @@
import { useState } from 'react';
import { api } from '../api';
import type { PromptResult } from '../types';
interface Props {
onClose: () => void;
onDone: () => void;
}
export default function PromptPanel({ onClose, onDone }: Props) {
const [text, setText] = useState('');
const [result, setResult] = useState<PromptResult | null>(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const submit = async () => {
if (!text.trim()) return;
setLoading(true);
setError(null);
setResult(null);
try {
const data = await api.prompt(text);
setResult(data);
if (data.success) onDone();
} catch (err: any) {
setError(err.message || 'Request failed');
} finally {
setLoading(false);
}
};
return (
<div className="h-full flex flex-col bg-gray-900/95 backdrop-blur border-t border-amber-500/30 rounded-t-xl">
{/* Header */}
<div className="flex items-center justify-between px-4 py-3 border-b border-gray-700/50">
<span className="text-amber-300 font-medium text-sm">AI Prompt</span>
<button onClick={onClose} className="text-gray-500 hover:text-gray-300 text-lg">&times;</button>
</div>
{/* Input */}
<div className="px-4 py-3 flex gap-2">
<input
value={text}
onChange={e => setText(e.target.value)}
onKeyDown={e => e.key === 'Enter' && !loading && submit()}
placeholder='e.g. "create a decision node about using Redis for caching"'
className="flex-1 bg-gray-800 border border-gray-600 rounded-lg px-3 py-2 text-sm text-gray-200 placeholder-gray-500 focus:outline-none focus:border-amber-500"
autoFocus
disabled={loading}
/>
<button
onClick={submit}
disabled={loading || !text.trim()}
className="px-4 py-2 bg-amber-600 hover:bg-amber-500 disabled:opacity-50 rounded-lg text-sm text-white font-medium min-w-[80px]"
>
{loading ? (
<span className="inline-block animate-pulse">Running...</span>
) : 'Run'}
</button>
</div>
{/* Results */}
<div className="flex-1 overflow-y-auto px-4 pb-4">
{!result && !loading && !error && (
<div className="text-gray-500 text-sm mt-4 space-y-1">
<p>Give a natural language instruction to modify the graph:</p>
<p className="text-gray-600 ml-2">"create a decision node about using Redis for caching"</p>
<p className="text-gray-600 ml-2">"tag all task nodes with 'backlog'"</p>
<p className="text-gray-600 ml-2">"create a component for auth and link it to the API gateway"</p>
</div>
)}
{loading && (
<div className="text-amber-400/70 text-sm mt-4 animate-pulse">
Generating and executing action plan...
</div>
)}
{error && (
<div className="mt-4 p-3 rounded-lg bg-red-900/30 border border-red-700/50 text-red-300 text-sm">
{error}
</div>
)}
{result && (
<div className="space-y-3 mt-2">
{/* Summary */}
<div className={`p-3 rounded-lg border text-sm ${
result.success
? 'bg-green-900/20 border-green-700/50 text-green-300'
: 'bg-yellow-900/20 border-yellow-700/50 text-yellow-300'
}`}>
<div className="font-medium mb-1">{result.success ? 'Completed' : 'Completed with errors'}</div>
<div>{result.summary}</div>
</div>
{/* Reasoning */}
{result.reasoning && (
<div className="text-xs text-gray-400 bg-gray-800/50 rounded-lg p-3">
<span className="text-gray-500 font-medium">Reasoning: </span>
{result.reasoning}
</div>
)}
{/* Execution log */}
{result.executionLog.length > 0 && (
<div className="space-y-1">
<div className="text-xs text-gray-500 font-medium uppercase tracking-wide">Execution Log</div>
{result.executionLog.map((entry, i) => (
<div
key={i}
className={`flex items-start gap-2 p-2 rounded text-sm ${
entry.status === 'completed' ? 'bg-gray-800/30' : 'bg-red-900/20'
}`}
>
<span className={`mt-0.5 text-xs ${
entry.status === 'completed' ? 'text-green-500' : 'text-red-500'
}`}>
{entry.status === 'completed' ? '✓' : '✗'}
</span>
<div className="flex-1 min-w-0">
<div className="text-gray-300 truncate">{entry.description}</div>
<div className="text-xs text-gray-500">{entry.action}</div>
{entry.error && (
<div className="text-xs text-red-400 mt-1">{entry.error}</div>
)}
</div>
</div>
))}
</div>
)}
</div>
)}
</div>
</div>
);
}