mirror of https://github.com/kortix-ai/suna.git
Merge pull request #822 from escapade-mckv/knowledge-base
Knowledge base
This commit is contained in:
commit
ccb04a2116
|
@ -11,7 +11,7 @@ from agent.tools.sb_expose_tool import SandboxExposeTool
|
|||
from agent.tools.web_search_tool import SandboxWebSearchTool
|
||||
from dotenv import load_dotenv
|
||||
from utils.config import config
|
||||
|
||||
from flags.flags import is_enabled
|
||||
from agent.agent_builder_prompt import get_agent_builder_prompt
|
||||
from agentpress.thread_manager import ThreadManager
|
||||
from agentpress.response_processor import ProcessorConfig
|
||||
|
@ -222,7 +222,27 @@ async def run_agent(
|
|||
system_content = default_system_content
|
||||
logger.info("Using default system prompt only")
|
||||
|
||||
# Add MCP tool information to system prompt if MCP tools are configured
|
||||
if await is_enabled("knowledge_base"):
|
||||
try:
|
||||
from services.supabase import DBConnection
|
||||
kb_db = DBConnection()
|
||||
kb_client = await kb_db.client
|
||||
|
||||
kb_result = await kb_client.rpc('get_knowledge_base_context', {
|
||||
'p_thread_id': thread_id,
|
||||
'p_max_tokens': 4000
|
||||
}).execute()
|
||||
|
||||
if kb_result.data and kb_result.data.strip():
|
||||
logger.info(f"Adding knowledge base context to system prompt for thread {thread_id}")
|
||||
system_content += "Here is the user's knowledge base context for this thread:\n\n" + kb_result.data
|
||||
else:
|
||||
logger.debug(f"No knowledge base context found for thread {thread_id}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error retrieving knowledge base context for thread {thread_id}: {e}")
|
||||
|
||||
|
||||
if agent_config and (agent_config.get('configured_mcps') or agent_config.get('custom_mcps')) and mcp_wrapper_instance and mcp_wrapper_instance._initialized:
|
||||
mcp_info = "\n\n--- MCP Tools Available ---\n"
|
||||
mcp_info += "You have access to external MCP (Model Context Protocol) server tools.\n"
|
||||
|
|
|
@ -152,9 +152,7 @@ from mcp_local import secure_api as secure_mcp_api
|
|||
app.include_router(mcp_api.router, prefix="/api")
|
||||
app.include_router(secure_mcp_api.router, prefix="/api/secure-mcp")
|
||||
|
||||
|
||||
app.include_router(transcription_api.router, prefix="/api")
|
||||
|
||||
app.include_router(email_api.router, prefix="/api")
|
||||
|
||||
from workflows import api as workflows_api
|
||||
|
@ -168,6 +166,9 @@ app.include_router(webhooks_api.router, prefix="/api")
|
|||
from scheduling import api as scheduling_api
|
||||
app.include_router(scheduling_api.router)
|
||||
|
||||
from knowledge_base import api as knowledge_base_api
|
||||
app.include_router(knowledge_base_api.router, prefix="/api")
|
||||
|
||||
@app.get("/api/health")
|
||||
async def health_check():
|
||||
"""Health check endpoint to verify API is working."""
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
# Knowledge Base Module
|
|
@ -0,0 +1,322 @@
|
|||
import json
|
||||
from typing import List, Optional
|
||||
from fastapi import APIRouter, HTTPException, Depends
|
||||
from pydantic import BaseModel, Field
|
||||
from utils.auth_utils import get_current_user_id_from_jwt
|
||||
from services.supabase import DBConnection
|
||||
from utils.logger import logger
|
||||
from flags.flags import is_enabled
|
||||
|
||||
router = APIRouter(prefix="/knowledge-base", tags=["knowledge-base"])
|
||||
|
||||
class KnowledgeBaseEntry(BaseModel):
|
||||
entry_id: Optional[str] = None
|
||||
name: str = Field(..., min_length=1, max_length=255)
|
||||
description: Optional[str] = None
|
||||
content: str = Field(..., min_length=1)
|
||||
usage_context: str = Field(default="always", pattern="^(always|on_request|contextual)$")
|
||||
is_active: bool = True
|
||||
|
||||
class KnowledgeBaseEntryResponse(BaseModel):
|
||||
entry_id: str
|
||||
name: str
|
||||
description: Optional[str]
|
||||
content: str
|
||||
usage_context: str
|
||||
is_active: bool
|
||||
content_tokens: Optional[int]
|
||||
created_at: str
|
||||
updated_at: str
|
||||
|
||||
class KnowledgeBaseListResponse(BaseModel):
|
||||
entries: List[KnowledgeBaseEntryResponse]
|
||||
total_count: int
|
||||
total_tokens: int
|
||||
|
||||
class CreateKnowledgeBaseEntryRequest(BaseModel):
|
||||
name: str = Field(..., min_length=1, max_length=255)
|
||||
description: Optional[str] = None
|
||||
content: str = Field(..., min_length=1)
|
||||
usage_context: str = Field(default="always", pattern="^(always|on_request|contextual)$")
|
||||
|
||||
class UpdateKnowledgeBaseEntryRequest(BaseModel):
|
||||
name: Optional[str] = Field(None, min_length=1, max_length=255)
|
||||
description: Optional[str] = None
|
||||
content: Optional[str] = Field(None, min_length=1)
|
||||
usage_context: Optional[str] = Field(None, pattern="^(always|on_request|contextual)$")
|
||||
is_active: Optional[bool] = None
|
||||
|
||||
db = DBConnection()
|
||||
|
||||
@router.get("/threads/{thread_id}", response_model=KnowledgeBaseListResponse)
|
||||
async def get_thread_knowledge_base(
|
||||
thread_id: str,
|
||||
include_inactive: bool = False,
|
||||
user_id: str = Depends(get_current_user_id_from_jwt)
|
||||
):
|
||||
if not await is_enabled("knowledge_base"):
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail="This feature is not available at the moment."
|
||||
)
|
||||
|
||||
"""Get all knowledge base entries for a thread"""
|
||||
try:
|
||||
client = await db.client
|
||||
|
||||
thread_result = await client.table('threads').select('*').eq('thread_id', thread_id).execute()
|
||||
if not thread_result.data:
|
||||
raise HTTPException(status_code=404, detail="Thread not found")
|
||||
|
||||
result = await client.rpc('get_thread_knowledge_base', {
|
||||
'p_thread_id': thread_id,
|
||||
'p_include_inactive': include_inactive
|
||||
}).execute()
|
||||
|
||||
entries = []
|
||||
total_tokens = 0
|
||||
|
||||
for entry_data in result.data or []:
|
||||
entry = KnowledgeBaseEntryResponse(
|
||||
entry_id=entry_data['entry_id'],
|
||||
name=entry_data['name'],
|
||||
description=entry_data['description'],
|
||||
content=entry_data['content'],
|
||||
usage_context=entry_data['usage_context'],
|
||||
is_active=entry_data['is_active'],
|
||||
content_tokens=entry_data.get('content_tokens'),
|
||||
created_at=entry_data['created_at'],
|
||||
updated_at=entry_data.get('updated_at', entry_data['created_at'])
|
||||
)
|
||||
entries.append(entry)
|
||||
total_tokens += entry_data.get('content_tokens', 0) or 0
|
||||
|
||||
return KnowledgeBaseListResponse(
|
||||
entries=entries,
|
||||
total_count=len(entries),
|
||||
total_tokens=total_tokens
|
||||
)
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting knowledge base for thread {thread_id}: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail="Failed to retrieve knowledge base")
|
||||
|
||||
@router.post("/threads/{thread_id}", response_model=KnowledgeBaseEntryResponse)
|
||||
async def create_knowledge_base_entry(
|
||||
thread_id: str,
|
||||
entry_data: CreateKnowledgeBaseEntryRequest,
|
||||
user_id: str = Depends(get_current_user_id_from_jwt)
|
||||
):
|
||||
if not await is_enabled("knowledge_base"):
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail="This feature is not available at the moment."
|
||||
)
|
||||
|
||||
"""Create a new knowledge base entry for a thread"""
|
||||
try:
|
||||
client = await db.client
|
||||
thread_result = await client.table('threads').select('account_id').eq('thread_id', thread_id).execute()
|
||||
if not thread_result.data:
|
||||
raise HTTPException(status_code=404, detail="Thread not found")
|
||||
|
||||
account_id = thread_result.data[0]['account_id']
|
||||
|
||||
insert_data = {
|
||||
'thread_id': thread_id,
|
||||
'account_id': account_id,
|
||||
'name': entry_data.name,
|
||||
'description': entry_data.description,
|
||||
'content': entry_data.content,
|
||||
'usage_context': entry_data.usage_context
|
||||
}
|
||||
|
||||
result = await client.table('knowledge_base_entries').insert(insert_data).execute()
|
||||
|
||||
if not result.data:
|
||||
raise HTTPException(status_code=500, detail="Failed to create knowledge base entry")
|
||||
|
||||
created_entry = result.data[0]
|
||||
|
||||
return KnowledgeBaseEntryResponse(
|
||||
entry_id=created_entry['entry_id'],
|
||||
name=created_entry['name'],
|
||||
description=created_entry['description'],
|
||||
content=created_entry['content'],
|
||||
usage_context=created_entry['usage_context'],
|
||||
is_active=created_entry['is_active'],
|
||||
content_tokens=created_entry.get('content_tokens'),
|
||||
created_at=created_entry['created_at'],
|
||||
updated_at=created_entry['updated_at']
|
||||
)
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error creating knowledge base entry for thread {thread_id}: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail="Failed to create knowledge base entry")
|
||||
|
||||
@router.put("/{entry_id}", response_model=KnowledgeBaseEntryResponse)
|
||||
async def update_knowledge_base_entry(
|
||||
entry_id: str,
|
||||
entry_data: UpdateKnowledgeBaseEntryRequest,
|
||||
user_id: str = Depends(get_current_user_id_from_jwt)
|
||||
):
|
||||
if not await is_enabled("knowledge_base"):
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail="This feature is not available at the moment."
|
||||
)
|
||||
|
||||
"""Update a knowledge base entry"""
|
||||
try:
|
||||
client = await db.client
|
||||
entry_result = await client.table('knowledge_base_entries').select('*').eq('entry_id', entry_id).execute()
|
||||
if not entry_result.data:
|
||||
raise HTTPException(status_code=404, detail="Knowledge base entry not found")
|
||||
|
||||
update_data = {}
|
||||
if entry_data.name is not None:
|
||||
update_data['name'] = entry_data.name
|
||||
if entry_data.description is not None:
|
||||
update_data['description'] = entry_data.description
|
||||
if entry_data.content is not None:
|
||||
update_data['content'] = entry_data.content
|
||||
if entry_data.usage_context is not None:
|
||||
update_data['usage_context'] = entry_data.usage_context
|
||||
if entry_data.is_active is not None:
|
||||
update_data['is_active'] = entry_data.is_active
|
||||
|
||||
if not update_data:
|
||||
raise HTTPException(status_code=400, detail="No fields to update")
|
||||
|
||||
result = await client.table('knowledge_base_entries').update(update_data).eq('entry_id', entry_id).execute()
|
||||
|
||||
if not result.data:
|
||||
raise HTTPException(status_code=500, detail="Failed to update knowledge base entry")
|
||||
|
||||
updated_entry = result.data[0]
|
||||
|
||||
return KnowledgeBaseEntryResponse(
|
||||
entry_id=updated_entry['entry_id'],
|
||||
name=updated_entry['name'],
|
||||
description=updated_entry['description'],
|
||||
content=updated_entry['content'],
|
||||
usage_context=updated_entry['usage_context'],
|
||||
is_active=updated_entry['is_active'],
|
||||
content_tokens=updated_entry.get('content_tokens'),
|
||||
created_at=updated_entry['created_at'],
|
||||
updated_at=updated_entry['updated_at']
|
||||
)
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error updating knowledge base entry {entry_id}: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail="Failed to update knowledge base entry")
|
||||
|
||||
@router.delete("/{entry_id}")
|
||||
async def delete_knowledge_base_entry(
|
||||
entry_id: str,
|
||||
user_id: str = Depends(get_current_user_id_from_jwt)
|
||||
):
|
||||
if not await is_enabled("knowledge_base"):
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail="This feature is not available at the moment."
|
||||
)
|
||||
|
||||
"""Delete a knowledge base entry"""
|
||||
try:
|
||||
client = await db.client
|
||||
entry_result = await client.table('knowledge_base_entries').select('entry_id').eq('entry_id', entry_id).execute()
|
||||
if not entry_result.data:
|
||||
raise HTTPException(status_code=404, detail="Knowledge base entry not found")
|
||||
|
||||
result = await client.table('knowledge_base_entries').delete().eq('entry_id', entry_id).execute()
|
||||
|
||||
return {"message": "Knowledge base entry deleted successfully"}
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error deleting knowledge base entry {entry_id}: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail="Failed to delete knowledge base entry")
|
||||
|
||||
@router.get("/{entry_id}", response_model=KnowledgeBaseEntryResponse)
|
||||
async def get_knowledge_base_entry(
|
||||
entry_id: str,
|
||||
user_id: str = Depends(get_current_user_id_from_jwt)
|
||||
):
|
||||
if not await is_enabled("knowledge_base"):
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail="This feature is not available at the moment."
|
||||
)
|
||||
"""Get a specific knowledge base entry"""
|
||||
try:
|
||||
client = await db.client
|
||||
result = await client.table('knowledge_base_entries').select('*').eq('entry_id', entry_id).execute()
|
||||
|
||||
if not result.data:
|
||||
raise HTTPException(status_code=404, detail="Knowledge base entry not found")
|
||||
|
||||
entry = result.data[0]
|
||||
|
||||
return KnowledgeBaseEntryResponse(
|
||||
entry_id=entry['entry_id'],
|
||||
name=entry['name'],
|
||||
description=entry['description'],
|
||||
content=entry['content'],
|
||||
usage_context=entry['usage_context'],
|
||||
is_active=entry['is_active'],
|
||||
content_tokens=entry.get('content_tokens'),
|
||||
created_at=entry['created_at'],
|
||||
updated_at=entry['updated_at']
|
||||
)
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting knowledge base entry {entry_id}: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail="Failed to retrieve knowledge base entry")
|
||||
|
||||
@router.get("/threads/{thread_id}/context")
|
||||
async def get_knowledge_base_context(
|
||||
thread_id: str,
|
||||
max_tokens: int = 4000,
|
||||
user_id: str = Depends(get_current_user_id_from_jwt)
|
||||
):
|
||||
if not await is_enabled("knowledge_base"):
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail="This feature is not available at the moment."
|
||||
)
|
||||
|
||||
"""Get knowledge base context for agent prompts"""
|
||||
try:
|
||||
client = await db.client
|
||||
thread_result = await client.table('threads').select('thread_id').eq('thread_id', thread_id).execute()
|
||||
if not thread_result.data:
|
||||
raise HTTPException(status_code=404, detail="Thread not found")
|
||||
|
||||
result = await client.rpc('get_knowledge_base_context', {
|
||||
'p_thread_id': thread_id,
|
||||
'p_max_tokens': max_tokens
|
||||
}).execute()
|
||||
|
||||
context = result.data if result.data else None
|
||||
|
||||
return {
|
||||
"context": context,
|
||||
"max_tokens": max_tokens,
|
||||
"thread_id": thread_id
|
||||
}
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting knowledge base context for thread {thread_id}: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail="Failed to retrieve knowledge base context")
|
|
@ -0,0 +1,215 @@
|
|||
BEGIN;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS knowledge_base_entries (
|
||||
entry_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
thread_id UUID NOT NULL REFERENCES threads(thread_id) ON DELETE CASCADE,
|
||||
account_id UUID NOT NULL REFERENCES basejump.accounts(id) ON DELETE CASCADE,
|
||||
|
||||
name VARCHAR(255) NOT NULL,
|
||||
description TEXT,
|
||||
|
||||
content TEXT NOT NULL,
|
||||
content_tokens INTEGER, -- Token count for content management
|
||||
|
||||
usage_context VARCHAR(100) DEFAULT 'always', -- 'always', 'on_request', 'contextual'
|
||||
|
||||
is_active BOOLEAN DEFAULT TRUE,
|
||||
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
last_accessed_at TIMESTAMPTZ,
|
||||
|
||||
CONSTRAINT kb_entries_valid_usage_context CHECK (
|
||||
usage_context IN ('always', 'on_request', 'contextual')
|
||||
),
|
||||
CONSTRAINT kb_entries_content_not_empty CHECK (
|
||||
content IS NOT NULL AND LENGTH(TRIM(content)) > 0
|
||||
)
|
||||
);
|
||||
|
||||
|
||||
CREATE TABLE IF NOT EXISTS knowledge_base_usage_log (
|
||||
log_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
entry_id UUID NOT NULL REFERENCES knowledge_base_entries(entry_id) ON DELETE CASCADE,
|
||||
thread_id UUID NOT NULL REFERENCES threads(thread_id) ON DELETE CASCADE,
|
||||
|
||||
usage_type VARCHAR(50) NOT NULL, -- 'context_injection', 'manual_reference'
|
||||
tokens_used INTEGER, -- How many tokens were used
|
||||
|
||||
-- Timestamps
|
||||
used_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_kb_entries_thread_id ON knowledge_base_entries(thread_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_kb_entries_account_id ON knowledge_base_entries(account_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_kb_entries_is_active ON knowledge_base_entries(is_active);
|
||||
CREATE INDEX IF NOT EXISTS idx_kb_entries_usage_context ON knowledge_base_entries(usage_context);
|
||||
CREATE INDEX IF NOT EXISTS idx_kb_entries_created_at ON knowledge_base_entries(created_at);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_kb_usage_entry_id ON knowledge_base_usage_log(entry_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_kb_usage_thread_id ON knowledge_base_usage_log(thread_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_kb_usage_used_at ON knowledge_base_usage_log(used_at);
|
||||
|
||||
ALTER TABLE knowledge_base_entries ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE knowledge_base_usage_log ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
CREATE POLICY kb_entries_user_access ON knowledge_base_entries
|
||||
FOR ALL
|
||||
USING (
|
||||
EXISTS (
|
||||
SELECT 1 FROM threads t
|
||||
LEFT JOIN projects p ON t.project_id = p.project_id
|
||||
WHERE t.thread_id = knowledge_base_entries.thread_id
|
||||
AND (
|
||||
basejump.has_role_on_account(t.account_id) = true OR
|
||||
basejump.has_role_on_account(p.account_id) = true OR
|
||||
basejump.has_role_on_account(knowledge_base_entries.account_id) = true
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
CREATE POLICY kb_usage_log_user_access ON knowledge_base_usage_log
|
||||
FOR ALL
|
||||
USING (
|
||||
EXISTS (
|
||||
SELECT 1 FROM threads t
|
||||
LEFT JOIN projects p ON t.project_id = p.project_id
|
||||
WHERE t.thread_id = knowledge_base_usage_log.thread_id
|
||||
AND (
|
||||
basejump.has_role_on_account(t.account_id) = true OR
|
||||
basejump.has_role_on_account(p.account_id) = true
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
CREATE OR REPLACE FUNCTION get_thread_knowledge_base(
|
||||
p_thread_id UUID,
|
||||
p_include_inactive BOOLEAN DEFAULT FALSE
|
||||
)
|
||||
RETURNS TABLE (
|
||||
entry_id UUID,
|
||||
name VARCHAR(255),
|
||||
description TEXT,
|
||||
content TEXT,
|
||||
usage_context VARCHAR(100),
|
||||
is_active BOOLEAN,
|
||||
created_at TIMESTAMPTZ
|
||||
)
|
||||
SECURITY DEFINER
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
BEGIN
|
||||
RETURN QUERY
|
||||
SELECT
|
||||
kbe.entry_id,
|
||||
kbe.name,
|
||||
kbe.description,
|
||||
kbe.content,
|
||||
kbe.usage_context,
|
||||
kbe.is_active,
|
||||
kbe.created_at
|
||||
FROM knowledge_base_entries kbe
|
||||
WHERE kbe.thread_id = p_thread_id
|
||||
AND (p_include_inactive OR kbe.is_active = TRUE)
|
||||
ORDER BY kbe.created_at DESC;
|
||||
END;
|
||||
$$;
|
||||
|
||||
CREATE OR REPLACE FUNCTION get_knowledge_base_context(
|
||||
p_thread_id UUID,
|
||||
p_max_tokens INTEGER DEFAULT 4000
|
||||
)
|
||||
RETURNS TEXT
|
||||
SECURITY DEFINER
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
DECLARE
|
||||
context_text TEXT := '';
|
||||
entry_record RECORD;
|
||||
current_tokens INTEGER := 0;
|
||||
estimated_tokens INTEGER;
|
||||
BEGIN
|
||||
FOR entry_record IN
|
||||
SELECT
|
||||
name,
|
||||
description,
|
||||
content,
|
||||
content_tokens
|
||||
FROM knowledge_base_entries
|
||||
WHERE thread_id = p_thread_id
|
||||
AND is_active = TRUE
|
||||
AND usage_context IN ('always', 'contextual')
|
||||
ORDER BY created_at DESC
|
||||
LOOP
|
||||
estimated_tokens := COALESCE(entry_record.content_tokens, LENGTH(entry_record.content) / 4);
|
||||
|
||||
IF current_tokens + estimated_tokens > p_max_tokens THEN
|
||||
EXIT;
|
||||
END IF;
|
||||
|
||||
context_text := context_text || E'\n\n## Knowledge Base: ' || entry_record.name || E'\n';
|
||||
|
||||
IF entry_record.description IS NOT NULL AND entry_record.description != '' THEN
|
||||
context_text := context_text || entry_record.description || E'\n\n';
|
||||
END IF;
|
||||
|
||||
context_text := context_text || entry_record.content;
|
||||
|
||||
current_tokens := current_tokens + estimated_tokens;
|
||||
|
||||
INSERT INTO knowledge_base_usage_log (entry_id, thread_id, usage_type, tokens_used)
|
||||
SELECT entry_id, p_thread_id, 'context_injection', estimated_tokens
|
||||
FROM knowledge_base_entries
|
||||
WHERE thread_id = p_thread_id AND name = entry_record.name
|
||||
LIMIT 1;
|
||||
END LOOP;
|
||||
|
||||
RETURN CASE
|
||||
WHEN context_text = '' THEN NULL
|
||||
ELSE E'# KNOWLEDGE BASE CONTEXT\n\nThe following information is from your knowledge base and should be used as reference when responding to the user:' || context_text
|
||||
END;
|
||||
END;
|
||||
$$;
|
||||
|
||||
CREATE OR REPLACE FUNCTION update_kb_entry_timestamp()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
NEW.updated_at = NOW();
|
||||
IF NEW.content != OLD.content THEN
|
||||
NEW.content_tokens = LENGTH(NEW.content) / 4;
|
||||
END IF;
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
CREATE TRIGGER trigger_kb_entries_updated_at
|
||||
BEFORE UPDATE ON knowledge_base_entries
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION update_kb_entry_timestamp();
|
||||
|
||||
CREATE OR REPLACE FUNCTION calculate_kb_entry_tokens()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
NEW.content_tokens = LENGTH(NEW.content) / 4;
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
CREATE TRIGGER trigger_kb_entries_calculate_tokens
|
||||
BEFORE INSERT ON knowledge_base_entries
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION calculate_kb_entry_tokens();
|
||||
|
||||
GRANT ALL PRIVILEGES ON TABLE knowledge_base_entries TO authenticated, service_role;
|
||||
GRANT ALL PRIVILEGES ON TABLE knowledge_base_usage_log TO authenticated, service_role;
|
||||
|
||||
GRANT EXECUTE ON FUNCTION get_thread_knowledge_base TO authenticated, service_role;
|
||||
GRANT EXECUTE ON FUNCTION get_knowledge_base_context TO authenticated, service_role;
|
||||
|
||||
COMMENT ON TABLE knowledge_base_entries IS 'Stores manual knowledge base entries for threads, similar to ChatGPT custom instructions';
|
||||
COMMENT ON TABLE knowledge_base_usage_log IS 'Logs when and how knowledge base entries are used';
|
||||
|
||||
COMMENT ON FUNCTION get_thread_knowledge_base IS 'Retrieves all knowledge base entries for a specific thread';
|
||||
COMMENT ON FUNCTION get_knowledge_base_context IS 'Generates knowledge base context text for agent prompts';
|
||||
|
||||
COMMIT;
|
|
@ -18,9 +18,5 @@ export default async function AgentsLayout({
|
|||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
const agentPlaygroundEnabled = await isFlagEnabled('custom_agents');
|
||||
if (!agentPlaygroundEnabled) {
|
||||
redirect('/dashboard');
|
||||
}
|
||||
return <>{children}</>;
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
'use client';
|
||||
|
||||
import React, { useState, useMemo } from 'react';
|
||||
import React, { useState, useMemo, useEffect } from 'react';
|
||||
import { Plus, AlertCircle, Loader2, File } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
|
||||
|
@ -16,8 +16,9 @@ import { Pagination } from './_components/pagination';
|
|||
import { useRouter } from 'next/navigation';
|
||||
import { DEFAULT_AGENTPRESS_TOOLS } from './_data/tools';
|
||||
import { AgentsParams } from '@/hooks/react-query/agents/utils';
|
||||
import { useFeatureFlags } from '@/lib/feature-flags';
|
||||
import { useFeatureFlag, useFeatureFlags } from '@/lib/feature-flags';
|
||||
import { generateRandomAvatar } from './_utils/_avatar-generator';
|
||||
import { Skeleton } from '@/components/ui/skeleton';
|
||||
|
||||
type ViewMode = 'grid' | 'list';
|
||||
type SortOption = 'name' | 'created_at' | 'updated_at' | 'tools_count';
|
||||
|
@ -31,7 +32,14 @@ interface FilterOptions {
|
|||
}
|
||||
|
||||
export default function AgentsPage() {
|
||||
const { enabled: customAgentsEnabled, loading: flagLoading } = useFeatureFlag("custom_agents");
|
||||
const router = useRouter();
|
||||
useEffect(() => {
|
||||
if (!flagLoading && !customAgentsEnabled) {
|
||||
router.replace("/dashboard");
|
||||
}
|
||||
}, [flagLoading, customAgentsEnabled, router]);
|
||||
|
||||
const [editDialogOpen, setEditDialogOpen] = useState(false);
|
||||
const [editingAgentId, setEditingAgentId] = useState<string | null>(null);
|
||||
const [viewMode, setViewMode] = useState<ViewMode>('grid');
|
||||
|
@ -182,6 +190,39 @@ export default function AgentsPage() {
|
|||
}
|
||||
};
|
||||
|
||||
if (flagLoading) {
|
||||
return (
|
||||
<div className="container max-w-7xl mx-auto px-4 py-8">
|
||||
<div className="flex justify-between items-center">
|
||||
<div className="space-y-1">
|
||||
<h1 className="text-2xl font-semibold tracking-tight text-foreground">
|
||||
Your Agents
|
||||
</h1>
|
||||
<p className="text-md text-muted-foreground max-w-2xl">
|
||||
Create and manage your AI agents with custom instructions and tools
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{Array.from({ length: 3 }).map((_, index) => (
|
||||
<div key={index} className="p-2 bg-neutral-100 dark:bg-sidebar rounded-2xl overflow-hidden group">
|
||||
<div className="h-24 flex items-center justify-center relative bg-gradient-to-br from-opacity-90 to-opacity-100">
|
||||
<Skeleton className="h-24 w-full rounded-xl" />
|
||||
</div>
|
||||
<div className="space-y-2 mt-4 mb-4">
|
||||
<Skeleton className="h-6 w-32 rounded" />
|
||||
<Skeleton className="h-4 w-24 rounded" />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (!customAgentsEnabled) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="container mx-auto max-w-7xl px-4 py-8">
|
||||
|
|
|
@ -17,9 +17,5 @@ export default async function MarketplaceLayout({
|
|||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
const marketplaceEnabled = await isFlagEnabled('agent_marketplace');
|
||||
if (!marketplaceEnabled) {
|
||||
redirect('/dashboard');
|
||||
}
|
||||
return <>{children}</>;
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
'use client';
|
||||
|
||||
import React, { useState, useMemo } from 'react';
|
||||
import React, { useState, useMemo, useEffect } from 'react';
|
||||
import { Search, Download, Star, Calendar, User, Tags, TrendingUp, Shield, CheckCircle, Loader2, Settings, Wrench, AlertTriangle, GitBranch, Plus } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
|
@ -20,6 +20,8 @@ import {
|
|||
} from '@/hooks/react-query/secure-mcp/use-secure-mcp';
|
||||
import { useCredentialProfilesForMcp } from '@/hooks/react-query/mcp/use-credential-profiles';
|
||||
import { createClient } from '@/lib/supabase/client';
|
||||
import { useFeatureFlag } from '@/lib/feature-flags';
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
type SortOption = 'newest' | 'popular' | 'most_downloaded' | 'name';
|
||||
|
||||
|
@ -516,6 +518,14 @@ const InstallDialog: React.FC<InstallDialogProps> = ({
|
|||
};
|
||||
|
||||
export default function MarketplacePage() {
|
||||
const { enabled: agentMarketplaceEnabled, loading: flagLoading } = useFeatureFlag("agent_marketplace");
|
||||
const router = useRouter();
|
||||
useEffect(() => {
|
||||
if (!flagLoading && !agentMarketplaceEnabled) {
|
||||
router.replace("/dashboard");
|
||||
}
|
||||
}, [flagLoading, agentMarketplaceEnabled, router]);
|
||||
|
||||
const [page, setPage] = useState(1);
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [selectedTags, setSelectedTags] = useState<string[]>([]);
|
||||
|
@ -654,6 +664,40 @@ export default function MarketplacePage() {
|
|||
return Array.from(tags);
|
||||
}, [marketplaceItems]);
|
||||
|
||||
if (flagLoading) {
|
||||
return (
|
||||
<div className="container max-w-7xl mx-auto px-4 py-8">
|
||||
<div className="flex justify-between items-center">
|
||||
<div className="space-y-2">
|
||||
<h1 className="text-2xl font-semibold tracking-tight text-foreground">
|
||||
Agent Marketplace
|
||||
</h1>
|
||||
<p className="text-md text-muted-foreground max-w-2xl">
|
||||
Discover and install secure AI agent templates created by the community
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{Array.from({ length: 3 }).map((_, index) => (
|
||||
<div key={index} className="p-2 bg-neutral-100 dark:bg-sidebar rounded-2xl overflow-hidden group">
|
||||
<div className="h-24 flex items-center justify-center relative bg-gradient-to-br from-opacity-90 to-opacity-100">
|
||||
<Skeleton className="h-24 w-full rounded-xl" />
|
||||
</div>
|
||||
<div className="space-y-2 mt-4 mb-4">
|
||||
<Skeleton className="h-6 w-32 rounded" />
|
||||
<Skeleton className="h-4 w-24 rounded" />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!agentMarketplaceEnabled) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="container mx-auto max-w-7xl px-4 py-8">
|
||||
<div className="space-y-8">
|
||||
|
|
|
@ -1,12 +1,11 @@
|
|||
'use client';
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import {
|
||||
Plus,
|
||||
Key,
|
||||
Shield,
|
||||
Trash2,
|
||||
CheckCircle,
|
||||
Loader2,
|
||||
AlertTriangle,
|
||||
Star,
|
||||
|
@ -35,6 +34,8 @@ import {
|
|||
} from '@/hooks/react-query/mcp/use-credential-profiles';
|
||||
import { EnhancedAddCredentialDialog } from './_components/enhanced-add-credential-dialog';
|
||||
import { Skeleton } from '@/components/ui/skeleton';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useFeatureFlag } from '@/lib/feature-flags';
|
||||
|
||||
interface CredentialProfileCardProps {
|
||||
profile: CredentialProfile;
|
||||
|
@ -219,6 +220,15 @@ const DeleteConfirmationDialog: React.FC<DeleteConfirmationDialogProps> = ({
|
|||
};
|
||||
|
||||
export default function CredentialsPage() {
|
||||
const { enabled: customAgentsEnabled, loading: flagLoading } = useFeatureFlag("custom_agents");
|
||||
const router = useRouter();
|
||||
useEffect(() => {
|
||||
if (!flagLoading && !customAgentsEnabled) {
|
||||
router.replace("/dashboard");
|
||||
}
|
||||
}, [flagLoading, customAgentsEnabled, router]);
|
||||
|
||||
|
||||
const [showAddDialog, setShowAddDialog] = useState(false);
|
||||
const [deletingId, setDeletingId] = useState<string | null>(null);
|
||||
const [settingDefaultId, setSettingDefaultId] = useState<string | null>(null);
|
||||
|
@ -279,6 +289,40 @@ export default function CredentialsPage() {
|
|||
return acc;
|
||||
}, {} as Record<string, { serverName: string; qualifiedName: string; profiles: CredentialProfile[] }>);
|
||||
|
||||
if (flagLoading) {
|
||||
return (
|
||||
<div className="h-screen max-w-7xl mx-auto p-6 space-y-6">
|
||||
<div className="flex justify-between items-center">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="p-2 rounded-lg bg-primary/10 border border-primary/20">
|
||||
<Shield className="h-5 w-5 text-primary" />
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-xl font-semibold text-foreground">MCP Credential Profiles</h1>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{Array.from({ length: 3 }).map((_, index) => (
|
||||
<div key={index} className="p-2 bg-neutral-100 dark:bg-sidebar rounded-2xl overflow-hidden group">
|
||||
<div className="h-24 flex items-center justify-center relative bg-gradient-to-br from-opacity-90 to-opacity-100">
|
||||
<Skeleton className="h-24 w-full rounded-xl" />
|
||||
</div>
|
||||
<div className="space-y-2 mt-4 mb-4">
|
||||
<Skeleton className="h-6 w-32 rounded" />
|
||||
<Skeleton className="h-4 w-24 rounded" />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!customAgentsEnabled) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="container mx-auto max-w-6xl px-6 py-6">
|
||||
|
|
|
@ -17,9 +17,5 @@ export default async function WorkflowsLayout({
|
|||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
const workflowsEnabled = await isFlagEnabled('workflows');
|
||||
if (!workflowsEnabled) {
|
||||
redirect('/dashboard');
|
||||
}
|
||||
return <>{children}</>;
|
||||
}
|
||||
|
|
|
@ -23,8 +23,17 @@ import { toast } from "sonner";
|
|||
import { useRouter } from "next/navigation";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { useSidebar } from "@/components/ui/sidebar";
|
||||
import { useFeatureFlag } from "@/lib/feature-flags";
|
||||
|
||||
export default function WorkflowsPage() {
|
||||
const { enabled: workflowsEnabled, loading: flagLoading } = useFeatureFlag("workflows");
|
||||
const router = useRouter();
|
||||
useEffect(() => {
|
||||
if (!flagLoading && !workflowsEnabled) {
|
||||
router.replace("/dashboard");
|
||||
}
|
||||
}, [flagLoading, workflowsEnabled, router]);
|
||||
|
||||
const [workflows, setWorkflows] = useState<Workflow[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
@ -36,7 +45,6 @@ export default function WorkflowsPage() {
|
|||
const [updatingWorkflows, setUpdatingWorkflows] = useState<Set<string>>(new Set());
|
||||
const [deletingWorkflows, setDeletingWorkflows] = useState<Set<string>>(new Set());
|
||||
const [togglingWorkflows, setTogglingWorkflows] = useState<Set<string>>(new Set());
|
||||
const router = useRouter();
|
||||
|
||||
const { state, setOpen, setOpenMobile } = useSidebar();
|
||||
const updateWorkflowStatusMutation = useUpdateWorkflowStatus();
|
||||
|
@ -64,7 +72,6 @@ export default function WorkflowsPage() {
|
|||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
loadData();
|
||||
}, []);
|
||||
|
||||
|
@ -73,7 +80,6 @@ export default function WorkflowsPage() {
|
|||
toast.error("No project selected");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setExecutingWorkflows(prev => new Set(prev).add(workflowId));
|
||||
const result = await executeWorkflow(workflowId);
|
||||
|
@ -273,10 +279,40 @@ export default function WorkflowsPage() {
|
|||
return "#8b5cf6";
|
||||
}
|
||||
};
|
||||
if (flagLoading) {
|
||||
return (
|
||||
<div className="container max-w-7xl mx-auto px-4 py-8">
|
||||
<div className="flex justify-between items-center">
|
||||
<div>
|
||||
<h1 className="text-xl font-bold">Workflows</h1>
|
||||
<p className="text-muted-foreground">
|
||||
Create and manage automated agent workflows
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{Array.from({ length: 3 }).map((_, index) => (
|
||||
<div key={index} className="p-2 bg-neutral-100 dark:bg-sidebar rounded-2xl overflow-hidden group">
|
||||
<div className="h-24 flex items-center justify-center relative bg-gradient-to-br from-opacity-90 to-opacity-100">
|
||||
<Skeleton className="h-24 w-full rounded-xl" />
|
||||
</div>
|
||||
<div className="space-y-2 mt-4 mb-4">
|
||||
<Skeleton className="h-6 w-32 rounded" />
|
||||
<Skeleton className="h-4 w-24 rounded" />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (!workflowsEnabled) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="h-screen max-w-7xl mx-auto p-6 space-y-6">
|
||||
<div className="max-w-7xl mx-auto p-6 space-y-6">
|
||||
<div className="flex justify-between items-center">
|
||||
<div>
|
||||
<h1 className="text-xl font-bold">Workflows</h1>
|
||||
|
|
|
@ -0,0 +1,519 @@
|
|||
'use client';
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Skeleton } from '@/components/ui/skeleton';
|
||||
import {
|
||||
Plus,
|
||||
Edit2,
|
||||
Trash2,
|
||||
Clock,
|
||||
MoreVertical,
|
||||
AlertCircle,
|
||||
FileText,
|
||||
Eye,
|
||||
EyeOff,
|
||||
Globe,
|
||||
Search,
|
||||
} from 'lucide-react';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu';
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from '@/components/ui/alert-dialog';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog';
|
||||
import {
|
||||
useKnowledgeBaseEntries,
|
||||
useCreateKnowledgeBaseEntry,
|
||||
useUpdateKnowledgeBaseEntry,
|
||||
useDeleteKnowledgeBaseEntry,
|
||||
} from '@/hooks/react-query/knowledge-base/use-knowledge-base-queries';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { CreateKnowledgeBaseEntryRequest, KnowledgeBaseEntry, UpdateKnowledgeBaseEntryRequest } from '@/hooks/react-query/knowledge-base/types';
|
||||
|
||||
interface KnowledgeBaseManagerProps {
|
||||
threadId: string;
|
||||
}
|
||||
|
||||
interface EditDialogData {
|
||||
entry?: KnowledgeBaseEntry;
|
||||
isOpen: boolean;
|
||||
}
|
||||
|
||||
const USAGE_CONTEXT_OPTIONS = [
|
||||
{
|
||||
value: 'always',
|
||||
label: 'Always Active',
|
||||
icon: Globe,
|
||||
color: 'bg-green-50 text-green-700 border-green-200 dark:bg-green-900/20 dark:text-green-400 dark:border-green-800'
|
||||
},
|
||||
// {
|
||||
// value: 'contextual',
|
||||
// label: 'Smart Context',
|
||||
// description: 'Included when contextually relevant',
|
||||
// icon: Target,
|
||||
// color: 'bg-blue-50 text-blue-700 border-blue-200 dark:bg-blue-900/20 dark:text-blue-400 dark:border-blue-800'
|
||||
// },
|
||||
// {
|
||||
// value: 'on_request',
|
||||
// label: 'On Demand',
|
||||
// description: 'Only when explicitly requested',
|
||||
// icon: Zap,
|
||||
// color: 'bg-amber-50 text-amber-700 border-amber-200 dark:bg-amber-900/20 dark:text-amber-400 dark:border-amber-800'
|
||||
// },
|
||||
] as const;
|
||||
|
||||
const KnowledgeBaseSkeleton = () => (
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="relative w-full">
|
||||
<Skeleton className="h-10 w-full" />
|
||||
</div>
|
||||
<Skeleton className="h-10 w-32 ml-4" />
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
{[1, 2, 3].map((i) => (
|
||||
<div key={i} className="border rounded-lg p-4">
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="flex-1 min-w-0 space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<Skeleton className="h-4 w-4" />
|
||||
<Skeleton className="h-5 w-48" />
|
||||
<Skeleton className="h-5 w-20" />
|
||||
</div>
|
||||
<Skeleton className="h-4 w-64" />
|
||||
<div className="space-y-1">
|
||||
<Skeleton className="h-4 w-full" />
|
||||
<Skeleton className="h-4 w-3/4" />
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<Skeleton className="h-5 w-24" />
|
||||
<Skeleton className="h-4 w-20" />
|
||||
</div>
|
||||
<Skeleton className="h-4 w-16" />
|
||||
</div>
|
||||
</div>
|
||||
<Skeleton className="h-8 w-8" />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
export const KnowledgeBaseManager = ({ threadId }: KnowledgeBaseManagerProps) => {
|
||||
const [editDialog, setEditDialog] = useState<EditDialogData>({ isOpen: false });
|
||||
const [deleteEntryId, setDeleteEntryId] = useState<string | null>(null);
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [formData, setFormData] = useState<CreateKnowledgeBaseEntryRequest>({
|
||||
name: '',
|
||||
description: '',
|
||||
content: '',
|
||||
usage_context: 'always',
|
||||
});
|
||||
|
||||
const { data: knowledgeBase, isLoading, error } = useKnowledgeBaseEntries(threadId);
|
||||
const createMutation = useCreateKnowledgeBaseEntry();
|
||||
const updateMutation = useUpdateKnowledgeBaseEntry();
|
||||
const deleteMutation = useDeleteKnowledgeBaseEntry();
|
||||
|
||||
const handleOpenCreateDialog = () => {
|
||||
setFormData({
|
||||
name: '',
|
||||
description: '',
|
||||
content: '',
|
||||
usage_context: 'always',
|
||||
});
|
||||
setEditDialog({ isOpen: true });
|
||||
};
|
||||
|
||||
const handleOpenEditDialog = (entry: KnowledgeBaseEntry) => {
|
||||
setFormData({
|
||||
name: entry.name,
|
||||
description: entry.description || '',
|
||||
content: entry.content,
|
||||
usage_context: entry.usage_context,
|
||||
});
|
||||
setEditDialog({ entry, isOpen: true });
|
||||
};
|
||||
|
||||
const handleCloseDialog = () => {
|
||||
setEditDialog({ isOpen: false });
|
||||
setFormData({
|
||||
name: '',
|
||||
description: '',
|
||||
content: '',
|
||||
usage_context: 'always',
|
||||
});
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (!formData.name.trim() || !formData.content.trim()) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (editDialog.entry) {
|
||||
const updateData: UpdateKnowledgeBaseEntryRequest = {
|
||||
name: formData.name !== editDialog.entry.name ? formData.name : undefined,
|
||||
description: formData.description !== editDialog.entry.description ? formData.description : undefined,
|
||||
content: formData.content !== editDialog.entry.content ? formData.content : undefined,
|
||||
usage_context: formData.usage_context !== editDialog.entry.usage_context ? formData.usage_context : undefined,
|
||||
};
|
||||
const hasChanges = Object.values(updateData).some(value => value !== undefined);
|
||||
if (hasChanges) {
|
||||
await updateMutation.mutateAsync({ entryId: editDialog.entry.entry_id, data: updateData });
|
||||
}
|
||||
} else {
|
||||
await createMutation.mutateAsync({ threadId, data: formData });
|
||||
}
|
||||
|
||||
handleCloseDialog();
|
||||
} catch (error) {
|
||||
console.error('Error saving knowledge base entry:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async (entryId: string) => {
|
||||
try {
|
||||
await deleteMutation.mutateAsync(entryId);
|
||||
setDeleteEntryId(null);
|
||||
} catch (error) {
|
||||
console.error('Error deleting knowledge base entry:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleToggleActive = async (entry: KnowledgeBaseEntry) => {
|
||||
try {
|
||||
await updateMutation.mutateAsync({
|
||||
entryId: entry.entry_id,
|
||||
data: { is_active: !entry.is_active }
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error toggling entry status:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const getUsageContextConfig = (context: string) => {
|
||||
return USAGE_CONTEXT_OPTIONS.find(option => option.value === context) || USAGE_CONTEXT_OPTIONS[0];
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return <KnowledgeBaseSkeleton />;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="flex items-center justify-center py-12">
|
||||
<div className="text-center">
|
||||
<AlertCircle className="h-8 w-8 text-red-500 mx-auto mb-4" />
|
||||
<p className="text-sm text-red-600 dark:text-red-400">Failed to load knowledge base</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const entries = knowledgeBase?.entries || [];
|
||||
const filteredEntries = entries.filter(entry =>
|
||||
entry.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
entry.content.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
(entry.description && entry.description.toLowerCase().includes(searchQuery.toLowerCase()))
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{entries.length > 0 && (
|
||||
<>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="relative w-full">
|
||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
placeholder="Search knowledge entries..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="pl-9"
|
||||
/>
|
||||
</div>
|
||||
<Button onClick={handleOpenCreateDialog} className="gap-2 ml-4">
|
||||
<Plus className="h-4 w-4" />
|
||||
Add Knowledge
|
||||
</Button>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
{filteredEntries.length === 0 ? (
|
||||
<div className="text-center py-8">
|
||||
<Search className="h-8 w-8 mx-auto text-muted-foreground/50 mb-2" />
|
||||
<p className="text-sm text-muted-foreground">No entries match your search</p>
|
||||
</div>
|
||||
) : (
|
||||
filteredEntries.map((entry) => {
|
||||
const contextConfig = getUsageContextConfig(entry.usage_context);
|
||||
const ContextIcon = contextConfig.icon;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={entry.entry_id}
|
||||
className={cn(
|
||||
"group border rounded-lg p-4 transition-all hover:shadow-sm",
|
||||
entry.is_active
|
||||
? "border-border bg-card hover:border-border/80"
|
||||
: "border-border/50 bg-muted/30 opacity-70"
|
||||
)}
|
||||
>
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="flex-1 min-w-0 space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<FileText className="h-4 w-4 text-muted-foreground flex-shrink-0" />
|
||||
<h3 className="font-medium truncate">{entry.name}</h3>
|
||||
{!entry.is_active && (
|
||||
<Badge variant="secondary" className="text-xs">
|
||||
<EyeOff className="h-3 w-3 mr-1" />
|
||||
Disabled
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
{entry.description && (
|
||||
<p className="text-sm text-muted-foreground line-clamp-1">
|
||||
{entry.description}
|
||||
</p>
|
||||
)}
|
||||
<p className="text-sm text-foreground/80 line-clamp-2 leading-relaxed">
|
||||
{entry.content}
|
||||
</p>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<Badge variant="outline" className={cn("text-xs gap-1", contextConfig.color)}>
|
||||
<ContextIcon className="h-3 w-3" />
|
||||
{contextConfig.label}
|
||||
</Badge>
|
||||
<span className="text-xs text-muted-foreground flex items-center gap-1">
|
||||
<Clock className="h-3 w-3" />
|
||||
{new Date(entry.created_at).toLocaleDateString()}
|
||||
</span>
|
||||
</div>
|
||||
{entry.content_tokens && (
|
||||
<span className="text-xs text-muted-foreground">
|
||||
~{entry.content_tokens.toLocaleString()} tokens
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-8 w-8 p-0 opacity-0 group-hover:opacity-100 transition-opacity"
|
||||
>
|
||||
<MoreVertical className="h-4 w-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-36">
|
||||
<DropdownMenuItem onClick={() => handleOpenEditDialog(entry)}>
|
||||
<Edit2 className="h-4 w-4" />
|
||||
Edit
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => handleToggleActive(entry)}>
|
||||
{entry.is_active ? (
|
||||
<>
|
||||
<EyeOff className="h-4 w-4" />
|
||||
Disable
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Eye className="h-4 w-4" />
|
||||
Enable
|
||||
</>
|
||||
)}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem
|
||||
onClick={() => setDeleteEntryId(entry.entry_id)}
|
||||
className="text-destructive focus:bg-destructive/10 focus:text-destructive"
|
||||
>
|
||||
<Trash2 className="h-4 w-4 text-destructive" />
|
||||
Delete
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{entries.length === 0 && (
|
||||
<div className="text-center py-12">
|
||||
<div className="mx-auto w-24 h-24 bg-muted/50 rounded-full flex items-center justify-center mb-4">
|
||||
<FileText className="h-8 w-8 text-muted-foreground" />
|
||||
</div>
|
||||
<h3 className="text-lg font-medium mb-2">No Knowledge Entries</h3>
|
||||
<p className="text-muted-foreground mb-6 max-w-md mx-auto">
|
||||
Add knowledge entries to provide your agent with context, guidelines, and information it should always remember.
|
||||
</p>
|
||||
<Button onClick={handleOpenCreateDialog} className="gap-2">
|
||||
<Plus className="h-4 w-4" />
|
||||
Create Your First Entry
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Dialog open={editDialog.isOpen} onOpenChange={handleCloseDialog}>
|
||||
<DialogContent className="max-w-2xl max-h-[85vh] overflow-hidden flex flex-col">
|
||||
<DialogHeader className="flex-shrink-0">
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<FileText className="h-5 w-5" />
|
||||
{editDialog.entry ? 'Edit Knowledge Entry' : 'Add Knowledge Entry'}
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
<form onSubmit={handleSubmit} className="space-y-6 p-1">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="name" className="text-sm font-medium">Name *</Label>
|
||||
<Input
|
||||
id="name"
|
||||
value={formData.name}
|
||||
onChange={(e) => setFormData(prev => ({ ...prev, name: e.target.value }))}
|
||||
placeholder="e.g., Company Guidelines, API Documentation"
|
||||
required
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="usage_context" className="text-sm font-medium">Usage Context</Label>
|
||||
<Select
|
||||
value={formData.usage_context}
|
||||
onValueChange={(value: 'always' | 'on_request' | 'contextual') =>
|
||||
setFormData(prev => ({ ...prev, usage_context: value }))
|
||||
}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{USAGE_CONTEXT_OPTIONS.map((option) => {
|
||||
const Icon = option.icon;
|
||||
return (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
<div className="flex items-center gap-2">
|
||||
<Icon className="h-4 w-4" />
|
||||
<div>
|
||||
<div className="font-medium">{option.label}</div>
|
||||
</div>
|
||||
</div>
|
||||
</SelectItem>
|
||||
);
|
||||
})}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="description" className="text-sm font-medium">Description</Label>
|
||||
<Input
|
||||
id="description"
|
||||
value={formData.description}
|
||||
onChange={(e) => setFormData(prev => ({ ...prev, description: e.target.value }))}
|
||||
placeholder="Brief description of this knowledge (optional)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="content" className="text-sm font-medium">Content *</Label>
|
||||
<Textarea
|
||||
id="content"
|
||||
value={formData.content}
|
||||
onChange={(e) => setFormData(prev => ({ ...prev, content: e.target.value }))}
|
||||
placeholder="Enter the knowledge content that your agent should know..."
|
||||
className="min-h-[200px] resize-y"
|
||||
required
|
||||
/>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
Approximately {Math.ceil(formData.content.length / 4).toLocaleString()} tokens
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end gap-3 pt-4 border-t">
|
||||
<Button type="button" variant="outline" onClick={handleCloseDialog}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={!formData.name.trim() || !formData.content.trim() ||
|
||||
createMutation.isPending || updateMutation.isPending}
|
||||
className="gap-2"
|
||||
>
|
||||
{createMutation.isPending || updateMutation.isPending ? (
|
||||
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-current" />
|
||||
) : (
|
||||
<Plus className="h-4 w-4" />
|
||||
)}
|
||||
{editDialog.entry ? 'Save Changes' : 'Add Knowledge'}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
<AlertDialog open={!!deleteEntryId} onOpenChange={() => setDeleteEntryId(null)}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle className="flex items-center gap-2">
|
||||
<AlertCircle className="h-5 w-5 text-destructive" />
|
||||
Delete Knowledge Entry
|
||||
</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
This will permanently delete this knowledge entry. Your agent will no longer have access to this information.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={() => deleteEntryId && handleDelete(deleteEntryId)}
|
||||
className="bg-destructive hover:bg-destructive/90"
|
||||
disabled={deleteMutation.isPending}
|
||||
>
|
||||
{deleteMutation.isPending ? (
|
||||
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-current mr-2" />
|
||||
) : (
|
||||
<Trash2 className="h-4 w-4" />
|
||||
)}
|
||||
Delete Entry
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
'use client';
|
||||
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { FolderOpen, Link, PanelRightOpen, Check, X, Menu, Share2 } from "lucide-react"
|
||||
import { FolderOpen, Link, PanelRightOpen, Check, X, Menu, Share2, Book } from "lucide-react"
|
||||
import { usePathname } from "next/navigation"
|
||||
import { toast } from "sonner"
|
||||
import {
|
||||
|
@ -21,6 +21,14 @@ import { ShareModal } from "@/components/sidebar/share-modal"
|
|||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { projectKeys } from "@/hooks/react-query/sidebar/keys";
|
||||
import { threadKeys } from "@/hooks/react-query/threads/keys";
|
||||
import { KnowledgeBaseManager } from "@/components/thread/knowledge-base/knowledge-base-manager";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { useFeatureFlags } from "@/lib/feature-flags";
|
||||
|
||||
interface ThreadSiteHeaderProps {
|
||||
threadId: string;
|
||||
|
@ -48,7 +56,10 @@ export function SiteHeader({
|
|||
const [editName, setEditName] = useState(projectName)
|
||||
const inputRef = useRef<HTMLInputElement>(null)
|
||||
const [showShareModal, setShowShareModal] = useState(false);
|
||||
const [showKnowledgeBase, setShowKnowledgeBase] = useState(false);
|
||||
const queryClient = useQueryClient();
|
||||
const { flags, loading: flagsLoading } = useFeatureFlags(['knowledge_base']);
|
||||
const knowledgeBaseEnabled = flags.knowledge_base;
|
||||
|
||||
const isMobile = useIsMobile() || isMobileView
|
||||
const { setOpenMobile } = useSidebar()
|
||||
|
@ -58,6 +69,10 @@ export function SiteHeader({
|
|||
setShowShareModal(true)
|
||||
}
|
||||
|
||||
const openKnowledgeBase = () => {
|
||||
setShowKnowledgeBase(true)
|
||||
}
|
||||
|
||||
const startEditing = () => {
|
||||
setEditName(projectName);
|
||||
setIsEditing(true);
|
||||
|
@ -216,6 +231,23 @@ export function SiteHeader({
|
|||
</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
{knowledgeBaseEnabled && (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={openKnowledgeBase}
|
||||
className="h-9 w-9 cursor-pointer"
|
||||
>
|
||||
<Book className="h-4 w-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Knowledge Base</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
|
@ -257,6 +289,22 @@ export function SiteHeader({
|
|||
threadId={threadId}
|
||||
projectId={projectId}
|
||||
/>
|
||||
|
||||
<Dialog open={showKnowledgeBase} onOpenChange={setShowKnowledgeBase}>
|
||||
<DialogContent className="max-w-2xl max-h-[90vh] overflow-hidden p-0">
|
||||
<div className="flex flex-col h-full">
|
||||
<DialogHeader className="px-6 py-4">
|
||||
<DialogTitle className="flex items-center gap-2 text-lg">
|
||||
<Book className="h-5 w-5" />
|
||||
Knowledge Base
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="flex-1 overflow-y-auto p-6">
|
||||
<KnowledgeBaseManager threadId={threadId} />
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
)
|
||||
}
|
|
@ -21,4 +21,6 @@ export * from './dashboard/use-initiate-agent';
|
|||
|
||||
export * from './usage/use-health';
|
||||
|
||||
export * from './scheduling/use-scheduling';
|
||||
export * from './scheduling/use-scheduling';
|
||||
|
||||
export * from './knowledge-base/use-knowledge-base-queries';
|
|
@ -0,0 +1,7 @@
|
|||
export const knowledgeBaseKeys = {
|
||||
all: ['knowledge-base'] as const,
|
||||
threads: () => [...knowledgeBaseKeys.all, 'threads'] as const,
|
||||
thread: (threadId: string) => [...knowledgeBaseKeys.threads(), threadId] as const,
|
||||
entry: (entryId: string) => [...knowledgeBaseKeys.all, 'entry', entryId] as const,
|
||||
context: (threadId: string) => [...knowledgeBaseKeys.all, 'context', threadId] as const,
|
||||
};
|
|
@ -0,0 +1,32 @@
|
|||
export interface KnowledgeBaseEntry {
|
||||
entry_id: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
content: string;
|
||||
usage_context: 'always' | 'on_request' | 'contextual';
|
||||
is_active: boolean;
|
||||
content_tokens?: number;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface KnowledgeBaseListResponse {
|
||||
entries: KnowledgeBaseEntry[];
|
||||
total_count: number;
|
||||
total_tokens: number;
|
||||
}
|
||||
|
||||
export interface CreateKnowledgeBaseEntryRequest {
|
||||
name: string;
|
||||
description?: string;
|
||||
content: string;
|
||||
usage_context?: 'always' | 'on_request' | 'contextual';
|
||||
}
|
||||
|
||||
export interface UpdateKnowledgeBaseEntryRequest {
|
||||
name?: string;
|
||||
description?: string;
|
||||
content?: string;
|
||||
usage_context?: 'always' | 'on_request' | 'contextual';
|
||||
is_active?: boolean;
|
||||
}
|
|
@ -0,0 +1,185 @@
|
|||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { toast } from 'sonner';
|
||||
import { createClient } from '@/lib/supabase/client';
|
||||
import { knowledgeBaseKeys } from './keys';
|
||||
import { CreateKnowledgeBaseEntryRequest, KnowledgeBaseEntry, KnowledgeBaseListResponse, UpdateKnowledgeBaseEntryRequest } from './types';
|
||||
|
||||
const API_URL = process.env.NEXT_PUBLIC_BACKEND_URL || '';
|
||||
|
||||
const useAuthHeaders = () => {
|
||||
const getHeaders = async () => {
|
||||
const supabase = createClient();
|
||||
const { data: { session } } = await supabase.auth.getSession();
|
||||
|
||||
if (!session?.access_token) {
|
||||
throw new Error('No access token available');
|
||||
}
|
||||
|
||||
return {
|
||||
'Authorization': `Bearer ${session.access_token}`,
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
};
|
||||
|
||||
return { getHeaders };
|
||||
};
|
||||
|
||||
export function useKnowledgeBaseEntries(threadId: string, includeInactive = false) {
|
||||
const { getHeaders } = useAuthHeaders();
|
||||
|
||||
return useQuery({
|
||||
queryKey: knowledgeBaseKeys.thread(threadId),
|
||||
queryFn: async (): Promise<KnowledgeBaseListResponse> => {
|
||||
const headers = await getHeaders();
|
||||
const url = new URL(`${API_URL}/knowledge-base/threads/${threadId}`);
|
||||
url.searchParams.set('include_inactive', includeInactive.toString());
|
||||
|
||||
const response = await fetch(url.toString(), { headers });
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.text();
|
||||
throw new Error(error || 'Failed to fetch knowledge base entries');
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
},
|
||||
enabled: !!threadId,
|
||||
});
|
||||
}
|
||||
|
||||
export function useKnowledgeBaseEntry(entryId: string) {
|
||||
const { getHeaders } = useAuthHeaders();
|
||||
|
||||
return useQuery({
|
||||
queryKey: knowledgeBaseKeys.entry(entryId),
|
||||
queryFn: async (): Promise<KnowledgeBaseEntry> => {
|
||||
const headers = await getHeaders();
|
||||
const response = await fetch(`${API_URL}/knowledge-base/${entryId}`, { headers });
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.text();
|
||||
throw new Error(error || 'Failed to fetch knowledge base entry');
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
},
|
||||
enabled: !!entryId,
|
||||
});
|
||||
}
|
||||
|
||||
export function useKnowledgeBaseContext(threadId: string, maxTokens = 4000) {
|
||||
const { getHeaders } = useAuthHeaders();
|
||||
|
||||
return useQuery({
|
||||
queryKey: knowledgeBaseKeys.context(threadId),
|
||||
queryFn: async () => {
|
||||
const headers = await getHeaders();
|
||||
const url = new URL(`${API_URL}/knowledge-base/threads/${threadId}/context`);
|
||||
url.searchParams.set('max_tokens', maxTokens.toString());
|
||||
|
||||
const response = await fetch(url.toString(), { headers });
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.text();
|
||||
throw new Error(error || 'Failed to fetch knowledge base context');
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
},
|
||||
enabled: !!threadId,
|
||||
});
|
||||
}
|
||||
|
||||
export function useCreateKnowledgeBaseEntry() {
|
||||
const queryClient = useQueryClient();
|
||||
const { getHeaders } = useAuthHeaders();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async ({ threadId, data }: { threadId: string; data: CreateKnowledgeBaseEntryRequest }): Promise<KnowledgeBaseEntry> => {
|
||||
const headers = await getHeaders();
|
||||
const response = await fetch(`${API_URL}/knowledge-base/threads/${threadId}`, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.text();
|
||||
throw new Error(error || 'Failed to create knowledge base entry');
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
},
|
||||
onSuccess: (_, { threadId }) => {
|
||||
queryClient.invalidateQueries({ queryKey: knowledgeBaseKeys.thread(threadId) });
|
||||
queryClient.invalidateQueries({ queryKey: knowledgeBaseKeys.context(threadId) });
|
||||
toast.success('Knowledge base entry created successfully');
|
||||
},
|
||||
onError: (error) => {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
toast.error(`Failed to create knowledge base entry: ${message}`);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useUpdateKnowledgeBaseEntry() {
|
||||
const queryClient = useQueryClient();
|
||||
const { getHeaders } = useAuthHeaders();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async ({ entryId, data }: { entryId: string; data: UpdateKnowledgeBaseEntryRequest }): Promise<KnowledgeBaseEntry> => {
|
||||
const headers = await getHeaders();
|
||||
const response = await fetch(`${API_URL}/knowledge-base/${entryId}`, {
|
||||
method: 'PUT',
|
||||
headers,
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.text();
|
||||
throw new Error(error || 'Failed to update knowledge base entry');
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
},
|
||||
onSuccess: (updatedEntry) => {
|
||||
queryClient.setQueryData(knowledgeBaseKeys.entry(updatedEntry.entry_id), updatedEntry);
|
||||
queryClient.invalidateQueries({ queryKey: knowledgeBaseKeys.threads() });
|
||||
queryClient.invalidateQueries({ queryKey: knowledgeBaseKeys.all });
|
||||
toast.success('Knowledge base entry updated successfully');
|
||||
},
|
||||
onError: (error) => {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
toast.error(`Failed to update knowledge base entry: ${message}`);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useDeleteKnowledgeBaseEntry() {
|
||||
const queryClient = useQueryClient();
|
||||
const { getHeaders } = useAuthHeaders();
|
||||
return useMutation({
|
||||
mutationFn: async (entryId: string): Promise<void> => {
|
||||
const headers = await getHeaders();
|
||||
const response = await fetch(`${API_URL}/knowledge-base/${entryId}`, {
|
||||
method: 'DELETE',
|
||||
headers,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.text();
|
||||
throw new Error(error || 'Failed to delete knowledge base entry');
|
||||
}
|
||||
},
|
||||
onSuccess: (_, entryId) => {
|
||||
queryClient.removeQueries({ queryKey: knowledgeBaseKeys.entry(entryId) });
|
||||
queryClient.invalidateQueries({ queryKey: knowledgeBaseKeys.threads() });
|
||||
queryClient.invalidateQueries({ queryKey: knowledgeBaseKeys.all });
|
||||
toast.success('Knowledge base entry deleted successfully');
|
||||
},
|
||||
onError: (error) => {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
toast.error(`Failed to delete knowledge base entry: ${message}`);
|
||||
},
|
||||
});
|
||||
}
|
|
@ -31,7 +31,7 @@ export class FeatureFlagManager {
|
|||
}
|
||||
return FeatureFlagManager.instance;
|
||||
}
|
||||
|
||||
|
||||
async isEnabled(flagName: string): Promise<boolean> {
|
||||
try {
|
||||
const cached = flagCache.get(flagName);
|
||||
|
|
Loading…
Reference in New Issue