Initial commit: RA-H Open Source Edition
Local-first knowledge management system with BYO API keys. Features: - 3-panel UI (Nodes | Focus | Helpers) - SQLite + sqlite-vec for vector search - Agent system (Easy/Hard mode orchestrators) - Content extraction (YouTube, PDF, web) - Integrate workflow for connection discovery - Dimension system with auto-assignment Tech stack: - Next.js 15 + TypeScript + Tailwind CSS - Anthropic (Claude) + OpenAI (GPT) via Vercel AI SDK Setup: npm install && npm rebuild better-sqlite3 scripts/dev/bootstrap-local.sh npm run dev MIT License
This commit is contained in:
@@ -0,0 +1,70 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { AgentDelegationService } from '@/services/agents/delegation';
|
||||
|
||||
export async function GET(
|
||||
_request: NextRequest,
|
||||
{ params }: { params: Promise<{ sessionId: string }> }
|
||||
) {
|
||||
try {
|
||||
const { sessionId } = await params;
|
||||
const delegation = AgentDelegationService.getBySessionId(sessionId);
|
||||
if (!delegation) {
|
||||
return NextResponse.json({ error: 'Delegation not found' }, { status: 404 });
|
||||
}
|
||||
return NextResponse.json({ delegation });
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch delegation:', error);
|
||||
return NextResponse.json({ error: 'Failed to fetch delegation' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
export async function PATCH(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ sessionId: string }> }
|
||||
) {
|
||||
try {
|
||||
const { sessionId } = await params;
|
||||
const body = await request.json();
|
||||
const summary: string | undefined = body?.summary;
|
||||
const status: string | undefined = body?.status;
|
||||
|
||||
if (!summary && !status) {
|
||||
return NextResponse.json({ error: 'Nothing to update' }, { status: 400 });
|
||||
}
|
||||
|
||||
const normalizedStatus = status && ['queued', 'in_progress', 'completed', 'failed'].includes(status)
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
? (status as any)
|
||||
: undefined;
|
||||
|
||||
const delegation = summary
|
||||
? AgentDelegationService.completeDelegation(sessionId, summary, normalizedStatus ?? 'completed')
|
||||
: AgentDelegationService.markInProgress(sessionId);
|
||||
|
||||
if (!delegation) {
|
||||
return NextResponse.json({ error: 'Delegation not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
return NextResponse.json({ delegation });
|
||||
} catch (error) {
|
||||
console.error('Failed to update delegation:', error);
|
||||
return NextResponse.json({ error: 'Failed to update delegation' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
export async function DELETE(
|
||||
_request: NextRequest,
|
||||
{ params }: { params: Promise<{ sessionId: string }> }
|
||||
) {
|
||||
try {
|
||||
const { sessionId } = await params;
|
||||
const deleted = AgentDelegationService.deleteDelegation(sessionId);
|
||||
if (!deleted) {
|
||||
return NextResponse.json({ error: 'Delegation not found' }, { status: 404 });
|
||||
}
|
||||
return NextResponse.json({ success: true });
|
||||
} catch (error) {
|
||||
console.error('Failed to delete delegation:', error);
|
||||
return NextResponse.json({ error: 'Failed to delete delegation' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { AgentDelegationService } from '@/services/agents/delegation';
|
||||
|
||||
export async function POST(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ sessionId: string }> }
|
||||
) {
|
||||
try {
|
||||
const { sessionId } = await params;
|
||||
const body = await request.json();
|
||||
const summary: string | undefined = body?.summary;
|
||||
const status: string | undefined = body?.status;
|
||||
|
||||
if (!summary) {
|
||||
return NextResponse.json({ error: 'Summary is required' }, { status: 400 });
|
||||
}
|
||||
|
||||
const normalizedStatus = status && ['queued', 'in_progress', 'completed', 'failed'].includes(status)
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
? (status as any)
|
||||
: 'completed';
|
||||
|
||||
const delegation = AgentDelegationService.completeDelegation(sessionId, summary, normalizedStatus);
|
||||
if (!delegation) {
|
||||
return NextResponse.json({ error: 'Delegation not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
return NextResponse.json({ delegation });
|
||||
} catch (error) {
|
||||
console.error('Failed to store delegation summary:', error);
|
||||
return NextResponse.json({ error: 'Failed to store delegation summary' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { AgentDelegationService } from '@/services/agents/delegation';
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const { searchParams } = new URL(request.url);
|
||||
const statusFilter = searchParams.get('status');
|
||||
|
||||
const includeCompleted = searchParams.get('includeCompleted') === 'true'
|
||||
|| statusFilter !== 'active';
|
||||
const delegations = statusFilter === 'active'
|
||||
? AgentDelegationService.listActive({ includeCompleted })
|
||||
: AgentDelegationService.listRecent();
|
||||
|
||||
return NextResponse.json({ delegations });
|
||||
} catch (error) {
|
||||
console.error('Failed to list delegations:', error);
|
||||
return NextResponse.json({ error: 'Failed to load delegations' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,179 @@
|
||||
import { NextRequest } from 'next/server';
|
||||
|
||||
export const runtime = 'nodejs';
|
||||
export const maxDuration = 900;
|
||||
|
||||
class DelegationStreamBroadcaster {
|
||||
private connections = new Map<string, Set<ReadableStreamDefaultController>>();
|
||||
private pendingMessages = new Map<string, any[]>();
|
||||
private encoder = new TextEncoder();
|
||||
|
||||
private encode(message: any) {
|
||||
return `data: ${JSON.stringify({ ...message, timestamp: Date.now() })}\n\n`;
|
||||
}
|
||||
|
||||
private send(controller: ReadableStreamDefaultController, encoded: string) {
|
||||
try {
|
||||
controller.enqueue(this.encoder.encode(encoded));
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.log('[DelegationStream] Removing dead connection', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
addConnection(sessionId: string, controller: ReadableStreamDefaultController) {
|
||||
if (!this.connections.has(sessionId)) {
|
||||
this.connections.set(sessionId, new Set());
|
||||
}
|
||||
this.connections.get(sessionId)!.add(controller);
|
||||
console.log(`[DelegationStream] Connection added for ${sessionId}, total: ${this.connections.get(sessionId)!.size}`);
|
||||
|
||||
const backlog = this.pendingMessages.get(sessionId);
|
||||
if (backlog && backlog.length > 0) {
|
||||
console.log(`[DelegationStream] Flushing ${backlog.length} queued events for ${sessionId}`);
|
||||
for (const message of backlog) {
|
||||
const encoded = this.encode(message);
|
||||
const delivered = this.send(controller, encoded);
|
||||
if (!delivered) {
|
||||
this.removeConnection(sessionId, controller);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if ((this.connections.get(sessionId)?.size || 0) > 0) {
|
||||
this.pendingMessages.delete(sessionId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
removeConnection(sessionId: string, controller: ReadableStreamDefaultController) {
|
||||
const sessionConns = this.connections.get(sessionId);
|
||||
if (sessionConns) {
|
||||
sessionConns.delete(controller);
|
||||
console.log(`[DelegationStream] Connection removed from ${sessionId}, remaining: ${sessionConns.size}`);
|
||||
if (sessionConns.size === 0) {
|
||||
this.connections.delete(sessionId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
broadcast(sessionId: string, message: any) {
|
||||
const sessionConns = this.connections.get(sessionId);
|
||||
if (!sessionConns || sessionConns.size === 0) {
|
||||
const queue = this.pendingMessages.get(sessionId) ?? [];
|
||||
queue.push(message);
|
||||
// Prevent unbounded growth by keeping the latest 200 events
|
||||
if (queue.length > 200) {
|
||||
queue.splice(0, queue.length - 200);
|
||||
}
|
||||
this.pendingMessages.set(sessionId, queue);
|
||||
console.log(`[DelegationStream] Queued event for ${sessionId}, pending=${queue.length}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const encoded = this.encode(message);
|
||||
|
||||
let successCount = 0;
|
||||
const staleControllers: ReadableStreamDefaultController[] = [];
|
||||
for (const controller of sessionConns) {
|
||||
if (this.send(controller, encoded)) {
|
||||
successCount++;
|
||||
} else {
|
||||
staleControllers.push(controller);
|
||||
}
|
||||
}
|
||||
|
||||
if (staleControllers.length > 0) {
|
||||
staleControllers.forEach((controller) => sessionConns.delete(controller));
|
||||
}
|
||||
|
||||
console.log(`[DelegationStream] Broadcasted to ${successCount}/${sessionConns.size} connections for ${sessionId}`);
|
||||
}
|
||||
|
||||
sendKeepAlive(sessionId: string) {
|
||||
const sessionConns = this.connections.get(sessionId);
|
||||
if (!sessionConns) return;
|
||||
|
||||
const ping = this.encoder.encode(`: keep-alive\n\n`);
|
||||
|
||||
for (const controller of sessionConns) {
|
||||
try {
|
||||
controller.enqueue(ping);
|
||||
} catch {
|
||||
sessionConns.delete(controller);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
// eslint-disable-next-line no-var
|
||||
var delegationStreamBroadcaster: DelegationStreamBroadcaster | undefined;
|
||||
}
|
||||
|
||||
export const delegationStreamBroadcaster =
|
||||
globalThis.delegationStreamBroadcaster ?? new DelegationStreamBroadcaster();
|
||||
|
||||
if (typeof window === 'undefined') {
|
||||
globalThis.delegationStreamBroadcaster = delegationStreamBroadcaster;
|
||||
}
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const { searchParams } = new URL(request.url);
|
||||
const sessionId = searchParams.get('sessionId');
|
||||
|
||||
if (!sessionId) {
|
||||
return new Response('Missing sessionId', { status: 400 });
|
||||
}
|
||||
|
||||
const stream = new ReadableStream({
|
||||
start(controller) {
|
||||
const state = this as unknown as { cleanup?: () => void; abortHandler?: () => void };
|
||||
delegationStreamBroadcaster.addConnection(sessionId, controller);
|
||||
|
||||
const encoder = new TextEncoder();
|
||||
controller.enqueue(encoder.encode(`data: ${JSON.stringify({ type: 'CONNECTION_ESTABLISHED', timestamp: Date.now() })}\n\n`));
|
||||
|
||||
const keepAliveInterval = setInterval(() => {
|
||||
delegationStreamBroadcaster.sendKeepAlive(sessionId);
|
||||
}, 30000);
|
||||
|
||||
const cleanup = () => {
|
||||
clearInterval(keepAliveInterval);
|
||||
delegationStreamBroadcaster.removeConnection(sessionId, controller);
|
||||
state.cleanup = undefined;
|
||||
};
|
||||
|
||||
const abortHandler = () => {
|
||||
cleanup();
|
||||
request.signal.removeEventListener('abort', abortHandler);
|
||||
state.abortHandler = undefined;
|
||||
};
|
||||
|
||||
request.signal.addEventListener('abort', abortHandler);
|
||||
|
||||
state.cleanup = cleanup;
|
||||
state.abortHandler = abortHandler;
|
||||
},
|
||||
cancel() {
|
||||
console.log(`[DelegationStream] Stream cancelled for ${sessionId}`);
|
||||
const state = this as unknown as { cleanup?: () => void; abortHandler?: () => void };
|
||||
if (state.abortHandler) {
|
||||
request.signal.removeEventListener('abort', state.abortHandler);
|
||||
state.abortHandler = undefined;
|
||||
}
|
||||
if (state.cleanup) {
|
||||
state.cleanup();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return new Response(stream, {
|
||||
headers: {
|
||||
'Content-Type': 'text/event-stream',
|
||||
'Cache-Control': 'no-cache, no-transform',
|
||||
'Connection': 'keep-alive',
|
||||
'X-Accel-Buffering': 'no',
|
||||
},
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user