mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-07-02 22:01:05 +02:00
feat: migrated to surfsense deep agent
This commit is contained in:
parent
b14283e300
commit
4a0c3e368a
90 changed files with 5337 additions and 6029 deletions
|
|
@ -0,0 +1,114 @@
|
|||
"""Add NewLLMConfig table for configurable LLM + prompt settings
|
||||
|
||||
Revision ID: 51
|
||||
Revises: 50
|
||||
"""
|
||||
|
||||
from collections.abc import Sequence
|
||||
|
||||
from alembic import op
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = "51"
|
||||
down_revision: str | None = "50"
|
||||
branch_labels: str | Sequence[str] | None = None
|
||||
depends_on: str | Sequence[str] | None = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
"""
|
||||
Add the new_llm_configs table that combines LLM model settings with prompt configuration.
|
||||
|
||||
This table includes:
|
||||
- LLM model configuration (provider, model_name, api_key, etc.)
|
||||
- Configurable system instructions
|
||||
- Citation toggle
|
||||
"""
|
||||
# Create new_llm_configs table only if it doesn't already exist
|
||||
op.execute(
|
||||
"""
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT FROM information_schema.tables
|
||||
WHERE table_name = 'new_llm_configs'
|
||||
) THEN
|
||||
CREATE TABLE new_llm_configs (
|
||||
id SERIAL PRIMARY KEY,
|
||||
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
|
||||
|
||||
-- Basic info
|
||||
name VARCHAR(100) NOT NULL,
|
||||
description VARCHAR(500),
|
||||
|
||||
-- LLM Model Configuration (same as llm_configs, excluding language)
|
||||
provider litellmprovider NOT NULL,
|
||||
custom_provider VARCHAR(100),
|
||||
model_name VARCHAR(100) NOT NULL,
|
||||
api_key TEXT NOT NULL,
|
||||
api_base VARCHAR(500),
|
||||
litellm_params JSONB DEFAULT '{}',
|
||||
|
||||
-- Prompt Configuration
|
||||
system_instructions TEXT NOT NULL DEFAULT '',
|
||||
use_default_system_instructions BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
citations_enabled BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
|
||||
-- Default flag
|
||||
is_default BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
|
||||
-- Foreign key to search space
|
||||
search_space_id INTEGER NOT NULL REFERENCES searchspaces(id) ON DELETE CASCADE
|
||||
);
|
||||
END IF;
|
||||
END$$;
|
||||
"""
|
||||
)
|
||||
|
||||
# Create indexes if they don't exist
|
||||
op.execute(
|
||||
"""
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM pg_indexes
|
||||
WHERE tablename = 'new_llm_configs' AND indexname = 'ix_new_llm_configs_id'
|
||||
) THEN
|
||||
CREATE INDEX ix_new_llm_configs_id ON new_llm_configs(id);
|
||||
END IF;
|
||||
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM pg_indexes
|
||||
WHERE tablename = 'new_llm_configs' AND indexname = 'ix_new_llm_configs_created_at'
|
||||
) THEN
|
||||
CREATE INDEX ix_new_llm_configs_created_at ON new_llm_configs(created_at);
|
||||
END IF;
|
||||
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM pg_indexes
|
||||
WHERE tablename = 'new_llm_configs' AND indexname = 'ix_new_llm_configs_name'
|
||||
) THEN
|
||||
CREATE INDEX ix_new_llm_configs_name ON new_llm_configs(name);
|
||||
END IF;
|
||||
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM pg_indexes
|
||||
WHERE tablename = 'new_llm_configs' AND indexname = 'ix_new_llm_configs_search_space_id'
|
||||
) THEN
|
||||
CREATE INDEX ix_new_llm_configs_search_space_id ON new_llm_configs(search_space_id);
|
||||
END IF;
|
||||
END$$;
|
||||
"""
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
"""Remove the new_llm_configs table."""
|
||||
# Drop indexes
|
||||
op.execute("DROP INDEX IF EXISTS ix_new_llm_configs_search_space_id")
|
||||
op.execute("DROP INDEX IF EXISTS ix_new_llm_configs_name")
|
||||
op.execute("DROP INDEX IF EXISTS ix_new_llm_configs_created_at")
|
||||
op.execute("DROP INDEX IF EXISTS ix_new_llm_configs_id")
|
||||
|
||||
# Drop table
|
||||
op.execute("DROP TABLE IF EXISTS new_llm_configs")
|
||||
|
|
@ -0,0 +1,130 @@
|
|||
"""Rename LLM preference columns in searchspaces table
|
||||
|
||||
Revision ID: 52
|
||||
Revises: 51
|
||||
Create Date: 2024-12-22
|
||||
|
||||
This migration renames the LLM preference columns:
|
||||
- fast_llm_id -> agent_llm_id
|
||||
- long_context_llm_id -> document_summary_llm_id
|
||||
- strategic_llm_id is removed (data migrated to document_summary_llm_id)
|
||||
"""
|
||||
|
||||
from alembic import op
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "52"
|
||||
down_revision = "51"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# First, migrate any strategic_llm_id values to document_summary_llm_id
|
||||
# (only if document_summary_llm_id/long_context_llm_id is NULL)
|
||||
# Use IF EXISTS check to handle case where column might not exist
|
||||
op.execute(
|
||||
"""
|
||||
DO $$
|
||||
BEGIN
|
||||
IF EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_name = 'searchspaces' AND column_name = 'strategic_llm_id'
|
||||
) THEN
|
||||
UPDATE searchspaces
|
||||
SET long_context_llm_id = strategic_llm_id
|
||||
WHERE long_context_llm_id IS NULL AND strategic_llm_id IS NOT NULL;
|
||||
END IF;
|
||||
END$$;
|
||||
"""
|
||||
)
|
||||
|
||||
# Rename columns (only if they exist with old names)
|
||||
op.execute(
|
||||
"""
|
||||
DO $$
|
||||
BEGIN
|
||||
IF EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_name = 'searchspaces' AND column_name = 'fast_llm_id'
|
||||
) THEN
|
||||
ALTER TABLE searchspaces RENAME COLUMN fast_llm_id TO agent_llm_id;
|
||||
END IF;
|
||||
END$$;
|
||||
"""
|
||||
)
|
||||
|
||||
op.execute(
|
||||
"""
|
||||
DO $$
|
||||
BEGIN
|
||||
IF EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_name = 'searchspaces' AND column_name = 'long_context_llm_id'
|
||||
) THEN
|
||||
ALTER TABLE searchspaces RENAME COLUMN long_context_llm_id TO document_summary_llm_id;
|
||||
END IF;
|
||||
END$$;
|
||||
"""
|
||||
)
|
||||
|
||||
# Drop the strategic_llm_id column if it exists
|
||||
op.execute(
|
||||
"""
|
||||
DO $$
|
||||
BEGIN
|
||||
IF EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_name = 'searchspaces' AND column_name = 'strategic_llm_id'
|
||||
) THEN
|
||||
ALTER TABLE searchspaces DROP COLUMN strategic_llm_id;
|
||||
END IF;
|
||||
END$$;
|
||||
"""
|
||||
)
|
||||
|
||||
|
||||
def downgrade():
|
||||
# Add back the strategic_llm_id column
|
||||
op.execute(
|
||||
"""
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_name = 'searchspaces' AND column_name = 'strategic_llm_id'
|
||||
) THEN
|
||||
ALTER TABLE searchspaces ADD COLUMN strategic_llm_id INTEGER;
|
||||
END IF;
|
||||
END$$;
|
||||
"""
|
||||
)
|
||||
|
||||
# Rename columns back
|
||||
op.execute(
|
||||
"""
|
||||
DO $$
|
||||
BEGIN
|
||||
IF EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_name = 'searchspaces' AND column_name = 'agent_llm_id'
|
||||
) THEN
|
||||
ALTER TABLE searchspaces RENAME COLUMN agent_llm_id TO fast_llm_id;
|
||||
END IF;
|
||||
END$$;
|
||||
"""
|
||||
)
|
||||
|
||||
op.execute(
|
||||
"""
|
||||
DO $$
|
||||
BEGIN
|
||||
IF EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_name = 'searchspaces' AND column_name = 'document_summary_llm_id'
|
||||
) THEN
|
||||
ALTER TABLE searchspaces RENAME COLUMN document_summary_llm_id TO long_context_llm_id;
|
||||
END IF;
|
||||
END$$;
|
||||
"""
|
||||
)
|
||||
244
surfsense_backend/alembic/versions/53_cleanup_old_llm_configs.py
Normal file
244
surfsense_backend/alembic/versions/53_cleanup_old_llm_configs.py
Normal file
|
|
@ -0,0 +1,244 @@
|
|||
"""Migrate data from old llm_configs to new_llm_configs and cleanup
|
||||
|
||||
Revision ID: 53
|
||||
Revises: 52
|
||||
Create Date: 2024-12-22
|
||||
|
||||
This migration:
|
||||
1. Migrates data from old llm_configs table to new_llm_configs (preserving user configs)
|
||||
2. Drops the old llm_configs table (no longer used)
|
||||
3. Removes the is_default column from new_llm_configs (roles now determine which config to use)
|
||||
"""
|
||||
|
||||
from alembic import op
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "53"
|
||||
down_revision = "52"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# STEP 1: Migrate data from old llm_configs to new_llm_configs
|
||||
# This preserves any user-created configurations
|
||||
op.execute(
|
||||
"""
|
||||
DO $$
|
||||
BEGIN
|
||||
-- Only migrate if both tables exist
|
||||
IF EXISTS (
|
||||
SELECT FROM information_schema.tables
|
||||
WHERE table_name = 'llm_configs'
|
||||
) AND EXISTS (
|
||||
SELECT FROM information_schema.tables
|
||||
WHERE table_name = 'new_llm_configs'
|
||||
) THEN
|
||||
-- Insert old configs into new table (skipping duplicates by name+search_space_id)
|
||||
INSERT INTO new_llm_configs (
|
||||
name,
|
||||
description,
|
||||
provider,
|
||||
custom_provider,
|
||||
model_name,
|
||||
api_key,
|
||||
api_base,
|
||||
litellm_params,
|
||||
system_instructions,
|
||||
use_default_system_instructions,
|
||||
citations_enabled,
|
||||
is_default,
|
||||
search_space_id,
|
||||
created_at
|
||||
)
|
||||
SELECT
|
||||
lc.name,
|
||||
NULL as description, -- Old table didn't have description
|
||||
lc.provider,
|
||||
lc.custom_provider,
|
||||
lc.model_name,
|
||||
lc.api_key,
|
||||
lc.api_base,
|
||||
COALESCE(lc.litellm_params, '{}'::jsonb),
|
||||
'' as system_instructions, -- Use defaults
|
||||
TRUE as use_default_system_instructions,
|
||||
TRUE as citations_enabled,
|
||||
FALSE as is_default,
|
||||
lc.search_space_id,
|
||||
COALESCE(lc.created_at, NOW())
|
||||
FROM llm_configs lc
|
||||
WHERE lc.search_space_id IS NOT NULL
|
||||
AND NOT EXISTS (
|
||||
-- Skip if a config with same name already exists in new_llm_configs for this search space
|
||||
SELECT 1 FROM new_llm_configs nlc
|
||||
WHERE nlc.name = lc.name
|
||||
AND nlc.search_space_id = lc.search_space_id
|
||||
);
|
||||
|
||||
-- Log how many configs were migrated
|
||||
RAISE NOTICE 'Migrated % configs from llm_configs to new_llm_configs',
|
||||
(SELECT COUNT(*) FROM llm_configs WHERE search_space_id IS NOT NULL);
|
||||
END IF;
|
||||
END$$;
|
||||
"""
|
||||
)
|
||||
|
||||
# STEP 2: Update searchspaces to point to new_llm_configs for their agent LLM
|
||||
# If a search space had an agent_llm_id pointing to old llm_configs,
|
||||
# try to find the corresponding config in new_llm_configs
|
||||
op.execute(
|
||||
"""
|
||||
DO $$
|
||||
BEGIN
|
||||
IF EXISTS (
|
||||
SELECT FROM information_schema.tables
|
||||
WHERE table_name = 'llm_configs'
|
||||
) THEN
|
||||
-- Update agent_llm_id to point to migrated config in new_llm_configs
|
||||
UPDATE searchspaces ss
|
||||
SET agent_llm_id = (
|
||||
SELECT nlc.id
|
||||
FROM new_llm_configs nlc
|
||||
JOIN llm_configs lc ON lc.name = nlc.name AND lc.search_space_id = nlc.search_space_id
|
||||
WHERE lc.id = ss.agent_llm_id
|
||||
AND nlc.search_space_id = ss.id
|
||||
LIMIT 1
|
||||
)
|
||||
WHERE ss.agent_llm_id IS NOT NULL
|
||||
AND ss.agent_llm_id > 0 -- Only positive IDs (not global configs)
|
||||
AND EXISTS (
|
||||
SELECT 1 FROM llm_configs lc WHERE lc.id = ss.agent_llm_id
|
||||
);
|
||||
|
||||
-- Update document_summary_llm_id similarly
|
||||
UPDATE searchspaces ss
|
||||
SET document_summary_llm_id = (
|
||||
SELECT nlc.id
|
||||
FROM new_llm_configs nlc
|
||||
JOIN llm_configs lc ON lc.name = nlc.name AND lc.search_space_id = nlc.search_space_id
|
||||
WHERE lc.id = ss.document_summary_llm_id
|
||||
AND nlc.search_space_id = ss.id
|
||||
LIMIT 1
|
||||
)
|
||||
WHERE ss.document_summary_llm_id IS NOT NULL
|
||||
AND ss.document_summary_llm_id > 0 -- Only positive IDs (not global configs)
|
||||
AND EXISTS (
|
||||
SELECT 1 FROM llm_configs lc WHERE lc.id = ss.document_summary_llm_id
|
||||
);
|
||||
END IF;
|
||||
END$$;
|
||||
"""
|
||||
)
|
||||
|
||||
# STEP 3: Drop the is_default column from new_llm_configs
|
||||
# (role assignments now determine which config to use)
|
||||
op.execute(
|
||||
"""
|
||||
DO $$
|
||||
BEGIN
|
||||
IF EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_name = 'new_llm_configs' AND column_name = 'is_default'
|
||||
) THEN
|
||||
ALTER TABLE new_llm_configs DROP COLUMN is_default;
|
||||
END IF;
|
||||
END$$;
|
||||
"""
|
||||
)
|
||||
|
||||
# STEP 4: Drop the old llm_configs table (data has been migrated)
|
||||
op.execute("DROP TABLE IF EXISTS llm_configs CASCADE")
|
||||
|
||||
|
||||
def downgrade():
|
||||
# Recreate the old llm_configs table
|
||||
op.execute(
|
||||
"""
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT FROM information_schema.tables
|
||||
WHERE table_name = 'llm_configs'
|
||||
) THEN
|
||||
CREATE TABLE llm_configs (
|
||||
id SERIAL PRIMARY KEY,
|
||||
name VARCHAR(100) NOT NULL,
|
||||
provider litellmprovider NOT NULL,
|
||||
custom_provider VARCHAR(100),
|
||||
model_name VARCHAR(100) NOT NULL,
|
||||
api_key TEXT NOT NULL,
|
||||
api_base VARCHAR(500),
|
||||
language VARCHAR(50),
|
||||
litellm_params JSONB DEFAULT '{}',
|
||||
search_space_id INTEGER REFERENCES searchspaces(id) ON DELETE CASCADE,
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
updated_at TIMESTAMP WITH TIME ZONE
|
||||
);
|
||||
|
||||
-- Create indexes
|
||||
CREATE INDEX IF NOT EXISTS ix_llm_configs_id ON llm_configs(id);
|
||||
CREATE INDEX IF NOT EXISTS ix_llm_configs_name ON llm_configs(name);
|
||||
CREATE INDEX IF NOT EXISTS ix_llm_configs_created_at ON llm_configs(created_at);
|
||||
END IF;
|
||||
END$$;
|
||||
"""
|
||||
)
|
||||
|
||||
# Migrate data back from new_llm_configs to llm_configs
|
||||
op.execute(
|
||||
"""
|
||||
DO $$
|
||||
BEGIN
|
||||
IF EXISTS (
|
||||
SELECT FROM information_schema.tables
|
||||
WHERE table_name = 'new_llm_configs'
|
||||
) THEN
|
||||
INSERT INTO llm_configs (
|
||||
name,
|
||||
provider,
|
||||
custom_provider,
|
||||
model_name,
|
||||
api_key,
|
||||
api_base,
|
||||
language,
|
||||
litellm_params,
|
||||
search_space_id,
|
||||
created_at
|
||||
)
|
||||
SELECT
|
||||
nlc.name,
|
||||
nlc.provider,
|
||||
nlc.custom_provider,
|
||||
nlc.model_name,
|
||||
nlc.api_key,
|
||||
nlc.api_base,
|
||||
'English' as language, -- Default language
|
||||
COALESCE(nlc.litellm_params, '{}'::jsonb),
|
||||
nlc.search_space_id,
|
||||
nlc.created_at
|
||||
FROM new_llm_configs nlc
|
||||
WHERE nlc.search_space_id IS NOT NULL
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM llm_configs lc
|
||||
WHERE lc.name = nlc.name
|
||||
AND lc.search_space_id = nlc.search_space_id
|
||||
);
|
||||
END IF;
|
||||
END$$;
|
||||
"""
|
||||
)
|
||||
|
||||
# Add back the is_default column to new_llm_configs
|
||||
op.execute(
|
||||
"""
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_name = 'new_llm_configs' AND column_name = 'is_default'
|
||||
) THEN
|
||||
ALTER TABLE new_llm_configs ADD COLUMN is_default BOOLEAN NOT NULL DEFAULT FALSE;
|
||||
END IF;
|
||||
END$$;
|
||||
"""
|
||||
)
|
||||
|
|
@ -31,56 +31,50 @@ from .system_prompt import (
|
|||
)
|
||||
|
||||
# Tools - registry exports
|
||||
# Tools - factory exports (for direct use)
|
||||
# Tools - knowledge base utilities
|
||||
from .tools import (
|
||||
BUILTIN_TOOLS,
|
||||
ToolDefinition,
|
||||
build_tools,
|
||||
get_all_tool_names,
|
||||
get_default_enabled_tools,
|
||||
get_tool_by_name,
|
||||
)
|
||||
|
||||
# Tools - factory exports (for direct use)
|
||||
from .tools import (
|
||||
create_display_image_tool,
|
||||
create_generate_podcast_tool,
|
||||
create_link_preview_tool,
|
||||
create_scrape_webpage_tool,
|
||||
create_search_knowledge_base_tool,
|
||||
)
|
||||
|
||||
# Tools - knowledge base utilities
|
||||
from .tools import (
|
||||
format_documents_for_context,
|
||||
get_all_tool_names,
|
||||
get_default_enabled_tools,
|
||||
get_tool_by_name,
|
||||
search_knowledge_base_async,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
# Agent factory
|
||||
"create_surfsense_deep_agent",
|
||||
# Context
|
||||
"SurfSenseContextSchema",
|
||||
# LLM config
|
||||
"create_chat_litellm_from_config",
|
||||
"load_llm_config_from_yaml",
|
||||
# Tools registry
|
||||
"BUILTIN_TOOLS",
|
||||
# System prompt
|
||||
"SURFSENSE_CITATION_INSTRUCTIONS",
|
||||
"SURFSENSE_SYSTEM_PROMPT",
|
||||
"build_surfsense_system_prompt",
|
||||
# Tools registry
|
||||
"BUILTIN_TOOLS",
|
||||
# Context
|
||||
"SurfSenseContextSchema",
|
||||
"ToolDefinition",
|
||||
"build_surfsense_system_prompt",
|
||||
"build_tools",
|
||||
"get_all_tool_names",
|
||||
"get_default_enabled_tools",
|
||||
"get_tool_by_name",
|
||||
# LLM config
|
||||
"create_chat_litellm_from_config",
|
||||
# Tool factories
|
||||
"create_display_image_tool",
|
||||
"create_generate_podcast_tool",
|
||||
"create_link_preview_tool",
|
||||
"create_scrape_webpage_tool",
|
||||
"create_search_knowledge_base_tool",
|
||||
# Agent factory
|
||||
"create_surfsense_deep_agent",
|
||||
# Knowledge base utilities
|
||||
"format_documents_for_context",
|
||||
"get_all_tool_names",
|
||||
"get_default_enabled_tools",
|
||||
"get_tool_by_name",
|
||||
"load_llm_config_from_yaml",
|
||||
"search_knowledge_base_async",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -2,7 +2,8 @@
|
|||
SurfSense deep agent implementation.
|
||||
|
||||
This module provides the factory function for creating SurfSense deep agents
|
||||
with configurable tools via the tools registry.
|
||||
with configurable tools via the tools registry and configurable prompts
|
||||
via NewLLMConfig.
|
||||
"""
|
||||
|
||||
from collections.abc import Sequence
|
||||
|
|
@ -14,7 +15,11 @@ from langgraph.types import Checkpointer
|
|||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.agents.new_chat.context import SurfSenseContextSchema
|
||||
from app.agents.new_chat.system_prompt import build_surfsense_system_prompt
|
||||
from app.agents.new_chat.llm_config import AgentConfig
|
||||
from app.agents.new_chat.system_prompt import (
|
||||
build_configurable_system_prompt,
|
||||
build_surfsense_system_prompt,
|
||||
)
|
||||
from app.agents.new_chat.tools import build_tools
|
||||
from app.services.connector_service import ConnectorService
|
||||
|
||||
|
|
@ -29,13 +34,14 @@ def create_surfsense_deep_agent(
|
|||
db_session: AsyncSession,
|
||||
connector_service: ConnectorService,
|
||||
checkpointer: Checkpointer,
|
||||
agent_config: AgentConfig | None = None,
|
||||
enabled_tools: list[str] | None = None,
|
||||
disabled_tools: list[str] | None = None,
|
||||
additional_tools: Sequence[BaseTool] | None = None,
|
||||
firecrawl_api_key: str | None = None,
|
||||
):
|
||||
"""
|
||||
Create a SurfSense deep agent with configurable tools.
|
||||
Create a SurfSense deep agent with configurable tools and prompts.
|
||||
|
||||
The agent comes with built-in tools that can be configured:
|
||||
- search_knowledge_base: Search the user's personal knowledge base
|
||||
|
|
@ -44,6 +50,10 @@ def create_surfsense_deep_agent(
|
|||
- display_image: Display images in chat
|
||||
- scrape_webpage: Extract content from webpages
|
||||
|
||||
The system prompt can be configured via agent_config:
|
||||
- Custom system instructions (or use defaults)
|
||||
- Citation toggle (enable/disable citation requirements)
|
||||
|
||||
Args:
|
||||
llm: ChatLiteLLM instance for the agent's language model
|
||||
search_space_id: The user's search space ID
|
||||
|
|
@ -51,6 +61,8 @@ def create_surfsense_deep_agent(
|
|||
connector_service: Initialized connector service for knowledge base search
|
||||
checkpointer: LangGraph checkpointer for conversation state persistence.
|
||||
Use AsyncPostgresSaver for production or MemorySaver for testing.
|
||||
agent_config: Optional AgentConfig from NewLLMConfig for prompt configuration.
|
||||
If None, uses default system prompt with citations enabled.
|
||||
enabled_tools: Explicit list of tool names to enable. If None, all default tools
|
||||
are enabled. Use this to limit which tools are available.
|
||||
disabled_tools: List of tool names to disable. Applied after enabled_tools.
|
||||
|
|
@ -64,9 +76,21 @@ def create_surfsense_deep_agent(
|
|||
CompiledStateGraph: The configured deep agent
|
||||
|
||||
Examples:
|
||||
# Create agent with all default tools
|
||||
# Create agent with all default tools and default prompt
|
||||
agent = create_surfsense_deep_agent(llm, search_space_id, db_session, ...)
|
||||
|
||||
# Create agent with custom prompt configuration
|
||||
agent = create_surfsense_deep_agent(
|
||||
llm, search_space_id, db_session, ...,
|
||||
agent_config=AgentConfig(
|
||||
provider="OPENAI",
|
||||
model_name="gpt-4",
|
||||
api_key="...",
|
||||
system_instructions="Custom instructions...",
|
||||
citations_enabled=False,
|
||||
)
|
||||
)
|
||||
|
||||
# Create agent with only specific tools
|
||||
agent = create_surfsense_deep_agent(
|
||||
llm, search_space_id, db_session, ...,
|
||||
|
|
@ -101,11 +125,23 @@ def create_surfsense_deep_agent(
|
|||
additional_tools=list(additional_tools) if additional_tools else None,
|
||||
)
|
||||
|
||||
# Build system prompt based on agent_config
|
||||
if agent_config is not None:
|
||||
# Use configurable prompt with settings from NewLLMConfig
|
||||
system_prompt = build_configurable_system_prompt(
|
||||
custom_system_instructions=agent_config.system_instructions,
|
||||
use_default_system_instructions=agent_config.use_default_system_instructions,
|
||||
citations_enabled=agent_config.citations_enabled,
|
||||
)
|
||||
else:
|
||||
# Use default prompt (with citations enabled)
|
||||
system_prompt = build_surfsense_system_prompt()
|
||||
|
||||
# Create the deep agent with system prompt and checkpointer
|
||||
agent = create_deep_agent(
|
||||
model=llm,
|
||||
tools=tools,
|
||||
system_prompt=build_surfsense_system_prompt(),
|
||||
system_prompt=system_prompt,
|
||||
context_schema=SurfSenseContextSchema,
|
||||
checkpointer=checkpointer,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,14 +1,144 @@
|
|||
"""
|
||||
LLM configuration utilities for SurfSense agents.
|
||||
|
||||
This module provides functions for loading LLM configurations from YAML files
|
||||
and creating ChatLiteLLM instances from configuration dictionaries.
|
||||
This module provides functions for loading LLM configurations from:
|
||||
1. YAML files (global configs with negative IDs)
|
||||
2. Database NewLLMConfig table (user-created configs with positive IDs)
|
||||
|
||||
It also provides utilities for creating ChatLiteLLM instances and
|
||||
managing prompt configurations.
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
|
||||
import yaml
|
||||
from langchain_litellm import ChatLiteLLM
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
# Provider mapping for LiteLLM model string construction
|
||||
PROVIDER_MAP = {
|
||||
"OPENAI": "openai",
|
||||
"ANTHROPIC": "anthropic",
|
||||
"GROQ": "groq",
|
||||
"COHERE": "cohere",
|
||||
"GOOGLE": "gemini",
|
||||
"OLLAMA": "ollama",
|
||||
"MISTRAL": "mistral",
|
||||
"AZURE_OPENAI": "azure",
|
||||
"OPENROUTER": "openrouter",
|
||||
"XAI": "xai",
|
||||
"BEDROCK": "bedrock",
|
||||
"VERTEX_AI": "vertex_ai",
|
||||
"TOGETHER_AI": "together_ai",
|
||||
"FIREWORKS_AI": "fireworks_ai",
|
||||
"DEEPSEEK": "openai",
|
||||
"ALIBABA_QWEN": "openai",
|
||||
"MOONSHOT": "openai",
|
||||
"ZHIPU": "openai",
|
||||
"REPLICATE": "replicate",
|
||||
"PERPLEXITY": "perplexity",
|
||||
"ANYSCALE": "anyscale",
|
||||
"DEEPINFRA": "deepinfra",
|
||||
"CEREBRAS": "cerebras",
|
||||
"SAMBANOVA": "sambanova",
|
||||
"AI21": "ai21",
|
||||
"CLOUDFLARE": "cloudflare",
|
||||
"DATABRICKS": "databricks",
|
||||
"COMETAPI": "cometapi",
|
||||
"HUGGINGFACE": "huggingface",
|
||||
"CUSTOM": "custom",
|
||||
}
|
||||
|
||||
|
||||
@dataclass
|
||||
class AgentConfig:
|
||||
"""
|
||||
Complete configuration for the SurfSense agent.
|
||||
|
||||
This combines LLM settings with prompt configuration from NewLLMConfig.
|
||||
"""
|
||||
|
||||
# LLM Model Settings
|
||||
provider: str
|
||||
model_name: str
|
||||
api_key: str
|
||||
api_base: str | None = None
|
||||
custom_provider: str | None = None
|
||||
litellm_params: dict | None = None
|
||||
|
||||
# Prompt Configuration
|
||||
system_instructions: str | None = None
|
||||
use_default_system_instructions: bool = True
|
||||
citations_enabled: bool = True
|
||||
|
||||
# Metadata
|
||||
config_id: int | None = None
|
||||
config_name: str | None = None
|
||||
|
||||
@classmethod
|
||||
def from_new_llm_config(cls, config) -> "AgentConfig":
|
||||
"""
|
||||
Create an AgentConfig from a NewLLMConfig database model.
|
||||
|
||||
Args:
|
||||
config: NewLLMConfig database model instance
|
||||
|
||||
Returns:
|
||||
AgentConfig instance
|
||||
"""
|
||||
return cls(
|
||||
provider=config.provider.value
|
||||
if hasattr(config.provider, "value")
|
||||
else str(config.provider),
|
||||
model_name=config.model_name,
|
||||
api_key=config.api_key,
|
||||
api_base=config.api_base,
|
||||
custom_provider=config.custom_provider,
|
||||
litellm_params=config.litellm_params,
|
||||
system_instructions=config.system_instructions,
|
||||
use_default_system_instructions=config.use_default_system_instructions,
|
||||
citations_enabled=config.citations_enabled,
|
||||
config_id=config.id,
|
||||
config_name=config.name,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def from_yaml_config(cls, yaml_config: dict) -> "AgentConfig":
|
||||
"""
|
||||
Create an AgentConfig from a YAML configuration dictionary.
|
||||
|
||||
YAML configs now support the same prompt configuration fields as NewLLMConfig:
|
||||
- system_instructions: Custom system instructions (empty string uses defaults)
|
||||
- use_default_system_instructions: Whether to use default instructions
|
||||
- citations_enabled: Whether citations are enabled
|
||||
|
||||
Args:
|
||||
yaml_config: Configuration dictionary from YAML file
|
||||
|
||||
Returns:
|
||||
AgentConfig instance
|
||||
"""
|
||||
# Get system instructions from YAML, default to empty string
|
||||
system_instructions = yaml_config.get("system_instructions", "")
|
||||
|
||||
return cls(
|
||||
provider=yaml_config.get("provider", "").upper(),
|
||||
model_name=yaml_config.get("model_name", ""),
|
||||
api_key=yaml_config.get("api_key", ""),
|
||||
api_base=yaml_config.get("api_base"),
|
||||
custom_provider=yaml_config.get("custom_provider"),
|
||||
litellm_params=yaml_config.get("litellm_params"),
|
||||
# Prompt configuration from YAML (with defaults for backwards compatibility)
|
||||
system_instructions=system_instructions if system_instructions else None,
|
||||
use_default_system_instructions=yaml_config.get(
|
||||
"use_default_system_instructions", True
|
||||
),
|
||||
citations_enabled=yaml_config.get("citations_enabled", True),
|
||||
config_id=yaml_config.get("id"),
|
||||
config_name=yaml_config.get("name"),
|
||||
)
|
||||
|
||||
|
||||
def load_llm_config_from_yaml(llm_config_id: int = -1) -> dict | None:
|
||||
|
|
@ -47,9 +177,118 @@ def load_llm_config_from_yaml(llm_config_id: int = -1) -> dict | None:
|
|||
return None
|
||||
|
||||
|
||||
async def load_new_llm_config_from_db(
|
||||
session: AsyncSession,
|
||||
config_id: int,
|
||||
) -> "AgentConfig | None":
|
||||
"""
|
||||
Load a NewLLMConfig from the database by ID.
|
||||
|
||||
Args:
|
||||
session: AsyncSession for database access
|
||||
config_id: The ID of the NewLLMConfig to load
|
||||
|
||||
Returns:
|
||||
AgentConfig instance or None if not found
|
||||
"""
|
||||
# Import here to avoid circular imports
|
||||
from app.db import NewLLMConfig
|
||||
|
||||
try:
|
||||
result = await session.execute(
|
||||
select(NewLLMConfig).filter(NewLLMConfig.id == config_id)
|
||||
)
|
||||
config = result.scalars().first()
|
||||
|
||||
if not config:
|
||||
print(f"Error: NewLLMConfig with id {config_id} not found")
|
||||
return None
|
||||
|
||||
return AgentConfig.from_new_llm_config(config)
|
||||
except Exception as e:
|
||||
print(f"Error loading NewLLMConfig from database: {e}")
|
||||
return None
|
||||
|
||||
|
||||
async def load_agent_llm_config_for_search_space(
|
||||
session: AsyncSession,
|
||||
search_space_id: int,
|
||||
) -> "AgentConfig | None":
|
||||
"""
|
||||
Load the agent LLM configuration for a search space.
|
||||
|
||||
This loads the LLM config based on the search space's agent_llm_id setting:
|
||||
- Positive ID: Load from NewLLMConfig database table
|
||||
- Negative ID: Load from YAML global configs
|
||||
- None: Falls back to first global config (id=-1)
|
||||
|
||||
Args:
|
||||
session: AsyncSession for database access
|
||||
search_space_id: The search space ID
|
||||
|
||||
Returns:
|
||||
AgentConfig instance or None if not found
|
||||
"""
|
||||
# Import here to avoid circular imports
|
||||
from app.db import SearchSpace
|
||||
|
||||
try:
|
||||
# Get the search space to check its agent_llm_id preference
|
||||
result = await session.execute(
|
||||
select(SearchSpace).filter(SearchSpace.id == search_space_id)
|
||||
)
|
||||
search_space = result.scalars().first()
|
||||
|
||||
if not search_space:
|
||||
print(f"Error: SearchSpace with id {search_space_id} not found")
|
||||
return None
|
||||
|
||||
# Use agent_llm_id from search space, fallback to -1 (first global config)
|
||||
config_id = (
|
||||
search_space.agent_llm_id if search_space.agent_llm_id is not None else -1
|
||||
)
|
||||
|
||||
# Load the config using the unified loader
|
||||
return await load_agent_config(session, config_id, search_space_id)
|
||||
except Exception as e:
|
||||
print(f"Error loading agent LLM config for search space {search_space_id}: {e}")
|
||||
return None
|
||||
|
||||
|
||||
async def load_agent_config(
|
||||
session: AsyncSession,
|
||||
config_id: int,
|
||||
search_space_id: int | None = None,
|
||||
) -> "AgentConfig | None":
|
||||
"""
|
||||
Load an agent configuration, supporting both YAML (negative IDs) and database (positive IDs) configs.
|
||||
|
||||
This is the main entry point for loading configurations:
|
||||
- Negative IDs: Load from YAML file (global configs)
|
||||
- Positive IDs: Load from NewLLMConfig database table
|
||||
|
||||
Args:
|
||||
session: AsyncSession for database access
|
||||
config_id: The config ID (negative for YAML, positive for database)
|
||||
search_space_id: Optional search space ID for context
|
||||
|
||||
Returns:
|
||||
AgentConfig instance or None if not found
|
||||
"""
|
||||
if config_id < 0:
|
||||
# Load from YAML (global configs have negative IDs)
|
||||
yaml_config = load_llm_config_from_yaml(config_id)
|
||||
if yaml_config:
|
||||
return AgentConfig.from_yaml_config(yaml_config)
|
||||
return None
|
||||
else:
|
||||
# Load from database (NewLLMConfig)
|
||||
return await load_new_llm_config_from_db(session, config_id)
|
||||
|
||||
|
||||
def create_chat_litellm_from_config(llm_config: dict) -> ChatLiteLLM | None:
|
||||
"""
|
||||
Create a ChatLiteLLM instance from a global LLM config.
|
||||
Create a ChatLiteLLM instance from a global LLM config dictionary.
|
||||
|
||||
Args:
|
||||
llm_config: LLM configuration dictionary from YAML
|
||||
|
|
@ -57,34 +296,12 @@ def create_chat_litellm_from_config(llm_config: dict) -> ChatLiteLLM | None:
|
|||
Returns:
|
||||
ChatLiteLLM instance or None on error
|
||||
"""
|
||||
# Provider mapping (same as in llm_service.py)
|
||||
provider_map = {
|
||||
"OPENAI": "openai",
|
||||
"ANTHROPIC": "anthropic",
|
||||
"GROQ": "groq",
|
||||
"COHERE": "cohere",
|
||||
"GOOGLE": "gemini",
|
||||
"OLLAMA": "ollama",
|
||||
"MISTRAL": "mistral",
|
||||
"AZURE_OPENAI": "azure",
|
||||
"OPENROUTER": "openrouter",
|
||||
"XAI": "xai",
|
||||
"BEDROCK": "bedrock",
|
||||
"VERTEX_AI": "vertex_ai",
|
||||
"TOGETHER_AI": "together_ai",
|
||||
"FIREWORKS_AI": "fireworks_ai",
|
||||
"DEEPSEEK": "openai",
|
||||
"ALIBABA_QWEN": "openai",
|
||||
"MOONSHOT": "openai",
|
||||
"ZHIPU": "openai",
|
||||
}
|
||||
|
||||
# Build the model string
|
||||
if llm_config.get("custom_provider"):
|
||||
model_string = f"{llm_config['custom_provider']}/{llm_config['model_name']}"
|
||||
else:
|
||||
provider = llm_config.get("provider", "").upper()
|
||||
provider_prefix = provider_map.get(provider, provider.lower())
|
||||
provider_prefix = PROVIDER_MAP.get(provider, provider.lower())
|
||||
model_string = f"{provider_prefix}/{llm_config['model_name']}"
|
||||
|
||||
# Create ChatLiteLLM instance with streaming enabled
|
||||
|
|
@ -103,3 +320,42 @@ def create_chat_litellm_from_config(llm_config: dict) -> ChatLiteLLM | None:
|
|||
litellm_kwargs.update(llm_config["litellm_params"])
|
||||
|
||||
return ChatLiteLLM(**litellm_kwargs)
|
||||
|
||||
|
||||
def create_chat_litellm_from_agent_config(
|
||||
agent_config: AgentConfig,
|
||||
) -> ChatLiteLLM | None:
|
||||
"""
|
||||
Create a ChatLiteLLM instance from an AgentConfig.
|
||||
|
||||
Args:
|
||||
agent_config: AgentConfig instance
|
||||
|
||||
Returns:
|
||||
ChatLiteLLM instance or None on error
|
||||
"""
|
||||
# Build the model string
|
||||
if agent_config.custom_provider:
|
||||
model_string = f"{agent_config.custom_provider}/{agent_config.model_name}"
|
||||
else:
|
||||
provider_prefix = PROVIDER_MAP.get(
|
||||
agent_config.provider, agent_config.provider.lower()
|
||||
)
|
||||
model_string = f"{provider_prefix}/{agent_config.model_name}"
|
||||
|
||||
# Create ChatLiteLLM instance with streaming enabled
|
||||
litellm_kwargs = {
|
||||
"model": model_string,
|
||||
"api_key": agent_config.api_key,
|
||||
"streaming": True, # Enable streaming for real-time token streaming
|
||||
}
|
||||
|
||||
# Add optional parameters
|
||||
if agent_config.api_base:
|
||||
litellm_kwargs["api_base"] = agent_config.api_base
|
||||
|
||||
# Add any additional litellm parameters
|
||||
if agent_config.litellm_params:
|
||||
litellm_kwargs.update(agent_config.litellm_params)
|
||||
|
||||
return ChatLiteLLM(**litellm_kwargs)
|
||||
|
|
|
|||
|
|
@ -3,10 +3,16 @@ System prompt building for SurfSense agents.
|
|||
|
||||
This module provides functions and constants for building the SurfSense system prompt
|
||||
with configurable user instructions and citation support.
|
||||
|
||||
The prompt is composed of three parts:
|
||||
1. System Instructions (configurable via NewLLMConfig)
|
||||
2. Tools Instructions (always included, not configurable)
|
||||
3. Citation Instructions (toggleable via NewLLMConfig.citations_enabled)
|
||||
"""
|
||||
|
||||
from datetime import UTC, datetime
|
||||
|
||||
# Default system instructions - can be overridden via NewLLMConfig.system_instructions
|
||||
SURFSENSE_SYSTEM_INSTRUCTIONS = """
|
||||
<system_instruction>
|
||||
You are SurfSense, a reasoning and acting AI agent designed to answer user questions using the user's personal knowledge base.
|
||||
|
|
@ -219,12 +225,38 @@ However, from your video learning, it's important to note that asyncio is not su
|
|||
</citation_instructions>
|
||||
"""
|
||||
|
||||
# Anti-citation prompt - used when citations are disabled
|
||||
# This explicitly tells the model NOT to include citations
|
||||
SURFSENSE_NO_CITATION_INSTRUCTIONS = """
|
||||
<citation_instructions>
|
||||
IMPORTANT: Citations are DISABLED for this configuration.
|
||||
|
||||
DO NOT include any citations in your responses. Specifically:
|
||||
1. Do NOT use the [citation:chunk_id] format anywhere in your response.
|
||||
2. Do NOT reference document IDs, chunk IDs, or source IDs.
|
||||
3. Simply provide the information naturally without any citation markers.
|
||||
4. Write your response as if you're having a normal conversation, incorporating the information from your knowledge seamlessly.
|
||||
|
||||
When answering questions based on documents from the knowledge base:
|
||||
- Present the information directly and confidently
|
||||
- Do not mention that information comes from specific documents or chunks
|
||||
- Integrate facts naturally into your response without attribution markers
|
||||
|
||||
Your goal is to provide helpful, informative answers in a clean, readable format without any citation notation.
|
||||
</citation_instructions>
|
||||
"""
|
||||
|
||||
|
||||
def build_surfsense_system_prompt(
|
||||
today: datetime | None = None,
|
||||
) -> str:
|
||||
"""
|
||||
Build the SurfSense system prompt.
|
||||
Build the SurfSense system prompt with default settings.
|
||||
|
||||
This is a convenience function that builds the prompt with:
|
||||
- Default system instructions
|
||||
- Tools instructions (always included)
|
||||
- Citation instructions enabled
|
||||
|
||||
Args:
|
||||
today: Optional datetime for today's date (defaults to current UTC date)
|
||||
|
|
@ -241,4 +273,74 @@ def build_surfsense_system_prompt(
|
|||
)
|
||||
|
||||
|
||||
def build_configurable_system_prompt(
|
||||
custom_system_instructions: str | None = None,
|
||||
use_default_system_instructions: bool = True,
|
||||
citations_enabled: bool = True,
|
||||
today: datetime | None = None,
|
||||
) -> str:
|
||||
"""
|
||||
Build a configurable SurfSense system prompt based on NewLLMConfig settings.
|
||||
|
||||
The prompt is composed of three parts:
|
||||
1. System Instructions - either custom or default SURFSENSE_SYSTEM_INSTRUCTIONS
|
||||
2. Tools Instructions - always included (SURFSENSE_TOOLS_INSTRUCTIONS)
|
||||
3. Citation Instructions - either SURFSENSE_CITATION_INSTRUCTIONS or SURFSENSE_NO_CITATION_INSTRUCTIONS
|
||||
|
||||
Args:
|
||||
custom_system_instructions: Custom system instructions to use. If empty/None and
|
||||
use_default_system_instructions is True, defaults to
|
||||
SURFSENSE_SYSTEM_INSTRUCTIONS.
|
||||
use_default_system_instructions: Whether to use default instructions when
|
||||
custom_system_instructions is empty/None.
|
||||
citations_enabled: Whether to include citation instructions (True) or
|
||||
anti-citation instructions (False).
|
||||
today: Optional datetime for today's date (defaults to current UTC date)
|
||||
|
||||
Returns:
|
||||
Complete system prompt string
|
||||
"""
|
||||
resolved_today = (today or datetime.now(UTC)).astimezone(UTC).date().isoformat()
|
||||
|
||||
# Determine system instructions
|
||||
if custom_system_instructions and custom_system_instructions.strip():
|
||||
# Use custom instructions, injecting the date placeholder if present
|
||||
system_instructions = custom_system_instructions.format(
|
||||
resolved_today=resolved_today
|
||||
)
|
||||
elif use_default_system_instructions:
|
||||
# Use default instructions
|
||||
system_instructions = SURFSENSE_SYSTEM_INSTRUCTIONS.format(
|
||||
resolved_today=resolved_today
|
||||
)
|
||||
else:
|
||||
# No system instructions (edge case)
|
||||
system_instructions = ""
|
||||
|
||||
# Tools instructions are always included
|
||||
tools_instructions = SURFSENSE_TOOLS_INSTRUCTIONS
|
||||
|
||||
# Citation instructions based on toggle
|
||||
citation_instructions = (
|
||||
SURFSENSE_CITATION_INSTRUCTIONS
|
||||
if citations_enabled
|
||||
else SURFSENSE_NO_CITATION_INSTRUCTIONS
|
||||
)
|
||||
|
||||
return system_instructions + tools_instructions + citation_instructions
|
||||
|
||||
|
||||
def get_default_system_instructions() -> str:
|
||||
"""
|
||||
Get the default system instructions template.
|
||||
|
||||
This is useful for populating the UI with the default value when
|
||||
creating a new NewLLMConfig.
|
||||
|
||||
Returns:
|
||||
Default system instructions string (with {resolved_today} placeholder)
|
||||
"""
|
||||
return SURFSENSE_SYSTEM_INSTRUCTIONS.strip()
|
||||
|
||||
|
||||
SURFSENSE_SYSTEM_PROMPT = build_surfsense_system_prompt()
|
||||
|
|
|
|||
|
|
@ -13,15 +13,6 @@ Available tools:
|
|||
"""
|
||||
|
||||
# Registry exports
|
||||
from .registry import (
|
||||
BUILTIN_TOOLS,
|
||||
ToolDefinition,
|
||||
build_tools,
|
||||
get_all_tool_names,
|
||||
get_default_enabled_tools,
|
||||
get_tool_by_name,
|
||||
)
|
||||
|
||||
# Tool factory exports (for direct use)
|
||||
from .display_image import create_display_image_tool
|
||||
from .knowledge_base import (
|
||||
|
|
@ -31,6 +22,14 @@ from .knowledge_base import (
|
|||
)
|
||||
from .link_preview import create_link_preview_tool
|
||||
from .podcast import create_generate_podcast_tool
|
||||
from .registry import (
|
||||
BUILTIN_TOOLS,
|
||||
ToolDefinition,
|
||||
build_tools,
|
||||
get_all_tool_names,
|
||||
get_default_enabled_tools,
|
||||
get_tool_by_name,
|
||||
)
|
||||
from .scrape_webpage import create_scrape_webpage_tool
|
||||
|
||||
__all__ = [
|
||||
|
|
@ -38,9 +37,6 @@ __all__ = [
|
|||
"BUILTIN_TOOLS",
|
||||
"ToolDefinition",
|
||||
"build_tools",
|
||||
"get_all_tool_names",
|
||||
"get_default_enabled_tools",
|
||||
"get_tool_by_name",
|
||||
# Tool factories
|
||||
"create_display_image_tool",
|
||||
"create_generate_podcast_tool",
|
||||
|
|
@ -49,6 +45,8 @@ __all__ = [
|
|||
"create_search_knowledge_base_tool",
|
||||
# Knowledge base utilities
|
||||
"format_documents_for_context",
|
||||
"get_all_tool_names",
|
||||
"get_default_enabled_tools",
|
||||
"get_tool_by_name",
|
||||
"search_knowledge_base_async",
|
||||
]
|
||||
|
||||
|
|
|
|||
|
|
@ -86,7 +86,9 @@ def create_display_image_tool():
|
|||
ratio = "16:9" # Default
|
||||
if "unsplash.com" in src or "pexels.com" in src:
|
||||
ratio = "16:9"
|
||||
elif "imgur.com" in src or "github.com" in src or "githubusercontent.com" in src:
|
||||
elif (
|
||||
"imgur.com" in src or "github.com" in src or "githubusercontent.com" in src
|
||||
):
|
||||
ratio = "auto"
|
||||
|
||||
return {
|
||||
|
|
@ -101,4 +103,3 @@ def create_display_image_tool():
|
|||
}
|
||||
|
||||
return display_image
|
||||
|
||||
|
|
|
|||
|
|
@ -605,4 +605,3 @@ def create_search_knowledge_base_tool(
|
|||
)
|
||||
|
||||
return search_knowledge_base
|
||||
|
||||
|
|
|
|||
|
|
@ -46,13 +46,17 @@ def extract_og_content(html: str, property_name: str) -> str | None:
|
|||
|
||||
def extract_twitter_content(html: str, name: str) -> str | None:
|
||||
"""Extract Twitter Card meta content from HTML."""
|
||||
pattern = rf'<meta[^>]+name=["\']twitter:{name}["\'][^>]+content=["\']([^"\']+)["\']'
|
||||
pattern = (
|
||||
rf'<meta[^>]+name=["\']twitter:{name}["\'][^>]+content=["\']([^"\']+)["\']'
|
||||
)
|
||||
match = re.search(pattern, html, re.IGNORECASE)
|
||||
if match:
|
||||
return match.group(1)
|
||||
|
||||
# Try content before name
|
||||
pattern = rf'<meta[^>]+content=["\']([^"\']+)["\'][^>]+name=["\']twitter:{name}["\']'
|
||||
pattern = (
|
||||
rf'<meta[^>]+content=["\']([^"\']+)["\'][^>]+name=["\']twitter:{name}["\']'
|
||||
)
|
||||
match = re.search(pattern, html, re.IGNORECASE)
|
||||
if match:
|
||||
return match.group(1)
|
||||
|
|
@ -289,4 +293,3 @@ def create_link_preview_tool():
|
|||
}
|
||||
|
||||
return link_preview
|
||||
|
||||
|
|
|
|||
|
|
@ -171,4 +171,3 @@ def create_generate_podcast_tool(
|
|||
}
|
||||
|
||||
return generate_podcast
|
||||
|
||||
|
|
|
|||
|
|
@ -37,11 +37,18 @@ Example of adding a new tool:
|
|||
),
|
||||
"""
|
||||
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any, Callable
|
||||
from typing import Any
|
||||
|
||||
from langchain_core.tools import BaseTool
|
||||
|
||||
from .display_image import create_display_image_tool
|
||||
from .knowledge_base import create_search_knowledge_base_tool
|
||||
from .link_preview import create_link_preview_tool
|
||||
from .podcast import create_generate_podcast_tool
|
||||
from .scrape_webpage import create_scrape_webpage_tool
|
||||
|
||||
# =============================================================================
|
||||
# Tool Definition
|
||||
# =============================================================================
|
||||
|
|
@ -71,13 +78,6 @@ class ToolDefinition:
|
|||
# Built-in Tools Registry
|
||||
# =============================================================================
|
||||
|
||||
# Import tool factory functions
|
||||
from .display_image import create_display_image_tool
|
||||
from .knowledge_base import create_search_knowledge_base_tool
|
||||
from .link_preview import create_link_preview_tool
|
||||
from .podcast import create_generate_podcast_tool
|
||||
from .scrape_webpage import create_scrape_webpage_tool
|
||||
|
||||
# Registry of all built-in tools
|
||||
# Contributors: Add your new tools here!
|
||||
BUILTIN_TOOLS: list[ToolDefinition] = [
|
||||
|
|
@ -228,4 +228,3 @@ def build_tools(
|
|||
tools.extend(additional_tools)
|
||||
|
||||
return tools
|
||||
|
||||
|
|
|
|||
|
|
@ -156,7 +156,9 @@ def create_scrape_webpage_tool(firecrawl_api_key: str | None = None):
|
|||
if not description and content:
|
||||
# Use first paragraph as description
|
||||
first_para = content.split("\n\n")[0] if content else ""
|
||||
description = first_para[:300] + "..." if len(first_para) > 300 else first_para
|
||||
description = (
|
||||
first_para[:300] + "..." if len(first_para) > 300 else first_para
|
||||
)
|
||||
|
||||
# Truncate content if needed
|
||||
content, was_truncated = truncate_content(content, max_length)
|
||||
|
|
@ -194,4 +196,3 @@ def create_scrape_webpage_tool(firecrawl_api_key: str | None = None):
|
|||
}
|
||||
|
||||
return scrape_webpage
|
||||
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ from litellm import aspeech
|
|||
|
||||
from app.config import config as app_config
|
||||
from app.services.kokoro_tts_service import get_kokoro_tts_service
|
||||
from app.services.llm_service import get_long_context_llm
|
||||
from app.services.llm_service import get_document_summary_llm
|
||||
|
||||
from .configuration import Configuration
|
||||
from .prompts import get_podcast_generation_prompt
|
||||
|
|
@ -30,10 +30,12 @@ async def create_podcast_transcript(
|
|||
search_space_id = configuration.search_space_id
|
||||
user_prompt = configuration.user_prompt
|
||||
|
||||
# Get search space's long context LLM
|
||||
llm = await get_long_context_llm(state.db_session, search_space_id)
|
||||
# Get search space's document summary LLM
|
||||
llm = await get_document_summary_llm(state.db_session, search_space_id)
|
||||
if not llm:
|
||||
error_message = f"No long context LLM configured for search space {search_space_id}"
|
||||
error_message = (
|
||||
f"No document summary LLM configured for search space {search_space_id}"
|
||||
)
|
||||
print(error_message)
|
||||
raise RuntimeError(error_message)
|
||||
|
||||
|
|
|
|||
|
|
@ -35,12 +35,6 @@ def load_global_llm_configs():
|
|||
# Try main config file first
|
||||
global_config_file = BASE_DIR / "app" / "config" / "global_llm_config.yaml"
|
||||
|
||||
# Fall back to example file for testing
|
||||
# if not global_config_file.exists():
|
||||
# global_config_file = BASE_DIR / "app" / "config" / "global_llm_config.example.yaml"
|
||||
# if global_config_file.exists():
|
||||
# print("Info: Using global_llm_config.example.yaml (copy to global_llm_config.yaml for production)")
|
||||
|
||||
if not global_config_file.exists():
|
||||
# No global configs available
|
||||
return []
|
||||
|
|
|
|||
|
|
@ -9,72 +9,101 @@
|
|||
#
|
||||
# These configurations will be available to all users as a convenient option
|
||||
# Users can choose to use these global configs or add their own
|
||||
#
|
||||
# Structure matches NewLLMConfig:
|
||||
# - LLM model configuration (provider, model_name, api_key, etc.)
|
||||
# - Prompt configuration (system_instructions, citations_enabled)
|
||||
|
||||
global_llm_configs:
|
||||
# Example: OpenAI GPT-4 Turbo
|
||||
# Example: OpenAI GPT-4 Turbo with citations enabled
|
||||
- id: -1
|
||||
name: "Global GPT-4 Turbo"
|
||||
description: "OpenAI's GPT-4 Turbo with default prompts and citations"
|
||||
provider: "OPENAI"
|
||||
model_name: "gpt-4-turbo-preview"
|
||||
api_key: "sk-your-openai-api-key-here"
|
||||
api_base: ""
|
||||
language: "English"
|
||||
litellm_params:
|
||||
temperature: 0.7
|
||||
max_tokens: 4000
|
||||
# Prompt Configuration
|
||||
system_instructions: "" # Empty = use default SURFSENSE_SYSTEM_INSTRUCTIONS
|
||||
use_default_system_instructions: true
|
||||
citations_enabled: true
|
||||
|
||||
# Example: Anthropic Claude 3 Opus
|
||||
- id: -2
|
||||
name: "Global Claude 3 Opus"
|
||||
description: "Anthropic's most capable model with citations"
|
||||
provider: "ANTHROPIC"
|
||||
model_name: "claude-3-opus-20240229"
|
||||
api_key: "sk-ant-your-anthropic-api-key-here"
|
||||
api_base: ""
|
||||
language: "English"
|
||||
litellm_params:
|
||||
temperature: 0.7
|
||||
max_tokens: 4000
|
||||
system_instructions: ""
|
||||
use_default_system_instructions: true
|
||||
citations_enabled: true
|
||||
|
||||
# Example: Fast model - GPT-3.5 Turbo
|
||||
# Example: Fast model - GPT-3.5 Turbo (citations disabled for speed)
|
||||
- id: -3
|
||||
name: "Global GPT-3.5 Turbo"
|
||||
name: "Global GPT-3.5 Turbo (Fast)"
|
||||
description: "Fast responses without citations for quick queries"
|
||||
provider: "OPENAI"
|
||||
model_name: "gpt-3.5-turbo"
|
||||
api_key: "sk-your-openai-api-key-here"
|
||||
api_base: ""
|
||||
language: "English"
|
||||
litellm_params:
|
||||
temperature: 0.5
|
||||
max_tokens: 2000
|
||||
system_instructions: ""
|
||||
use_default_system_instructions: true
|
||||
citations_enabled: false # Disabled for faster responses
|
||||
|
||||
# Example: Chinese LLM - DeepSeek
|
||||
# Example: Chinese LLM - DeepSeek with custom instructions
|
||||
- id: -4
|
||||
name: "Global DeepSeek Chat"
|
||||
name: "Global DeepSeek Chat (Chinese)"
|
||||
description: "DeepSeek optimized for Chinese language responses"
|
||||
provider: "DEEPSEEK"
|
||||
model_name: "deepseek-chat"
|
||||
api_key: "your-deepseek-api-key-here"
|
||||
api_base: "https://api.deepseek.com/v1"
|
||||
language: "Chinese"
|
||||
litellm_params:
|
||||
temperature: 0.7
|
||||
max_tokens: 4000
|
||||
# Custom system instructions for Chinese responses
|
||||
system_instructions: |
|
||||
<system_instruction>
|
||||
You are SurfSense, a reasoning and acting AI agent designed to answer user questions using the user's personal knowledge base.
|
||||
|
||||
Today's date (UTC): {resolved_today}
|
||||
|
||||
IMPORTANT: Please respond in Chinese (简体中文) unless the user specifically requests another language.
|
||||
</system_instruction>
|
||||
use_default_system_instructions: false
|
||||
citations_enabled: true
|
||||
|
||||
# Example: Groq - Fast inference
|
||||
- id: -5
|
||||
name: "Global Groq Llama 3"
|
||||
description: "Ultra-fast Llama 3 70B via Groq"
|
||||
provider: "GROQ"
|
||||
model_name: "llama3-70b-8192"
|
||||
api_key: "your-groq-api-key-here"
|
||||
api_base: ""
|
||||
language: "English"
|
||||
litellm_params:
|
||||
temperature: 0.7
|
||||
max_tokens: 8000
|
||||
system_instructions: ""
|
||||
use_default_system_instructions: true
|
||||
citations_enabled: true
|
||||
|
||||
# Notes:
|
||||
# - Use negative IDs to distinguish global configs from user configs
|
||||
# - Use negative IDs to distinguish global configs from user configs (NewLLMConfig in DB)
|
||||
# - IDs should be unique and sequential (e.g., -1, -2, -3, etc.)
|
||||
# - The 'api_key' field will not be exposed to users via API
|
||||
# - Users can select these configs for their long_context, fast, or strategic LLM roles
|
||||
# - system_instructions: Custom prompt or empty string to use defaults
|
||||
# - use_default_system_instructions: true = use SURFSENSE_SYSTEM_INSTRUCTIONS when system_instructions is empty
|
||||
# - citations_enabled: true = include citation instructions, false = include anti-citation instructions
|
||||
# - All standard LiteLLM providers are supported
|
||||
|
||||
|
|
|
|||
|
|
@ -452,9 +452,10 @@ class SearchSpace(BaseModel, TimestampMixin):
|
|||
|
||||
# Search space-level LLM preferences (shared by all members)
|
||||
# Note: These can be negative IDs for global configs (from YAML) or positive IDs for custom configs (from DB)
|
||||
long_context_llm_id = Column(Integer, nullable=True)
|
||||
fast_llm_id = Column(Integer, nullable=True)
|
||||
strategic_llm_id = Column(Integer, nullable=True)
|
||||
agent_llm_id = Column(Integer, nullable=True) # For agent/chat operations
|
||||
document_summary_llm_id = Column(
|
||||
Integer, nullable=True
|
||||
) # For document summarization
|
||||
|
||||
user_id = Column(
|
||||
UUID(as_uuid=True), ForeignKey("user.id", ondelete="CASCADE"), nullable=False
|
||||
|
|
@ -491,10 +492,10 @@ class SearchSpace(BaseModel, TimestampMixin):
|
|||
order_by="SearchSourceConnector.id",
|
||||
cascade="all, delete-orphan",
|
||||
)
|
||||
llm_configs = relationship(
|
||||
"LLMConfig",
|
||||
new_llm_configs = relationship(
|
||||
"NewLLMConfig",
|
||||
back_populates="search_space",
|
||||
order_by="LLMConfig.id",
|
||||
order_by="NewLLMConfig.id",
|
||||
cascade="all, delete-orphan",
|
||||
)
|
||||
|
||||
|
|
@ -553,10 +554,24 @@ class SearchSourceConnector(BaseModel, TimestampMixin):
|
|||
)
|
||||
|
||||
|
||||
class LLMConfig(BaseModel, TimestampMixin):
|
||||
__tablename__ = "llm_configs"
|
||||
class NewLLMConfig(BaseModel, TimestampMixin):
|
||||
"""
|
||||
New LLM configuration table that combines model settings with prompt configuration.
|
||||
|
||||
This table provides:
|
||||
- LLM model configuration (provider, model_name, api_key, etc.)
|
||||
- Configurable system instructions (defaults to SURFSENSE_SYSTEM_INSTRUCTIONS)
|
||||
- Citation toggle (enable/disable citation instructions)
|
||||
|
||||
Note: SURFSENSE_TOOLS_INSTRUCTIONS is always used and not configurable.
|
||||
"""
|
||||
|
||||
__tablename__ = "new_llm_configs"
|
||||
|
||||
name = Column(String(100), nullable=False, index=True)
|
||||
description = Column(String(500), nullable=True)
|
||||
|
||||
# === LLM Model Configuration (from original LLMConfig, excluding 'language') ===
|
||||
# Provider from the enum
|
||||
provider = Column(SQLAlchemyEnum(LiteLLMProvider), nullable=False)
|
||||
# Custom provider name when provider is CUSTOM
|
||||
|
|
@ -566,16 +581,29 @@ class LLMConfig(BaseModel, TimestampMixin):
|
|||
# API Key should be encrypted before storing
|
||||
api_key = Column(String, nullable=False)
|
||||
api_base = Column(String(500), nullable=True)
|
||||
|
||||
language = Column(String(50), nullable=True, default="English")
|
||||
|
||||
# For any other parameters that litellm supports
|
||||
litellm_params = Column(JSON, nullable=True, default={})
|
||||
|
||||
# === Prompt Configuration ===
|
||||
# Configurable system instructions (defaults to SURFSENSE_SYSTEM_INSTRUCTIONS)
|
||||
# Users can customize this from the UI
|
||||
system_instructions = Column(
|
||||
Text,
|
||||
nullable=False,
|
||||
default="", # Empty string means use default SURFSENSE_SYSTEM_INSTRUCTIONS
|
||||
)
|
||||
# Whether to use the default system instructions when system_instructions is empty
|
||||
use_default_system_instructions = Column(Boolean, nullable=False, default=True)
|
||||
|
||||
# Citation toggle - when enabled, SURFSENSE_CITATION_INSTRUCTIONS is injected
|
||||
# When disabled, an anti-citation prompt is injected instead
|
||||
citations_enabled = Column(Boolean, nullable=False, default=True)
|
||||
|
||||
# === Relationships ===
|
||||
search_space_id = Column(
|
||||
Integer, ForeignKey("searchspaces.id", ondelete="CASCADE"), nullable=False
|
||||
)
|
||||
search_space = relationship("SearchSpace", back_populates="llm_configs")
|
||||
search_space = relationship("SearchSpace", back_populates="new_llm_configs")
|
||||
|
||||
|
||||
class Log(BaseModel, TimestampMixin):
|
||||
|
|
|
|||
|
|
@ -1,190 +0,0 @@
|
|||
prompts:
|
||||
# Developer-focused prompts
|
||||
- key: ethereum_developer
|
||||
value: "Imagine you are an experienced Ethereum developer tasked with creating a smart contract for a blockchain messenger. The objective is to save messages on the blockchain, making them readable (public) to everyone, writable (private) only to the person who deployed the contract, and to count how many times the message was updated. Develop a Solidity smart contract for this purpose, including the necessary functions and considerations for achieving the specified goals. Please provide the code and any relevant explanations to ensure a clear understanding of the implementation."
|
||||
author: "awesome-chatgpt-prompts"
|
||||
link: "https://github.com/f/awesome-chatgpt-prompts"
|
||||
category: "developer"
|
||||
|
||||
- key: linux_terminal
|
||||
value: "I want you to act as a linux terminal. I will type commands and you will reply with what the terminal should show. I want you to only reply with the terminal output inside one unique code block, and nothing else. do not write explanations. do not type commands unless I instruct you to do so. when i need to tell you something in english, i will do so by putting text inside curly brackets {like this}."
|
||||
author: "awesome-chatgpt-prompts"
|
||||
link: "https://github.com/f/awesome-chatgpt-prompts"
|
||||
category: "developer"
|
||||
|
||||
- key: javascript_console
|
||||
value: "I want you to act as a javascript console. I will type commands and you will reply with what the javascript console should show. I want you to only reply with the terminal output inside one unique code block, and nothing else. do not write explanations. do not type commands unless I instruct you to do so. when i need to tell you something in english, i will do so by putting text inside curly brackets {like this}."
|
||||
author: "awesome-chatgpt-prompts"
|
||||
link: "https://github.com/f/awesome-chatgpt-prompts"
|
||||
category: "developer"
|
||||
|
||||
- key: fullstack_developer
|
||||
value: "I want you to act as a software developer. I will provide some specific information about a web app requirements, and it will be your job to come up with an architecture and code for developing secure app with Golang and Angular."
|
||||
author: "awesome-chatgpt-prompts"
|
||||
link: "https://github.com/f/awesome-chatgpt-prompts"
|
||||
category: "developer"
|
||||
|
||||
- key: regex_generator
|
||||
value: "I want you to act as a regex generator. Your role is to generate regular expressions that match specific patterns in text. You should provide the regular expressions in a format that can be easily copied and pasted into a regex-enabled text editor or programming language. Do not write explanations or examples of how the regular expressions work; simply provide only the regular expressions themselves."
|
||||
author: "awesome-chatgpt-prompts"
|
||||
link: "https://github.com/f/awesome-chatgpt-prompts"
|
||||
category: "developer"
|
||||
|
||||
- key: senior_frontend_developer
|
||||
value: "I want you to act as a Senior Frontend developer. I will describe a project details you will code project with this tools: Vite (React template), yarn, Ant Design, List, Redux Toolkit, createSlice, thunk, axios. You should merge files in single index.js file and nothing else. Do not write explanations."
|
||||
author: "awesome-chatgpt-prompts"
|
||||
link: "https://github.com/f/awesome-chatgpt-prompts"
|
||||
category: "developer"
|
||||
|
||||
- key: code_reviewer
|
||||
value: "I want you to act as a Code reviewer who is experienced developer in the given code language. I will provide you with the code block or methods or code file along with the code language name, and I would like you to review the code and share the feedback, suggestions and alternative recommended approaches. Please write explanations behind the feedback or suggestions or alternative approaches."
|
||||
author: "awesome-chatgpt-prompts"
|
||||
link: "https://github.com/f/awesome-chatgpt-prompts"
|
||||
category: "developer"
|
||||
|
||||
- key: machine_learning_engineer
|
||||
value: "I want you to act as a machine learning engineer. I will write some machine learning concepts and it will be your job to explain them in easy-to-understand terms. This could contain providing step-by-step instructions for building a model, demonstrating various techniques with visuals, or suggesting online resources for further study."
|
||||
author: "awesome-chatgpt-prompts"
|
||||
link: "https://github.com/f/awesome-chatgpt-prompts"
|
||||
category: "developer"
|
||||
|
||||
- key: sql_terminal
|
||||
value: "I want you to act as a SQL terminal in front of an example database. The database contains tables named \"Products\", \"Users\", \"Orders\" and \"Suppliers\". I will type queries and you will reply with what the terminal would show. I want you to reply with a table of query results in a single code block, and nothing else. Do not write explanations. Do not type commands unless I instruct you to do so. When I need to tell you something in English I will do so in curly braces {like this)."
|
||||
author: "awesome-chatgpt-prompts"
|
||||
link: "https://github.com/f/awesome-chatgpt-prompts"
|
||||
category: "developer"
|
||||
|
||||
- key: python_interpreter
|
||||
value: "Act as a Python interpreter. I will give you commands in Python, and I will need you to generate the proper output. Only say the output. But if there is none, say nothing, and don't give me an explanation. If I need to say something, I will do so through comments."
|
||||
author: "awesome-chatgpt-prompts"
|
||||
link: "https://github.com/f/awesome-chatgpt-prompts"
|
||||
category: "developer"
|
||||
|
||||
- key: devops_engineer
|
||||
value: "You are a Senior DevOps engineer working at a Big Company. Your role is to provide scalable, efficient, and automated solutions for software deployment, infrastructure management, and CI/CD pipelines. Suggest the best DevOps practices, including infrastructure setup, deployment strategies, automation tools, and cost-effective scaling solutions."
|
||||
author: "awesome-chatgpt-prompts"
|
||||
link: "https://github.com/f/awesome-chatgpt-prompts"
|
||||
category: "developer"
|
||||
|
||||
- key: cyber_security_specialist
|
||||
value: "I want you to act as a cyber security specialist. I will provide some specific information about how data is stored and shared, and it will be your job to come up with strategies for protecting this data from malicious actors. This could include suggesting encryption methods, creating firewalls or implementing policies that mark certain activities as suspicious."
|
||||
author: "awesome-chatgpt-prompts"
|
||||
link: "https://github.com/f/awesome-chatgpt-prompts"
|
||||
category: "developer"
|
||||
|
||||
# General productivity prompts
|
||||
- key: english_translator
|
||||
value: "I want you to act as an English translator, spelling corrector and improver. I will speak to you in any language and you will detect the language, translate it and answer in the corrected and improved version of my text, in English. I want you to replace my simplified A0-level words and sentences with more beautiful and elegant, upper level English words and sentences. Keep the meaning same, but make them more literary. I want you to only reply the correction, the improvements and nothing else, do not write explanations."
|
||||
author: "awesome-chatgpt-prompts"
|
||||
link: "https://github.com/f/awesome-chatgpt-prompts"
|
||||
category: "general"
|
||||
|
||||
- key: proofreader
|
||||
value: "I want you act as a proofreader. I will provide you texts and I would like you to review them for any spelling, grammar, or punctuation errors. Once you have finished reviewing the text, provide me with any necessary corrections or suggestions for improve the text."
|
||||
author: "awesome-chatgpt-prompts"
|
||||
link: "https://github.com/f/awesome-chatgpt-prompts"
|
||||
category: "general"
|
||||
|
||||
- key: note_taking_assistant
|
||||
value: "I want you to act as a note-taking assistant for a lecture. Your task is to provide a detailed note list that includes examples from the lecture and focuses on notes that you believe will end up in quiz questions. Additionally, please make a separate list for notes that have numbers and data in them and another separated list for the examples that included in this lecture. The notes should be concise and easy to read."
|
||||
author: "awesome-chatgpt-prompts"
|
||||
link: "https://github.com/f/awesome-chatgpt-prompts"
|
||||
category: "general"
|
||||
|
||||
- key: essay_writer
|
||||
value: "I want you to act as an essay writer. You will need to research a given topic, formulate a thesis statement, and create a persuasive piece of work that is both informative and engaging."
|
||||
author: "awesome-chatgpt-prompts"
|
||||
link: "https://github.com/f/awesome-chatgpt-prompts"
|
||||
category: "general"
|
||||
|
||||
- key: career_counselor
|
||||
value: "I want you to act as a career counselor. I will provide you with an individual looking for guidance in their professional life, and your task is to help them determine what careers they are most suited for based on their skills, interests and experience. You should also conduct research into the various options available, explain the job market trends in different industries and advice on which qualifications would be beneficial for pursuing particular fields."
|
||||
author: "awesome-chatgpt-prompts"
|
||||
link: "https://github.com/f/awesome-chatgpt-prompts"
|
||||
category: "general"
|
||||
|
||||
- key: life_coach
|
||||
value: "I want you to act as a life coach. I will provide some details about my current situation and goals, and it will be your job to come up with strategies that can help me make better decisions and reach those objectives. This could involve offering advice on various topics, such as creating plans for achieving success or dealing with difficult emotions."
|
||||
author: "awesome-chatgpt-prompts"
|
||||
link: "https://github.com/f/awesome-chatgpt-prompts"
|
||||
category: "general"
|
||||
|
||||
- key: motivational_coach
|
||||
value: "I want you to act as a motivational coach. I will provide you with some information about someone's goals and challenges, and it will be your job to come up with strategies that can help this person achieve their goals. This could involve providing positive affirmations, giving helpful advice or suggesting activities they can do to reach their end goal."
|
||||
author: "awesome-chatgpt-prompts"
|
||||
link: "https://github.com/f/awesome-chatgpt-prompts"
|
||||
category: "general"
|
||||
|
||||
- key: travel_guide
|
||||
value: "I want you to act as a travel guide. I will write you my location and you will suggest a place to visit near my location. In some cases, I will also give you the type of places I will visit. You will also suggest me places of similar type that are close to my first location."
|
||||
author: "awesome-chatgpt-prompts"
|
||||
link: "https://github.com/f/awesome-chatgpt-prompts"
|
||||
category: "general"
|
||||
|
||||
# Creative prompts
|
||||
- key: storyteller
|
||||
value: "I want you to act as a storyteller. You will come up with entertaining stories that are engaging, imaginative and captivating for the audience. It can be fairy tales, educational stories or any other type of stories which has the potential to capture people's attention and imagination. Depending on the target audience, you may choose specific themes or topics for your storytelling session e.g., if it's children then you can talk about animals; If it's adults then history-based tales might engage them better etc."
|
||||
author: "awesome-chatgpt-prompts"
|
||||
link: "https://github.com/f/awesome-chatgpt-prompts"
|
||||
category: "creative"
|
||||
|
||||
- key: screenwriter
|
||||
value: "I want you to act as a screenwriter. You will develop an engaging and creative script for either a feature length film, or a Web Series that can captivate its viewers. Start with coming up with interesting characters, the setting of the story, dialogues between the characters etc. Once your character development is complete - create an exciting storyline filled with twists and turns that keeps the viewers in suspense until the end."
|
||||
author: "awesome-chatgpt-prompts"
|
||||
link: "https://github.com/f/awesome-chatgpt-prompts"
|
||||
category: "creative"
|
||||
|
||||
- key: novelist
|
||||
value: "I want you to act as a novelist. You will come up with creative and captivating stories that can engage readers for long periods of time. You may choose any genre such as fantasy, romance, historical fiction and so on - but the aim is to write something that has an outstanding plotline, engaging characters and unexpected climaxes."
|
||||
author: "awesome-chatgpt-prompts"
|
||||
link: "https://github.com/f/awesome-chatgpt-prompts"
|
||||
category: "creative"
|
||||
|
||||
- key: poet
|
||||
value: "I want you to act as a poet. You will create poems that evoke emotions and have the power to stir people's soul. Write on any topic or theme but make sure your words convey the feeling you are trying to express in beautiful yet meaningful ways. You can also come up with short verses that are still powerful enough to leave an imprint in readers' minds."
|
||||
author: "awesome-chatgpt-prompts"
|
||||
link: "https://github.com/f/awesome-chatgpt-prompts"
|
||||
category: "creative"
|
||||
|
||||
- key: rapper
|
||||
value: "I want you to act as a rapper. You will come up with powerful and meaningful lyrics, beats and rhythm that can 'wow' the audience. Your lyrics should have an intriguing meaning and message which people can relate too. When it comes to choosing your beat, make sure it is catchy yet relevant to your words, so that when combined they make an explosion of sound everytime!"
|
||||
author: "awesome-chatgpt-prompts"
|
||||
link: "https://github.com/f/awesome-chatgpt-prompts"
|
||||
category: "creative"
|
||||
|
||||
- key: composer
|
||||
value: "I want you to act as a composer. I will provide the lyrics to a song and you will create music for it. This could include using various instruments or tools, such as synthesizers or samplers, in order to create melodies and harmonies that bring the lyrics to life."
|
||||
author: "awesome-chatgpt-prompts"
|
||||
link: "https://github.com/f/awesome-chatgpt-prompts"
|
||||
category: "creative"
|
||||
|
||||
# Educational prompts
|
||||
- key: math_teacher
|
||||
value: "I want you to act as a math teacher. I will provide some mathematical equations or concepts, and it will be your job to explain them in easy-to-understand terms. This could include providing step-by-step instructions for solving a problem, demonstrating various techniques with visuals or suggesting online resources for further study."
|
||||
author: "awesome-chatgpt-prompts"
|
||||
link: "https://github.com/f/awesome-chatgpt-prompts"
|
||||
category: "educational"
|
||||
|
||||
- key: philosophy_teacher
|
||||
value: "I want you to act as a philosophy teacher. I will provide some topics related to the study of philosophy, and it will be your job to explain these concepts in an easy-to-understand manner. This could include providing examples, posing questions or breaking down complex ideas into smaller pieces that are easier to comprehend."
|
||||
author: "awesome-chatgpt-prompts"
|
||||
link: "https://github.com/f/awesome-chatgpt-prompts"
|
||||
category: "educational"
|
||||
|
||||
- key: historian
|
||||
value: "I want you to act as a historian. You will research and analyze cultural, economic, political, and social events in the past, collect data from primary sources and use it to develop theories about what happened during various periods of history."
|
||||
author: "awesome-chatgpt-prompts"
|
||||
link: "https://github.com/f/awesome-chatgpt-prompts"
|
||||
category: "educational"
|
||||
|
||||
- key: debater
|
||||
value: "I want you to act as a debater. I will provide you with some topics related to current events and your task is to research both sides of the debates, present valid arguments for each side, refute opposing points of view, and draw persuasive conclusions based on evidence. Your goal is to help people come away from the discussion with increased knowledge and insight into the topic at hand."
|
||||
author: "awesome-chatgpt-prompts"
|
||||
link: "https://github.com/f/awesome-chatgpt-prompts"
|
||||
category: "educational"
|
||||
|
||||
- key: explainer_with_analogies
|
||||
value: "I want you to act as an explainer who uses analogies to clarify complex topics. When I give you a subject (technical, philosophical or scientific), you'll follow this structure: 1. Ask me 1-2 quick questions to assess my current level of understanding. 2. Based on my answer, create three analogies to explain the topic: one that a 10-year-old would understand, one for a high-school student, and one for a college-level person. 3. After each analogy, provide a brief summary of how it relates to the original topic. 4. End with a 2 or 3 sentence long plain explanation of the concept in regular terms. Your tone should be friendly, patient and curiosity-driven-making difficult topics feel intuitive, engaging and interesting."
|
||||
author: "awesome-chatgpt-prompts"
|
||||
link: "https://github.com/f/awesome-chatgpt-prompts"
|
||||
category: "educational"
|
||||
|
|
@ -11,10 +11,10 @@ from .google_calendar_add_connector_route import (
|
|||
from .google_gmail_add_connector_route import (
|
||||
router as google_gmail_add_connector_router,
|
||||
)
|
||||
from .llm_config_routes import router as llm_config_router
|
||||
from .logs_routes import router as logs_router
|
||||
from .luma_add_connector_route import router as luma_add_connector_router
|
||||
from .new_chat_routes import router as new_chat_router
|
||||
from .new_llm_config_routes import router as new_llm_config_router
|
||||
from .notes_routes import router as notes_router
|
||||
from .podcasts_routes import router as podcasts_router
|
||||
from .rbac_routes import router as rbac_router
|
||||
|
|
@ -35,5 +35,5 @@ router.include_router(google_calendar_add_connector_router)
|
|||
router.include_router(google_gmail_add_connector_router)
|
||||
router.include_router(airtable_add_connector_router)
|
||||
router.include_router(luma_add_connector_router)
|
||||
router.include_router(llm_config_router)
|
||||
router.include_router(new_llm_config_router) # LLM configs with prompt configuration
|
||||
router.include_router(logs_router)
|
||||
|
|
|
|||
|
|
@ -1,576 +0,0 @@
|
|||
import logging
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from pydantic import BaseModel
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.future import select
|
||||
|
||||
from app.config import config
|
||||
from app.db import (
|
||||
LLMConfig,
|
||||
Permission,
|
||||
SearchSpace,
|
||||
User,
|
||||
get_async_session,
|
||||
)
|
||||
from app.schemas import LLMConfigCreate, LLMConfigRead, LLMConfigUpdate
|
||||
from app.services.llm_service import validate_llm_config
|
||||
from app.users import current_active_user
|
||||
from app.utils.rbac import check_permission
|
||||
|
||||
router = APIRouter()
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class LLMPreferencesUpdate(BaseModel):
|
||||
"""Schema for updating search space LLM preferences"""
|
||||
|
||||
long_context_llm_id: int | None = None
|
||||
fast_llm_id: int | None = None
|
||||
strategic_llm_id: int | None = None
|
||||
|
||||
|
||||
class LLMPreferencesRead(BaseModel):
|
||||
"""Schema for reading search space LLM preferences"""
|
||||
|
||||
long_context_llm_id: int | None = None
|
||||
fast_llm_id: int | None = None
|
||||
strategic_llm_id: int | None = None
|
||||
long_context_llm: LLMConfigRead | None = None
|
||||
fast_llm: LLMConfigRead | None = None
|
||||
strategic_llm: LLMConfigRead | None = None
|
||||
|
||||
|
||||
class GlobalLLMConfigRead(BaseModel):
|
||||
"""Schema for reading global LLM configs (without API key)"""
|
||||
|
||||
id: int
|
||||
name: str
|
||||
provider: str
|
||||
custom_provider: str | None = None
|
||||
model_name: str
|
||||
api_base: str | None = None
|
||||
language: str | None = None
|
||||
litellm_params: dict | None = None
|
||||
is_global: bool = True
|
||||
|
||||
|
||||
# Global LLM Config endpoints
|
||||
|
||||
|
||||
@router.get("/global-llm-configs", response_model=list[GlobalLLMConfigRead])
|
||||
async def get_global_llm_configs(
|
||||
user: User = Depends(current_active_user),
|
||||
):
|
||||
"""
|
||||
Get all available global LLM configurations.
|
||||
These are pre-configured by the system administrator and available to all users.
|
||||
API keys are not exposed through this endpoint.
|
||||
"""
|
||||
try:
|
||||
global_configs = config.GLOBAL_LLM_CONFIGS
|
||||
|
||||
# Remove API keys from response
|
||||
safe_configs = []
|
||||
for cfg in global_configs:
|
||||
safe_config = {
|
||||
"id": cfg.get("id"),
|
||||
"name": cfg.get("name"),
|
||||
"provider": cfg.get("provider"),
|
||||
"custom_provider": cfg.get("custom_provider"),
|
||||
"model_name": cfg.get("model_name"),
|
||||
"api_base": cfg.get("api_base"),
|
||||
"language": cfg.get("language"),
|
||||
"litellm_params": cfg.get("litellm_params", {}),
|
||||
"is_global": True,
|
||||
}
|
||||
safe_configs.append(safe_config)
|
||||
|
||||
return safe_configs
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=500, detail=f"Failed to fetch global LLM configs: {e!s}"
|
||||
) from e
|
||||
|
||||
|
||||
@router.post("/llm-configs", response_model=LLMConfigRead)
|
||||
async def create_llm_config(
|
||||
llm_config: LLMConfigCreate,
|
||||
session: AsyncSession = Depends(get_async_session),
|
||||
user: User = Depends(current_active_user),
|
||||
):
|
||||
"""
|
||||
Create a new LLM configuration for a search space.
|
||||
Requires LLM_CONFIGS_CREATE permission.
|
||||
"""
|
||||
try:
|
||||
# Verify user has permission to create LLM configs
|
||||
await check_permission(
|
||||
session,
|
||||
user,
|
||||
llm_config.search_space_id,
|
||||
Permission.LLM_CONFIGS_CREATE.value,
|
||||
"You don't have permission to create LLM configurations in this search space",
|
||||
)
|
||||
|
||||
# Validate the LLM configuration by making a test API call
|
||||
is_valid, error_message = await validate_llm_config(
|
||||
provider=llm_config.provider.value,
|
||||
model_name=llm_config.model_name,
|
||||
api_key=llm_config.api_key,
|
||||
api_base=llm_config.api_base,
|
||||
custom_provider=llm_config.custom_provider,
|
||||
litellm_params=llm_config.litellm_params,
|
||||
)
|
||||
|
||||
if not is_valid:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"Invalid LLM configuration: {error_message}",
|
||||
)
|
||||
|
||||
db_llm_config = LLMConfig(**llm_config.model_dump())
|
||||
session.add(db_llm_config)
|
||||
await session.commit()
|
||||
await session.refresh(db_llm_config)
|
||||
return db_llm_config
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
await session.rollback()
|
||||
raise HTTPException(
|
||||
status_code=500, detail=f"Failed to create LLM configuration: {e!s}"
|
||||
) from e
|
||||
|
||||
|
||||
@router.get("/llm-configs", response_model=list[LLMConfigRead])
|
||||
async def read_llm_configs(
|
||||
search_space_id: int,
|
||||
skip: int = 0,
|
||||
limit: int = 200,
|
||||
session: AsyncSession = Depends(get_async_session),
|
||||
user: User = Depends(current_active_user),
|
||||
):
|
||||
"""
|
||||
Get all LLM configurations for a search space.
|
||||
Requires LLM_CONFIGS_READ permission.
|
||||
"""
|
||||
try:
|
||||
# Verify user has permission to read LLM configs
|
||||
await check_permission(
|
||||
session,
|
||||
user,
|
||||
search_space_id,
|
||||
Permission.LLM_CONFIGS_READ.value,
|
||||
"You don't have permission to view LLM configurations in this search space",
|
||||
)
|
||||
|
||||
result = await session.execute(
|
||||
select(LLMConfig)
|
||||
.filter(LLMConfig.search_space_id == search_space_id)
|
||||
.offset(skip)
|
||||
.limit(limit)
|
||||
)
|
||||
return result.scalars().all()
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=500, detail=f"Failed to fetch LLM configurations: {e!s}"
|
||||
) from e
|
||||
|
||||
|
||||
@router.get("/llm-configs/{llm_config_id}", response_model=LLMConfigRead)
|
||||
async def read_llm_config(
|
||||
llm_config_id: int,
|
||||
session: AsyncSession = Depends(get_async_session),
|
||||
user: User = Depends(current_active_user),
|
||||
):
|
||||
"""
|
||||
Get a specific LLM configuration by ID.
|
||||
Requires LLM_CONFIGS_READ permission.
|
||||
"""
|
||||
try:
|
||||
# Get the LLM config
|
||||
result = await session.execute(
|
||||
select(LLMConfig).filter(LLMConfig.id == llm_config_id)
|
||||
)
|
||||
llm_config = result.scalars().first()
|
||||
|
||||
if not llm_config:
|
||||
raise HTTPException(status_code=404, detail="LLM configuration not found")
|
||||
|
||||
# Verify user has permission to read LLM configs
|
||||
await check_permission(
|
||||
session,
|
||||
user,
|
||||
llm_config.search_space_id,
|
||||
Permission.LLM_CONFIGS_READ.value,
|
||||
"You don't have permission to view LLM configurations in this search space",
|
||||
)
|
||||
|
||||
return llm_config
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=500, detail=f"Failed to fetch LLM configuration: {e!s}"
|
||||
) from e
|
||||
|
||||
|
||||
@router.put("/llm-configs/{llm_config_id}", response_model=LLMConfigRead)
|
||||
async def update_llm_config(
|
||||
llm_config_id: int,
|
||||
llm_config_update: LLMConfigUpdate,
|
||||
session: AsyncSession = Depends(get_async_session),
|
||||
user: User = Depends(current_active_user),
|
||||
):
|
||||
"""
|
||||
Update an existing LLM configuration.
|
||||
Requires LLM_CONFIGS_UPDATE permission.
|
||||
"""
|
||||
try:
|
||||
# Get the LLM config
|
||||
result = await session.execute(
|
||||
select(LLMConfig).filter(LLMConfig.id == llm_config_id)
|
||||
)
|
||||
db_llm_config = result.scalars().first()
|
||||
|
||||
if not db_llm_config:
|
||||
raise HTTPException(status_code=404, detail="LLM configuration not found")
|
||||
|
||||
# Verify user has permission to update LLM configs
|
||||
await check_permission(
|
||||
session,
|
||||
user,
|
||||
db_llm_config.search_space_id,
|
||||
Permission.LLM_CONFIGS_UPDATE.value,
|
||||
"You don't have permission to update LLM configurations in this search space",
|
||||
)
|
||||
|
||||
update_data = llm_config_update.model_dump(exclude_unset=True)
|
||||
|
||||
# Apply updates to a temporary copy for validation
|
||||
temp_config = {
|
||||
"provider": update_data.get("provider", db_llm_config.provider.value),
|
||||
"model_name": update_data.get("model_name", db_llm_config.model_name),
|
||||
"api_key": update_data.get("api_key", db_llm_config.api_key),
|
||||
"api_base": update_data.get("api_base", db_llm_config.api_base),
|
||||
"custom_provider": update_data.get(
|
||||
"custom_provider", db_llm_config.custom_provider
|
||||
),
|
||||
"litellm_params": update_data.get(
|
||||
"litellm_params", db_llm_config.litellm_params
|
||||
),
|
||||
}
|
||||
|
||||
# Validate the updated configuration
|
||||
is_valid, error_message = await validate_llm_config(
|
||||
provider=temp_config["provider"],
|
||||
model_name=temp_config["model_name"],
|
||||
api_key=temp_config["api_key"],
|
||||
api_base=temp_config["api_base"],
|
||||
custom_provider=temp_config["custom_provider"],
|
||||
litellm_params=temp_config["litellm_params"],
|
||||
)
|
||||
|
||||
if not is_valid:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"Invalid LLM configuration: {error_message}",
|
||||
)
|
||||
|
||||
# Apply updates to the database object
|
||||
for key, value in update_data.items():
|
||||
setattr(db_llm_config, key, value)
|
||||
|
||||
await session.commit()
|
||||
await session.refresh(db_llm_config)
|
||||
return db_llm_config
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
await session.rollback()
|
||||
raise HTTPException(
|
||||
status_code=500, detail=f"Failed to update LLM configuration: {e!s}"
|
||||
) from e
|
||||
|
||||
|
||||
@router.delete("/llm-configs/{llm_config_id}", response_model=dict)
|
||||
async def delete_llm_config(
|
||||
llm_config_id: int,
|
||||
session: AsyncSession = Depends(get_async_session),
|
||||
user: User = Depends(current_active_user),
|
||||
):
|
||||
"""
|
||||
Delete an LLM configuration.
|
||||
Requires LLM_CONFIGS_DELETE permission.
|
||||
"""
|
||||
try:
|
||||
# Get the LLM config
|
||||
result = await session.execute(
|
||||
select(LLMConfig).filter(LLMConfig.id == llm_config_id)
|
||||
)
|
||||
db_llm_config = result.scalars().first()
|
||||
|
||||
if not db_llm_config:
|
||||
raise HTTPException(status_code=404, detail="LLM configuration not found")
|
||||
|
||||
# Verify user has permission to delete LLM configs
|
||||
await check_permission(
|
||||
session,
|
||||
user,
|
||||
db_llm_config.search_space_id,
|
||||
Permission.LLM_CONFIGS_DELETE.value,
|
||||
"You don't have permission to delete LLM configurations in this search space",
|
||||
)
|
||||
|
||||
await session.delete(db_llm_config)
|
||||
await session.commit()
|
||||
return {"message": "LLM configuration deleted successfully"}
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
await session.rollback()
|
||||
raise HTTPException(
|
||||
status_code=500, detail=f"Failed to delete LLM configuration: {e!s}"
|
||||
) from e
|
||||
|
||||
|
||||
# Search Space LLM Preferences endpoints
|
||||
|
||||
|
||||
@router.get(
|
||||
"/search-spaces/{search_space_id}/llm-preferences",
|
||||
response_model=LLMPreferencesRead,
|
||||
)
|
||||
async def get_llm_preferences(
|
||||
search_space_id: int,
|
||||
session: AsyncSession = Depends(get_async_session),
|
||||
user: User = Depends(current_active_user),
|
||||
):
|
||||
"""
|
||||
Get the LLM preferences for a specific search space.
|
||||
LLM preferences are shared by all members of the search space.
|
||||
Requires LLM_CONFIGS_READ permission.
|
||||
"""
|
||||
try:
|
||||
# Verify user has permission to read LLM configs
|
||||
await check_permission(
|
||||
session,
|
||||
user,
|
||||
search_space_id,
|
||||
Permission.LLM_CONFIGS_READ.value,
|
||||
"You don't have permission to view LLM preferences in this search space",
|
||||
)
|
||||
|
||||
# Get the search space
|
||||
result = await session.execute(
|
||||
select(SearchSpace).filter(SearchSpace.id == search_space_id)
|
||||
)
|
||||
search_space = result.scalars().first()
|
||||
|
||||
if not search_space:
|
||||
raise HTTPException(status_code=404, detail="Search space not found")
|
||||
|
||||
# Helper function to get config (global or custom)
|
||||
async def get_config_for_id(config_id):
|
||||
if config_id is None:
|
||||
return None
|
||||
|
||||
# Check if it's a global config (negative ID)
|
||||
if config_id < 0:
|
||||
for cfg in config.GLOBAL_LLM_CONFIGS:
|
||||
if cfg.get("id") == config_id:
|
||||
# Return as LLMConfigRead-compatible dict
|
||||
return {
|
||||
"id": cfg.get("id"),
|
||||
"name": cfg.get("name"),
|
||||
"provider": cfg.get("provider"),
|
||||
"custom_provider": cfg.get("custom_provider"),
|
||||
"model_name": cfg.get("model_name"),
|
||||
"api_key": "***GLOBAL***", # Don't expose the actual key
|
||||
"api_base": cfg.get("api_base"),
|
||||
"language": cfg.get("language"),
|
||||
"litellm_params": cfg.get("litellm_params"),
|
||||
"created_at": None,
|
||||
"search_space_id": search_space_id,
|
||||
}
|
||||
return None
|
||||
|
||||
# It's a custom config, fetch from database
|
||||
result = await session.execute(
|
||||
select(LLMConfig).filter(LLMConfig.id == config_id)
|
||||
)
|
||||
return result.scalars().first()
|
||||
|
||||
# Get the configs (from DB for custom, or constructed for global)
|
||||
long_context_llm = await get_config_for_id(search_space.long_context_llm_id)
|
||||
fast_llm = await get_config_for_id(search_space.fast_llm_id)
|
||||
strategic_llm = await get_config_for_id(search_space.strategic_llm_id)
|
||||
|
||||
return {
|
||||
"long_context_llm_id": search_space.long_context_llm_id,
|
||||
"fast_llm_id": search_space.fast_llm_id,
|
||||
"strategic_llm_id": search_space.strategic_llm_id,
|
||||
"long_context_llm": long_context_llm,
|
||||
"fast_llm": fast_llm,
|
||||
"strategic_llm": strategic_llm,
|
||||
}
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=500, detail=f"Failed to fetch LLM preferences: {e!s}"
|
||||
) from e
|
||||
|
||||
|
||||
@router.put(
|
||||
"/search-spaces/{search_space_id}/llm-preferences",
|
||||
response_model=LLMPreferencesRead,
|
||||
)
|
||||
async def update_llm_preferences(
|
||||
search_space_id: int,
|
||||
preferences: LLMPreferencesUpdate,
|
||||
session: AsyncSession = Depends(get_async_session),
|
||||
user: User = Depends(current_active_user),
|
||||
):
|
||||
"""
|
||||
Update the LLM preferences for a specific search space.
|
||||
LLM preferences are shared by all members of the search space.
|
||||
Requires SETTINGS_UPDATE permission (only users with settings access can change).
|
||||
"""
|
||||
try:
|
||||
# Verify user has permission to update settings (not just LLM configs)
|
||||
# This ensures only users with settings access can change shared LLM preferences
|
||||
await check_permission(
|
||||
session,
|
||||
user,
|
||||
search_space_id,
|
||||
Permission.SETTINGS_UPDATE.value,
|
||||
"You don't have permission to update LLM preferences in this search space",
|
||||
)
|
||||
|
||||
# Get the search space
|
||||
result = await session.execute(
|
||||
select(SearchSpace).filter(SearchSpace.id == search_space_id)
|
||||
)
|
||||
search_space = result.scalars().first()
|
||||
|
||||
if not search_space:
|
||||
raise HTTPException(status_code=404, detail="Search space not found")
|
||||
|
||||
# Validate that all provided LLM config IDs belong to the search space
|
||||
update_data = preferences.model_dump(exclude_unset=True)
|
||||
|
||||
# Store language from configs to validate consistency
|
||||
languages = set()
|
||||
|
||||
for _key, llm_config_id in update_data.items():
|
||||
if llm_config_id is not None:
|
||||
# Check if this is a global config (negative ID)
|
||||
if llm_config_id < 0:
|
||||
# Validate global config exists
|
||||
global_config = None
|
||||
for cfg in config.GLOBAL_LLM_CONFIGS:
|
||||
if cfg.get("id") == llm_config_id:
|
||||
global_config = cfg
|
||||
break
|
||||
|
||||
if not global_config:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail=f"Global LLM configuration {llm_config_id} not found",
|
||||
)
|
||||
|
||||
# Collect language for consistency check (if explicitly set)
|
||||
lang = global_config.get("language")
|
||||
if lang and lang.strip(): # Only add non-empty languages
|
||||
languages.add(lang.strip())
|
||||
else:
|
||||
# Verify the LLM config belongs to the search space (custom config)
|
||||
result = await session.execute(
|
||||
select(LLMConfig).filter(
|
||||
LLMConfig.id == llm_config_id,
|
||||
LLMConfig.search_space_id == search_space_id,
|
||||
)
|
||||
)
|
||||
llm_config = result.scalars().first()
|
||||
if not llm_config:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail=f"LLM configuration {llm_config_id} not found in this search space",
|
||||
)
|
||||
|
||||
# Collect language for consistency check (if explicitly set)
|
||||
if llm_config.language and llm_config.language.strip():
|
||||
languages.add(llm_config.language.strip())
|
||||
|
||||
# Language consistency check - only warn if there are multiple explicit languages
|
||||
# Allow mixing configs with and without language settings
|
||||
if len(languages) > 1:
|
||||
# Log warning but allow the operation
|
||||
logger.warning(
|
||||
f"Multiple languages detected in LLM selection for search_space {search_space_id}: {languages}. "
|
||||
"This may affect response quality."
|
||||
)
|
||||
|
||||
# Update search space LLM preferences
|
||||
for key, value in update_data.items():
|
||||
setattr(search_space, key, value)
|
||||
|
||||
await session.commit()
|
||||
await session.refresh(search_space)
|
||||
|
||||
# Helper function to get config (global or custom)
|
||||
async def get_config_for_id(config_id):
|
||||
if config_id is None:
|
||||
return None
|
||||
|
||||
# Check if it's a global config (negative ID)
|
||||
if config_id < 0:
|
||||
for cfg in config.GLOBAL_LLM_CONFIGS:
|
||||
if cfg.get("id") == config_id:
|
||||
# Return as LLMConfigRead-compatible dict
|
||||
return {
|
||||
"id": cfg.get("id"),
|
||||
"name": cfg.get("name"),
|
||||
"provider": cfg.get("provider"),
|
||||
"custom_provider": cfg.get("custom_provider"),
|
||||
"model_name": cfg.get("model_name"),
|
||||
"api_key": "***GLOBAL***", # Don't expose the actual key
|
||||
"api_base": cfg.get("api_base"),
|
||||
"language": cfg.get("language"),
|
||||
"litellm_params": cfg.get("litellm_params"),
|
||||
"created_at": None,
|
||||
"search_space_id": search_space_id,
|
||||
}
|
||||
return None
|
||||
|
||||
# It's a custom config, fetch from database
|
||||
result = await session.execute(
|
||||
select(LLMConfig).filter(LLMConfig.id == config_id)
|
||||
)
|
||||
return result.scalars().first()
|
||||
|
||||
# Get the configs (from DB for custom, or constructed for global)
|
||||
long_context_llm = await get_config_for_id(search_space.long_context_llm_id)
|
||||
fast_llm = await get_config_for_id(search_space.fast_llm_id)
|
||||
strategic_llm = await get_config_for_id(search_space.strategic_llm_id)
|
||||
|
||||
# Return updated preferences
|
||||
return {
|
||||
"long_context_llm_id": search_space.long_context_llm_id,
|
||||
"fast_llm_id": search_space.fast_llm_id,
|
||||
"strategic_llm_id": search_space.strategic_llm_id,
|
||||
"long_context_llm": long_context_llm,
|
||||
"fast_llm": fast_llm,
|
||||
"strategic_llm": strategic_llm,
|
||||
}
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
await session.rollback()
|
||||
raise HTTPException(
|
||||
status_code=500, detail=f"Failed to update LLM preferences: {e!s}"
|
||||
) from e
|
||||
|
|
@ -685,8 +685,16 @@ async def handle_new_chat(
|
|||
)
|
||||
search_space = search_space_result.scalars().first()
|
||||
|
||||
# TODO: Add new llm config arch then complete this
|
||||
llm_config_id = -1
|
||||
if not search_space:
|
||||
raise HTTPException(status_code=404, detail="Search space not found")
|
||||
|
||||
# Use agent_llm_id from search space for chat operations
|
||||
# Positive IDs load from NewLLMConfig database table
|
||||
# Negative IDs load from YAML global configs
|
||||
# Falls back to -1 (first global config) if not configured
|
||||
llm_config_id = (
|
||||
search_space.agent_llm_id if search_space.agent_llm_id is not None else -1
|
||||
)
|
||||
|
||||
# Return streaming response
|
||||
return StreamingResponse(
|
||||
|
|
@ -696,7 +704,6 @@ async def handle_new_chat(
|
|||
chat_id=request.chat_id,
|
||||
session=session,
|
||||
llm_config_id=llm_config_id,
|
||||
messages=request.messages,
|
||||
attachments=request.attachments,
|
||||
),
|
||||
media_type="text/event-stream",
|
||||
|
|
|
|||
376
surfsense_backend/app/routes/new_llm_config_routes.py
Normal file
376
surfsense_backend/app/routes/new_llm_config_routes.py
Normal file
|
|
@ -0,0 +1,376 @@
|
|||
"""
|
||||
API routes for NewLLMConfig CRUD operations.
|
||||
|
||||
NewLLMConfig combines LLM model settings with prompt configuration:
|
||||
- LLM provider, model, API key, etc.
|
||||
- Configurable system instructions
|
||||
- Citation toggle
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.future import select
|
||||
|
||||
from app.agents.new_chat.system_prompt import get_default_system_instructions
|
||||
from app.config import config
|
||||
from app.db import (
|
||||
NewLLMConfig,
|
||||
Permission,
|
||||
User,
|
||||
get_async_session,
|
||||
)
|
||||
from app.schemas import (
|
||||
DefaultSystemInstructionsResponse,
|
||||
GlobalNewLLMConfigRead,
|
||||
NewLLMConfigCreate,
|
||||
NewLLMConfigRead,
|
||||
NewLLMConfigUpdate,
|
||||
)
|
||||
from app.services.llm_service import validate_llm_config
|
||||
from app.users import current_active_user
|
||||
from app.utils.rbac import check_permission
|
||||
|
||||
router = APIRouter()
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Global Configs Routes
|
||||
# =============================================================================
|
||||
|
||||
|
||||
@router.get("/global-new-llm-configs", response_model=list[GlobalNewLLMConfigRead])
|
||||
async def get_global_new_llm_configs(
|
||||
user: User = Depends(current_active_user),
|
||||
):
|
||||
"""
|
||||
Get all available global NewLLMConfig configurations.
|
||||
These are pre-configured by the system administrator and available to all users.
|
||||
API keys are not exposed through this endpoint.
|
||||
|
||||
Global configs have negative IDs to distinguish from user-created configs.
|
||||
"""
|
||||
try:
|
||||
global_configs = config.GLOBAL_LLM_CONFIGS
|
||||
|
||||
# Transform to new structure, hiding API keys
|
||||
safe_configs = []
|
||||
for cfg in global_configs:
|
||||
safe_config = {
|
||||
"id": cfg.get("id"),
|
||||
"name": cfg.get("name"),
|
||||
"description": cfg.get("description"),
|
||||
"provider": cfg.get("provider"),
|
||||
"custom_provider": cfg.get("custom_provider"),
|
||||
"model_name": cfg.get("model_name"),
|
||||
"api_base": cfg.get("api_base") or None,
|
||||
"litellm_params": cfg.get("litellm_params", {}),
|
||||
# New prompt configuration fields
|
||||
"system_instructions": cfg.get("system_instructions", ""),
|
||||
"use_default_system_instructions": cfg.get(
|
||||
"use_default_system_instructions", True
|
||||
),
|
||||
"citations_enabled": cfg.get("citations_enabled", True),
|
||||
"is_global": True,
|
||||
}
|
||||
safe_configs.append(safe_config)
|
||||
|
||||
return safe_configs
|
||||
except Exception as e:
|
||||
logger.exception("Failed to fetch global NewLLMConfigs")
|
||||
raise HTTPException(
|
||||
status_code=500, detail=f"Failed to fetch global configurations: {e!s}"
|
||||
) from e
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# CRUD Routes
|
||||
# =============================================================================
|
||||
|
||||
|
||||
@router.post("/new-llm-configs", response_model=NewLLMConfigRead)
|
||||
async def create_new_llm_config(
|
||||
config_data: NewLLMConfigCreate,
|
||||
session: AsyncSession = Depends(get_async_session),
|
||||
user: User = Depends(current_active_user),
|
||||
):
|
||||
"""
|
||||
Create a new NewLLMConfig for a search space.
|
||||
Requires LLM_CONFIGS_CREATE permission.
|
||||
"""
|
||||
try:
|
||||
# Verify user has permission
|
||||
await check_permission(
|
||||
session,
|
||||
user,
|
||||
config_data.search_space_id,
|
||||
Permission.LLM_CONFIGS_CREATE.value,
|
||||
"You don't have permission to create LLM configurations in this search space",
|
||||
)
|
||||
|
||||
# Validate the LLM configuration by making a test API call
|
||||
is_valid, error_message = await validate_llm_config(
|
||||
provider=config_data.provider.value,
|
||||
model_name=config_data.model_name,
|
||||
api_key=config_data.api_key,
|
||||
api_base=config_data.api_base,
|
||||
custom_provider=config_data.custom_provider,
|
||||
litellm_params=config_data.litellm_params,
|
||||
)
|
||||
|
||||
if not is_valid:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"Invalid LLM configuration: {error_message}",
|
||||
)
|
||||
|
||||
# Create the config
|
||||
db_config = NewLLMConfig(**config_data.model_dump())
|
||||
session.add(db_config)
|
||||
await session.commit()
|
||||
await session.refresh(db_config)
|
||||
|
||||
return db_config
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
await session.rollback()
|
||||
logger.exception("Failed to create NewLLMConfig")
|
||||
raise HTTPException(
|
||||
status_code=500, detail=f"Failed to create configuration: {e!s}"
|
||||
) from e
|
||||
|
||||
|
||||
@router.get("/new-llm-configs", response_model=list[NewLLMConfigRead])
|
||||
async def list_new_llm_configs(
|
||||
search_space_id: int,
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
session: AsyncSession = Depends(get_async_session),
|
||||
user: User = Depends(current_active_user),
|
||||
):
|
||||
"""
|
||||
Get all NewLLMConfigs for a search space.
|
||||
Requires LLM_CONFIGS_READ permission.
|
||||
"""
|
||||
try:
|
||||
# Verify user has permission
|
||||
await check_permission(
|
||||
session,
|
||||
user,
|
||||
search_space_id,
|
||||
Permission.LLM_CONFIGS_READ.value,
|
||||
"You don't have permission to view LLM configurations in this search space",
|
||||
)
|
||||
|
||||
result = await session.execute(
|
||||
select(NewLLMConfig)
|
||||
.filter(NewLLMConfig.search_space_id == search_space_id)
|
||||
.order_by(NewLLMConfig.created_at.desc())
|
||||
.offset(skip)
|
||||
.limit(limit)
|
||||
)
|
||||
|
||||
return result.scalars().all()
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.exception("Failed to list NewLLMConfigs")
|
||||
raise HTTPException(
|
||||
status_code=500, detail=f"Failed to fetch configurations: {e!s}"
|
||||
) from e
|
||||
|
||||
|
||||
@router.get(
|
||||
"/new-llm-configs/default-system-instructions",
|
||||
response_model=DefaultSystemInstructionsResponse,
|
||||
)
|
||||
async def get_default_system_instructions_endpoint(
|
||||
user: User = Depends(current_active_user),
|
||||
):
|
||||
"""
|
||||
Get the default SURFSENSE_SYSTEM_INSTRUCTIONS template.
|
||||
Useful for pre-populating the UI when creating a new configuration.
|
||||
"""
|
||||
return DefaultSystemInstructionsResponse(
|
||||
default_system_instructions=get_default_system_instructions()
|
||||
)
|
||||
|
||||
|
||||
@router.get("/new-llm-configs/{config_id}", response_model=NewLLMConfigRead)
|
||||
async def get_new_llm_config(
|
||||
config_id: int,
|
||||
session: AsyncSession = Depends(get_async_session),
|
||||
user: User = Depends(current_active_user),
|
||||
):
|
||||
"""
|
||||
Get a specific NewLLMConfig by ID.
|
||||
Requires LLM_CONFIGS_READ permission.
|
||||
"""
|
||||
try:
|
||||
result = await session.execute(
|
||||
select(NewLLMConfig).filter(NewLLMConfig.id == config_id)
|
||||
)
|
||||
config = result.scalars().first()
|
||||
|
||||
if not config:
|
||||
raise HTTPException(status_code=404, detail="Configuration not found")
|
||||
|
||||
# Verify user has permission
|
||||
await check_permission(
|
||||
session,
|
||||
user,
|
||||
config.search_space_id,
|
||||
Permission.LLM_CONFIGS_READ.value,
|
||||
"You don't have permission to view LLM configurations in this search space",
|
||||
)
|
||||
|
||||
return config
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.exception("Failed to get NewLLMConfig")
|
||||
raise HTTPException(
|
||||
status_code=500, detail=f"Failed to fetch configuration: {e!s}"
|
||||
) from e
|
||||
|
||||
|
||||
@router.put("/new-llm-configs/{config_id}", response_model=NewLLMConfigRead)
|
||||
async def update_new_llm_config(
|
||||
config_id: int,
|
||||
update_data: NewLLMConfigUpdate,
|
||||
session: AsyncSession = Depends(get_async_session),
|
||||
user: User = Depends(current_active_user),
|
||||
):
|
||||
"""
|
||||
Update an existing NewLLMConfig.
|
||||
Requires LLM_CONFIGS_UPDATE permission.
|
||||
"""
|
||||
try:
|
||||
result = await session.execute(
|
||||
select(NewLLMConfig).filter(NewLLMConfig.id == config_id)
|
||||
)
|
||||
config = result.scalars().first()
|
||||
|
||||
if not config:
|
||||
raise HTTPException(status_code=404, detail="Configuration not found")
|
||||
|
||||
# Verify user has permission
|
||||
await check_permission(
|
||||
session,
|
||||
user,
|
||||
config.search_space_id,
|
||||
Permission.LLM_CONFIGS_UPDATE.value,
|
||||
"You don't have permission to update LLM configurations in this search space",
|
||||
)
|
||||
|
||||
update_dict = update_data.model_dump(exclude_unset=True)
|
||||
|
||||
# If updating LLM settings, validate them
|
||||
if any(
|
||||
key in update_dict
|
||||
for key in [
|
||||
"provider",
|
||||
"model_name",
|
||||
"api_key",
|
||||
"api_base",
|
||||
"custom_provider",
|
||||
"litellm_params",
|
||||
]
|
||||
):
|
||||
# Build the validation config from existing + updates
|
||||
validation_config = {
|
||||
"provider": update_dict.get("provider", config.provider).value
|
||||
if hasattr(update_dict.get("provider", config.provider), "value")
|
||||
else update_dict.get("provider", config.provider.value),
|
||||
"model_name": update_dict.get("model_name", config.model_name),
|
||||
"api_key": update_dict.get("api_key", config.api_key),
|
||||
"api_base": update_dict.get("api_base", config.api_base),
|
||||
"custom_provider": update_dict.get(
|
||||
"custom_provider", config.custom_provider
|
||||
),
|
||||
"litellm_params": update_dict.get(
|
||||
"litellm_params", config.litellm_params
|
||||
),
|
||||
}
|
||||
|
||||
is_valid, error_message = await validate_llm_config(
|
||||
provider=validation_config["provider"],
|
||||
model_name=validation_config["model_name"],
|
||||
api_key=validation_config["api_key"],
|
||||
api_base=validation_config["api_base"],
|
||||
custom_provider=validation_config["custom_provider"],
|
||||
litellm_params=validation_config["litellm_params"],
|
||||
)
|
||||
|
||||
if not is_valid:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"Invalid LLM configuration: {error_message}",
|
||||
)
|
||||
|
||||
# Apply updates
|
||||
for key, value in update_dict.items():
|
||||
setattr(config, key, value)
|
||||
|
||||
await session.commit()
|
||||
await session.refresh(config)
|
||||
|
||||
return config
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
await session.rollback()
|
||||
logger.exception("Failed to update NewLLMConfig")
|
||||
raise HTTPException(
|
||||
status_code=500, detail=f"Failed to update configuration: {e!s}"
|
||||
) from e
|
||||
|
||||
|
||||
@router.delete("/new-llm-configs/{config_id}", response_model=dict)
|
||||
async def delete_new_llm_config(
|
||||
config_id: int,
|
||||
session: AsyncSession = Depends(get_async_session),
|
||||
user: User = Depends(current_active_user),
|
||||
):
|
||||
"""
|
||||
Delete a NewLLMConfig.
|
||||
Requires LLM_CONFIGS_DELETE permission.
|
||||
"""
|
||||
try:
|
||||
result = await session.execute(
|
||||
select(NewLLMConfig).filter(NewLLMConfig.id == config_id)
|
||||
)
|
||||
config = result.scalars().first()
|
||||
|
||||
if not config:
|
||||
raise HTTPException(status_code=404, detail="Configuration not found")
|
||||
|
||||
# Verify user has permission
|
||||
await check_permission(
|
||||
session,
|
||||
user,
|
||||
config.search_space_id,
|
||||
Permission.LLM_CONFIGS_DELETE.value,
|
||||
"You don't have permission to delete LLM configurations in this search space",
|
||||
)
|
||||
|
||||
await session.delete(config)
|
||||
await session.commit()
|
||||
|
||||
return {"message": "Configuration deleted successfully", "id": config_id}
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
await session.rollback()
|
||||
logger.exception("Failed to delete NewLLMConfig")
|
||||
raise HTTPException(
|
||||
status_code=500, detail=f"Failed to delete configuration: {e!s}"
|
||||
) from e
|
||||
|
|
@ -1,13 +1,13 @@
|
|||
import logging
|
||||
from pathlib import Path
|
||||
|
||||
import yaml
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlalchemy import func
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.future import select
|
||||
|
||||
from app.config import config
|
||||
from app.db import (
|
||||
NewLLMConfig,
|
||||
Permission,
|
||||
SearchSpace,
|
||||
SearchSpaceMembership,
|
||||
|
|
@ -17,6 +17,8 @@ from app.db import (
|
|||
get_default_roles_config,
|
||||
)
|
||||
from app.schemas import (
|
||||
LLMPreferencesRead,
|
||||
LLMPreferencesUpdate,
|
||||
SearchSpaceCreate,
|
||||
SearchSpaceRead,
|
||||
SearchSpaceUpdate,
|
||||
|
|
@ -184,37 +186,6 @@ async def read_search_spaces(
|
|||
) from e
|
||||
|
||||
|
||||
@router.get("/searchspaces/prompts/community")
|
||||
async def get_community_prompts():
|
||||
"""
|
||||
Get community-curated prompts for SearchSpace System Instructions.
|
||||
This endpoint does not require authentication as it serves public prompts.
|
||||
"""
|
||||
try:
|
||||
# Get the path to the prompts YAML file
|
||||
prompts_file = (
|
||||
Path(__file__).parent.parent
|
||||
/ "prompts"
|
||||
/ "public_search_space_prompts.yaml"
|
||||
)
|
||||
|
||||
if not prompts_file.exists():
|
||||
raise HTTPException(
|
||||
status_code=404, detail="Community prompts file not found"
|
||||
)
|
||||
|
||||
with open(prompts_file, encoding="utf-8") as f:
|
||||
data = yaml.safe_load(f)
|
||||
|
||||
return data.get("prompts", [])
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=500, detail=f"Failed to load community prompts: {e!s}"
|
||||
) from e
|
||||
|
||||
|
||||
@router.get("/searchspaces/{search_space_id}", response_model=SearchSpaceRead)
|
||||
async def read_search_space(
|
||||
search_space_id: int,
|
||||
|
|
@ -329,3 +300,184 @@ async def delete_search_space(
|
|||
raise HTTPException(
|
||||
status_code=500, detail=f"Failed to delete search space: {e!s}"
|
||||
) from e
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# LLM Preferences Routes
|
||||
# =============================================================================
|
||||
|
||||
|
||||
async def _get_llm_config_by_id(
|
||||
session: AsyncSession, config_id: int | None
|
||||
) -> dict | None:
|
||||
"""
|
||||
Get an LLM config by ID as a dictionary. Returns database config for positive IDs,
|
||||
global config for negative IDs, or None if ID is None.
|
||||
"""
|
||||
if config_id is None:
|
||||
return None
|
||||
|
||||
if config_id < 0:
|
||||
# Global config - find from YAML
|
||||
global_configs = config.GLOBAL_LLM_CONFIGS
|
||||
for cfg in global_configs:
|
||||
if cfg.get("id") == config_id:
|
||||
return {
|
||||
"id": cfg.get("id"),
|
||||
"name": cfg.get("name"),
|
||||
"description": cfg.get("description"),
|
||||
"provider": cfg.get("provider"),
|
||||
"custom_provider": cfg.get("custom_provider"),
|
||||
"model_name": cfg.get("model_name"),
|
||||
"api_base": cfg.get("api_base"),
|
||||
"litellm_params": cfg.get("litellm_params", {}),
|
||||
"system_instructions": cfg.get("system_instructions", ""),
|
||||
"use_default_system_instructions": cfg.get(
|
||||
"use_default_system_instructions", True
|
||||
),
|
||||
"citations_enabled": cfg.get("citations_enabled", True),
|
||||
"is_global": True,
|
||||
}
|
||||
return None
|
||||
else:
|
||||
# Database config - convert to dict
|
||||
result = await session.execute(
|
||||
select(NewLLMConfig).filter(NewLLMConfig.id == config_id)
|
||||
)
|
||||
db_config = result.scalars().first()
|
||||
if db_config:
|
||||
return {
|
||||
"id": db_config.id,
|
||||
"name": db_config.name,
|
||||
"description": db_config.description,
|
||||
"provider": db_config.provider.value if db_config.provider else None,
|
||||
"custom_provider": db_config.custom_provider,
|
||||
"model_name": db_config.model_name,
|
||||
"api_key": db_config.api_key,
|
||||
"api_base": db_config.api_base,
|
||||
"litellm_params": db_config.litellm_params or {},
|
||||
"system_instructions": db_config.system_instructions or "",
|
||||
"use_default_system_instructions": db_config.use_default_system_instructions,
|
||||
"citations_enabled": db_config.citations_enabled,
|
||||
"created_at": db_config.created_at.isoformat()
|
||||
if db_config.created_at
|
||||
else None,
|
||||
"search_space_id": db_config.search_space_id,
|
||||
}
|
||||
return None
|
||||
|
||||
|
||||
@router.get(
|
||||
"/search-spaces/{search_space_id}/llm-preferences",
|
||||
response_model=LLMPreferencesRead,
|
||||
)
|
||||
async def get_llm_preferences(
|
||||
search_space_id: int,
|
||||
session: AsyncSession = Depends(get_async_session),
|
||||
user: User = Depends(current_active_user),
|
||||
):
|
||||
"""
|
||||
Get LLM preferences (role assignments) for a search space.
|
||||
Requires LLM_CONFIGS_READ permission.
|
||||
"""
|
||||
try:
|
||||
# Check permission
|
||||
await check_permission(
|
||||
session,
|
||||
user,
|
||||
search_space_id,
|
||||
Permission.LLM_CONFIGS_READ.value,
|
||||
"You don't have permission to view LLM preferences",
|
||||
)
|
||||
|
||||
result = await session.execute(
|
||||
select(SearchSpace).filter(SearchSpace.id == search_space_id)
|
||||
)
|
||||
search_space = result.scalars().first()
|
||||
|
||||
if not search_space:
|
||||
raise HTTPException(status_code=404, detail="Search space not found")
|
||||
|
||||
# Get full config objects for each role
|
||||
agent_llm = await _get_llm_config_by_id(session, search_space.agent_llm_id)
|
||||
document_summary_llm = await _get_llm_config_by_id(
|
||||
session, search_space.document_summary_llm_id
|
||||
)
|
||||
|
||||
return LLMPreferencesRead(
|
||||
agent_llm_id=search_space.agent_llm_id,
|
||||
document_summary_llm_id=search_space.document_summary_llm_id,
|
||||
agent_llm=agent_llm,
|
||||
document_summary_llm=document_summary_llm,
|
||||
)
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.exception("Failed to get LLM preferences")
|
||||
raise HTTPException(
|
||||
status_code=500, detail=f"Failed to get LLM preferences: {e!s}"
|
||||
) from e
|
||||
|
||||
|
||||
@router.put(
|
||||
"/search-spaces/{search_space_id}/llm-preferences",
|
||||
response_model=LLMPreferencesRead,
|
||||
)
|
||||
async def update_llm_preferences(
|
||||
search_space_id: int,
|
||||
preferences: LLMPreferencesUpdate,
|
||||
session: AsyncSession = Depends(get_async_session),
|
||||
user: User = Depends(current_active_user),
|
||||
):
|
||||
"""
|
||||
Update LLM preferences (role assignments) for a search space.
|
||||
Requires LLM_CONFIGS_UPDATE permission.
|
||||
"""
|
||||
try:
|
||||
# Check permission
|
||||
await check_permission(
|
||||
session,
|
||||
user,
|
||||
search_space_id,
|
||||
Permission.LLM_CONFIGS_UPDATE.value,
|
||||
"You don't have permission to update LLM preferences",
|
||||
)
|
||||
|
||||
result = await session.execute(
|
||||
select(SearchSpace).filter(SearchSpace.id == search_space_id)
|
||||
)
|
||||
search_space = result.scalars().first()
|
||||
|
||||
if not search_space:
|
||||
raise HTTPException(status_code=404, detail="Search space not found")
|
||||
|
||||
# Update preferences
|
||||
update_data = preferences.model_dump(exclude_unset=True)
|
||||
for key, value in update_data.items():
|
||||
setattr(search_space, key, value)
|
||||
|
||||
await session.commit()
|
||||
await session.refresh(search_space)
|
||||
|
||||
# Get full config objects for response
|
||||
agent_llm = await _get_llm_config_by_id(session, search_space.agent_llm_id)
|
||||
document_summary_llm = await _get_llm_config_by_id(
|
||||
session, search_space.document_summary_llm_id
|
||||
)
|
||||
|
||||
return LLMPreferencesRead(
|
||||
agent_llm_id=search_space.agent_llm_id,
|
||||
document_summary_llm_id=search_space.document_summary_llm_id,
|
||||
agent_llm=agent_llm,
|
||||
document_summary_llm=document_summary_llm,
|
||||
)
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
await session.rollback()
|
||||
logger.exception("Failed to update LLM preferences")
|
||||
raise HTTPException(
|
||||
status_code=500, detail=f"Failed to update LLM preferences: {e!s}"
|
||||
) from e
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@ from .documents import (
|
|||
ExtensionDocumentMetadata,
|
||||
PaginatedResponse,
|
||||
)
|
||||
from .llm_config import LLMConfigBase, LLMConfigCreate, LLMConfigRead, LLMConfigUpdate
|
||||
from .logs import LogBase, LogCreate, LogFilter, LogRead, LogUpdate
|
||||
from .new_chat import (
|
||||
ChatMessage,
|
||||
|
|
@ -26,6 +25,16 @@ from .new_chat import (
|
|||
ThreadListItem,
|
||||
ThreadListResponse,
|
||||
)
|
||||
from .new_llm_config import (
|
||||
DefaultSystemInstructionsResponse,
|
||||
GlobalNewLLMConfigRead,
|
||||
LLMPreferencesRead,
|
||||
LLMPreferencesUpdate,
|
||||
NewLLMConfigCreate,
|
||||
NewLLMConfigPublic,
|
||||
NewLLMConfigRead,
|
||||
NewLLMConfigUpdate,
|
||||
)
|
||||
from .podcasts import PodcastBase, PodcastCreate, PodcastRead, PodcastUpdate
|
||||
from .rbac_schemas import (
|
||||
InviteAcceptRequest,
|
||||
|
|
@ -67,6 +76,7 @@ __all__ = [
|
|||
"ChunkCreate",
|
||||
"ChunkRead",
|
||||
"ChunkUpdate",
|
||||
"DefaultSystemInstructionsResponse",
|
||||
# Document schemas
|
||||
"DocumentBase",
|
||||
"DocumentRead",
|
||||
|
|
@ -75,6 +85,7 @@ __all__ = [
|
|||
"DocumentsCreate",
|
||||
"ExtensionDocumentContent",
|
||||
"ExtensionDocumentMetadata",
|
||||
"GlobalNewLLMConfigRead",
|
||||
# Base schemas
|
||||
"IDModel",
|
||||
# RBAC schemas
|
||||
|
|
@ -84,11 +95,9 @@ __all__ = [
|
|||
"InviteInfoResponse",
|
||||
"InviteRead",
|
||||
"InviteUpdate",
|
||||
# LLM Config schemas
|
||||
"LLMConfigBase",
|
||||
"LLMConfigCreate",
|
||||
"LLMConfigRead",
|
||||
"LLMConfigUpdate",
|
||||
# LLM Preferences schemas
|
||||
"LLMPreferencesRead",
|
||||
"LLMPreferencesUpdate",
|
||||
# Log schemas
|
||||
"LogBase",
|
||||
"LogCreate",
|
||||
|
|
@ -106,6 +115,11 @@ __all__ = [
|
|||
"NewChatThreadRead",
|
||||
"NewChatThreadUpdate",
|
||||
"NewChatThreadWithMessages",
|
||||
# NewLLMConfig schemas
|
||||
"NewLLMConfigCreate",
|
||||
"NewLLMConfigPublic",
|
||||
"NewLLMConfigRead",
|
||||
"NewLLMConfigUpdate",
|
||||
"PaginatedResponse",
|
||||
"PermissionInfo",
|
||||
"PermissionsListResponse",
|
||||
|
|
|
|||
|
|
@ -1,72 +0,0 @@
|
|||
from datetime import datetime
|
||||
from typing import Any
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
|
||||
from app.db import LiteLLMProvider
|
||||
|
||||
from .base import IDModel, TimestampModel
|
||||
|
||||
|
||||
class LLMConfigBase(BaseModel):
|
||||
name: str = Field(
|
||||
..., max_length=100, description="User-friendly name for the LLM configuration"
|
||||
)
|
||||
provider: LiteLLMProvider = Field(..., description="LiteLLM provider type")
|
||||
custom_provider: str | None = Field(
|
||||
None, max_length=100, description="Custom provider name when provider is CUSTOM"
|
||||
)
|
||||
model_name: str = Field(
|
||||
..., max_length=100, description="Model name without provider prefix"
|
||||
)
|
||||
api_key: str = Field(..., description="API key for the provider")
|
||||
api_base: str | None = Field(
|
||||
None, max_length=500, description="Optional API base URL"
|
||||
)
|
||||
litellm_params: dict[str, Any] | None = Field(
|
||||
default=None, description="Additional LiteLLM parameters"
|
||||
)
|
||||
language: str | None = Field(
|
||||
default="English", max_length=50, description="Language for the LLM"
|
||||
)
|
||||
|
||||
|
||||
class LLMConfigCreate(LLMConfigBase):
|
||||
search_space_id: int = Field(
|
||||
..., description="Search space ID to associate the LLM config with"
|
||||
)
|
||||
|
||||
|
||||
class LLMConfigUpdate(BaseModel):
|
||||
name: str | None = Field(
|
||||
None, max_length=100, description="User-friendly name for the LLM configuration"
|
||||
)
|
||||
provider: LiteLLMProvider | None = Field(None, description="LiteLLM provider type")
|
||||
custom_provider: str | None = Field(
|
||||
None, max_length=100, description="Custom provider name when provider is CUSTOM"
|
||||
)
|
||||
model_name: str | None = Field(
|
||||
None, max_length=100, description="Model name without provider prefix"
|
||||
)
|
||||
api_key: str | None = Field(None, description="API key for the provider")
|
||||
api_base: str | None = Field(
|
||||
None, max_length=500, description="Optional API base URL"
|
||||
)
|
||||
language: str | None = Field(
|
||||
None, max_length=50, description="Language for the LLM"
|
||||
)
|
||||
litellm_params: dict[str, Any] | None = Field(
|
||||
None, description="Additional LiteLLM parameters"
|
||||
)
|
||||
|
||||
|
||||
class LLMConfigRead(LLMConfigBase, IDModel, TimestampModel):
|
||||
id: int
|
||||
created_at: datetime | None = Field(
|
||||
None, description="Creation timestamp (None for global configs)"
|
||||
)
|
||||
search_space_id: int | None = Field(
|
||||
None, description="Search space ID (None for global configs)"
|
||||
)
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
191
surfsense_backend/app/schemas/new_llm_config.py
Normal file
191
surfsense_backend/app/schemas/new_llm_config.py
Normal file
|
|
@ -0,0 +1,191 @@
|
|||
"""
|
||||
Pydantic schemas for the NewLLMConfig API.
|
||||
|
||||
NewLLMConfig combines LLM model settings with prompt configuration:
|
||||
- LLM provider, model, API key, etc.
|
||||
- Configurable system instructions
|
||||
- Citation toggle
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
from typing import Any
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
|
||||
from app.db import LiteLLMProvider
|
||||
|
||||
|
||||
class NewLLMConfigBase(BaseModel):
|
||||
"""Base schema with common fields for NewLLMConfig."""
|
||||
|
||||
name: str = Field(
|
||||
..., max_length=100, description="User-friendly name for the configuration"
|
||||
)
|
||||
description: str | None = Field(
|
||||
None, max_length=500, description="Optional description"
|
||||
)
|
||||
|
||||
# LLM Model Configuration
|
||||
provider: LiteLLMProvider = Field(..., description="LiteLLM provider type")
|
||||
custom_provider: str | None = Field(
|
||||
None, max_length=100, description="Custom provider name when provider is CUSTOM"
|
||||
)
|
||||
model_name: str = Field(
|
||||
..., max_length=100, description="Model name without provider prefix"
|
||||
)
|
||||
api_key: str = Field(..., description="API key for the provider")
|
||||
api_base: str | None = Field(
|
||||
None, max_length=500, description="Optional API base URL"
|
||||
)
|
||||
litellm_params: dict[str, Any] | None = Field(
|
||||
default=None, description="Additional LiteLLM parameters"
|
||||
)
|
||||
|
||||
# Prompt Configuration
|
||||
system_instructions: str = Field(
|
||||
default="",
|
||||
description="Custom system instructions. Empty string uses default SURFSENSE_SYSTEM_INSTRUCTIONS.",
|
||||
)
|
||||
use_default_system_instructions: bool = Field(
|
||||
default=True,
|
||||
description="Whether to use default instructions when system_instructions is empty",
|
||||
)
|
||||
citations_enabled: bool = Field(
|
||||
default=True,
|
||||
description="Whether to include citation instructions in the system prompt",
|
||||
)
|
||||
|
||||
|
||||
class NewLLMConfigCreate(NewLLMConfigBase):
|
||||
"""Schema for creating a new NewLLMConfig."""
|
||||
|
||||
search_space_id: int = Field(
|
||||
..., description="Search space ID to associate the config with"
|
||||
)
|
||||
|
||||
|
||||
class NewLLMConfigUpdate(BaseModel):
|
||||
"""Schema for updating an existing NewLLMConfig. All fields are optional."""
|
||||
|
||||
name: str | None = Field(None, max_length=100)
|
||||
description: str | None = Field(None, max_length=500)
|
||||
|
||||
# LLM Model Configuration
|
||||
provider: LiteLLMProvider | None = None
|
||||
custom_provider: str | None = Field(None, max_length=100)
|
||||
model_name: str | None = Field(None, max_length=100)
|
||||
api_key: str | None = None
|
||||
api_base: str | None = Field(None, max_length=500)
|
||||
litellm_params: dict[str, Any] | None = None
|
||||
|
||||
# Prompt Configuration
|
||||
system_instructions: str | None = None
|
||||
use_default_system_instructions: bool | None = None
|
||||
citations_enabled: bool | None = None
|
||||
|
||||
|
||||
class NewLLMConfigRead(NewLLMConfigBase):
|
||||
"""Schema for reading a NewLLMConfig (includes id and timestamps)."""
|
||||
|
||||
id: int
|
||||
created_at: datetime
|
||||
search_space_id: int
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
class NewLLMConfigPublic(BaseModel):
|
||||
"""
|
||||
Public schema for NewLLMConfig that hides the API key.
|
||||
Used when returning configs in list views or to users who shouldn't see keys.
|
||||
"""
|
||||
|
||||
id: int
|
||||
name: str
|
||||
description: str | None = None
|
||||
|
||||
# LLM Model Configuration (no api_key)
|
||||
provider: LiteLLMProvider
|
||||
custom_provider: str | None = None
|
||||
model_name: str
|
||||
api_base: str | None = None
|
||||
litellm_params: dict[str, Any] | None = None
|
||||
|
||||
# Prompt Configuration
|
||||
system_instructions: str
|
||||
use_default_system_instructions: bool
|
||||
citations_enabled: bool
|
||||
|
||||
created_at: datetime
|
||||
search_space_id: int
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
class DefaultSystemInstructionsResponse(BaseModel):
|
||||
"""Response schema for getting default system instructions."""
|
||||
|
||||
default_system_instructions: str = Field(
|
||||
..., description="The default SURFSENSE_SYSTEM_INSTRUCTIONS template"
|
||||
)
|
||||
|
||||
|
||||
class GlobalNewLLMConfigRead(BaseModel):
|
||||
"""
|
||||
Schema for reading global LLM configs from YAML.
|
||||
Global configs have negative IDs and no search_space_id.
|
||||
API key is hidden for security.
|
||||
"""
|
||||
|
||||
id: int = Field(..., description="Negative ID for global configs")
|
||||
name: str
|
||||
description: str | None = None
|
||||
|
||||
# LLM Model Configuration (no api_key)
|
||||
provider: str # String because YAML doesn't enforce enum
|
||||
custom_provider: str | None = None
|
||||
model_name: str
|
||||
api_base: str | None = None
|
||||
litellm_params: dict[str, Any] | None = None
|
||||
|
||||
# Prompt Configuration
|
||||
system_instructions: str = ""
|
||||
use_default_system_instructions: bool = True
|
||||
citations_enabled: bool = True
|
||||
|
||||
is_global: bool = True # Always true for global configs
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# LLM Preferences Schemas (for role assignments)
|
||||
# =============================================================================
|
||||
|
||||
|
||||
class LLMPreferencesRead(BaseModel):
|
||||
"""Schema for reading LLM preferences (role assignments) for a search space."""
|
||||
|
||||
agent_llm_id: int | None = Field(
|
||||
None, description="ID of the LLM config to use for agent/chat tasks"
|
||||
)
|
||||
document_summary_llm_id: int | None = Field(
|
||||
None, description="ID of the LLM config to use for document summarization"
|
||||
)
|
||||
agent_llm: dict[str, Any] | None = Field(
|
||||
None, description="Full config for agent LLM"
|
||||
)
|
||||
document_summary_llm: dict[str, Any] | None = Field(
|
||||
None, description="Full config for document summary LLM"
|
||||
)
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
class LLMPreferencesUpdate(BaseModel):
|
||||
"""Schema for updating LLM preferences."""
|
||||
|
||||
agent_llm_id: int | None = Field(
|
||||
None, description="ID of the LLM config to use for agent/chat tasks"
|
||||
)
|
||||
document_summary_llm_id: int | None = Field(
|
||||
None, description="ID of the LLM config to use for document summarization"
|
||||
)
|
||||
|
|
@ -7,7 +7,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
|||
from sqlalchemy.future import select
|
||||
|
||||
from app.config import config
|
||||
from app.db import LLMConfig, SearchSpace
|
||||
from app.db import NewLLMConfig, SearchSpace
|
||||
|
||||
# Configure litellm to automatically drop unsupported parameters
|
||||
litellm.drop_params = True
|
||||
|
|
@ -16,9 +16,8 @@ logger = logging.getLogger(__name__)
|
|||
|
||||
|
||||
class LLMRole:
|
||||
LONG_CONTEXT = "long_context"
|
||||
FAST = "fast"
|
||||
STRATEGIC = "strategic"
|
||||
AGENT = "agent" # For agent/chat operations
|
||||
DOCUMENT_SUMMARY = "document_summary" # For document summarization
|
||||
|
||||
|
||||
def get_global_llm_config(llm_config_id: int) -> dict | None:
|
||||
|
|
@ -155,7 +154,7 @@ async def get_search_space_llm_instance(
|
|||
Args:
|
||||
session: Database session
|
||||
search_space_id: Search Space ID
|
||||
role: LLM role ('long_context', 'fast', or 'strategic')
|
||||
role: LLM role ('agent' or 'document_summary')
|
||||
|
||||
Returns:
|
||||
ChatLiteLLM instance or None if not found
|
||||
|
|
@ -173,12 +172,10 @@ async def get_search_space_llm_instance(
|
|||
|
||||
# Get the appropriate LLM config ID based on role
|
||||
llm_config_id = None
|
||||
if role == LLMRole.LONG_CONTEXT:
|
||||
llm_config_id = search_space.long_context_llm_id
|
||||
elif role == LLMRole.FAST:
|
||||
llm_config_id = search_space.fast_llm_id
|
||||
elif role == LLMRole.STRATEGIC:
|
||||
llm_config_id = search_space.strategic_llm_id
|
||||
if role == LLMRole.AGENT:
|
||||
llm_config_id = search_space.agent_llm_id
|
||||
elif role == LLMRole.DOCUMENT_SUMMARY:
|
||||
llm_config_id = search_space.document_summary_llm_id
|
||||
else:
|
||||
logger.error(f"Invalid LLM role: {role}")
|
||||
return None
|
||||
|
|
@ -250,11 +247,11 @@ async def get_search_space_llm_instance(
|
|||
|
||||
return ChatLiteLLM(**litellm_kwargs)
|
||||
|
||||
# Get the LLM configuration from database (user-specific config)
|
||||
# Get the LLM configuration from database (NewLLMConfig)
|
||||
result = await session.execute(
|
||||
select(LLMConfig).where(
|
||||
LLMConfig.id == llm_config_id,
|
||||
LLMConfig.search_space_id == search_space_id,
|
||||
select(NewLLMConfig).where(
|
||||
NewLLMConfig.id == llm_config_id,
|
||||
NewLLMConfig.search_space_id == search_space_id,
|
||||
)
|
||||
)
|
||||
llm_config = result.scalars().first()
|
||||
|
|
@ -265,11 +262,11 @@ async def get_search_space_llm_instance(
|
|||
)
|
||||
return None
|
||||
|
||||
# Build the model string for litellm / 构建 LiteLLM 的模型字符串
|
||||
# Build the model string for litellm
|
||||
if llm_config.custom_provider:
|
||||
model_string = f"{llm_config.custom_provider}/{llm_config.model_name}"
|
||||
else:
|
||||
# Map provider enum to litellm format / 将提供商枚举映射为 LiteLLM 格式
|
||||
# Map provider enum to litellm format
|
||||
provider_map = {
|
||||
"OPENAI": "openai",
|
||||
"ANTHROPIC": "anthropic",
|
||||
|
|
@ -283,7 +280,7 @@ async def get_search_space_llm_instance(
|
|||
"COMETAPI": "cometapi",
|
||||
"XAI": "xai",
|
||||
"BEDROCK": "bedrock",
|
||||
"AWS_BEDROCK": "bedrock", # Legacy support (backward compatibility)
|
||||
"AWS_BEDROCK": "bedrock",
|
||||
"VERTEX_AI": "vertex_ai",
|
||||
"TOGETHER_AI": "together_ai",
|
||||
"FIREWORKS_AI": "fireworks_ai",
|
||||
|
|
@ -296,7 +293,6 @@ async def get_search_space_llm_instance(
|
|||
"AI21": "ai21",
|
||||
"CLOUDFLARE": "cloudflare",
|
||||
"DATABRICKS": "databricks",
|
||||
# Chinese LLM providers
|
||||
"DEEPSEEK": "openai",
|
||||
"ALIBABA_QWEN": "openai",
|
||||
"MOONSHOT": "openai",
|
||||
|
|
@ -330,28 +326,19 @@ async def get_search_space_llm_instance(
|
|||
return None
|
||||
|
||||
|
||||
async def get_long_context_llm(
|
||||
async def get_agent_llm(
|
||||
session: AsyncSession, search_space_id: int
|
||||
) -> ChatLiteLLM | None:
|
||||
"""Get the search space's long context LLM instance."""
|
||||
"""Get the search space's agent LLM instance for chat operations."""
|
||||
return await get_search_space_llm_instance(session, search_space_id, LLMRole.AGENT)
|
||||
|
||||
|
||||
async def get_document_summary_llm(
|
||||
session: AsyncSession, search_space_id: int
|
||||
) -> ChatLiteLLM | None:
|
||||
"""Get the search space's document summary LLM instance."""
|
||||
return await get_search_space_llm_instance(
|
||||
session, search_space_id, LLMRole.LONG_CONTEXT
|
||||
)
|
||||
|
||||
|
||||
async def get_fast_llm(
|
||||
session: AsyncSession, search_space_id: int
|
||||
) -> ChatLiteLLM | None:
|
||||
"""Get the search space's fast LLM instance."""
|
||||
return await get_search_space_llm_instance(session, search_space_id, LLMRole.FAST)
|
||||
|
||||
|
||||
async def get_strategic_llm(
|
||||
session: AsyncSession, search_space_id: int
|
||||
) -> ChatLiteLLM | None:
|
||||
"""Get the search space's strategic LLM instance."""
|
||||
return await get_search_space_llm_instance(
|
||||
session, search_space_id, LLMRole.STRATEGIC
|
||||
session, search_space_id, LLMRole.DOCUMENT_SUMMARY
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -366,22 +353,54 @@ async def get_user_llm_instance(
|
|||
return await get_search_space_llm_instance(session, search_space_id, role)
|
||||
|
||||
|
||||
# Legacy aliases for backward compatibility
|
||||
async def get_long_context_llm(
|
||||
session: AsyncSession, search_space_id: int
|
||||
) -> ChatLiteLLM | None:
|
||||
"""Deprecated: Use get_document_summary_llm instead."""
|
||||
return await get_document_summary_llm(session, search_space_id)
|
||||
|
||||
|
||||
async def get_fast_llm(
|
||||
session: AsyncSession, search_space_id: int
|
||||
) -> ChatLiteLLM | None:
|
||||
"""Deprecated: Use get_agent_llm instead."""
|
||||
return await get_agent_llm(session, search_space_id)
|
||||
|
||||
|
||||
async def get_strategic_llm(
|
||||
session: AsyncSession, search_space_id: int
|
||||
) -> ChatLiteLLM | None:
|
||||
"""Deprecated: Use get_document_summary_llm instead."""
|
||||
return await get_document_summary_llm(session, search_space_id)
|
||||
|
||||
|
||||
# User-based legacy aliases (LLM preferences are now per-search-space, not per-user)
|
||||
async def get_user_long_context_llm(
|
||||
session: AsyncSession, user_id: str, search_space_id: int
|
||||
) -> ChatLiteLLM | None:
|
||||
"""Deprecated: Use get_long_context_llm instead."""
|
||||
return await get_long_context_llm(session, search_space_id)
|
||||
"""
|
||||
Deprecated: Use get_document_summary_llm instead.
|
||||
The user_id parameter is ignored as LLM preferences are now per-search-space.
|
||||
"""
|
||||
return await get_document_summary_llm(session, search_space_id)
|
||||
|
||||
|
||||
async def get_user_fast_llm(
|
||||
session: AsyncSession, user_id: str, search_space_id: int
|
||||
) -> ChatLiteLLM | None:
|
||||
"""Deprecated: Use get_fast_llm instead."""
|
||||
return await get_fast_llm(session, search_space_id)
|
||||
"""
|
||||
Deprecated: Use get_agent_llm instead.
|
||||
The user_id parameter is ignored as LLM preferences are now per-search-space.
|
||||
"""
|
||||
return await get_agent_llm(session, search_space_id)
|
||||
|
||||
|
||||
async def get_user_strategic_llm(
|
||||
session: AsyncSession, user_id: str, search_space_id: int
|
||||
) -> ChatLiteLLM | None:
|
||||
"""Deprecated: Use get_strategic_llm instead."""
|
||||
return await get_strategic_llm(session, search_space_id)
|
||||
"""
|
||||
Deprecated: Use get_document_summary_llm instead.
|
||||
The user_id parameter is ignored as LLM preferences are now per-search-space.
|
||||
"""
|
||||
return await get_document_summary_llm(session, search_space_id)
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ from typing import Any
|
|||
from langchain_core.messages import AIMessage, HumanMessage, SystemMessage
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.services.llm_service import get_strategic_llm
|
||||
from app.services.llm_service import get_document_summary_llm
|
||||
|
||||
|
||||
class QueryService:
|
||||
|
|
@ -20,7 +20,7 @@ class QueryService:
|
|||
chat_history_str: str | None = None,
|
||||
) -> str:
|
||||
"""
|
||||
Reformulate the user query using the search space's strategic LLM to make it more
|
||||
Reformulate the user query using the search space's document summary LLM to make it more
|
||||
effective for information retrieval and research purposes.
|
||||
|
||||
Args:
|
||||
|
|
@ -36,11 +36,11 @@ class QueryService:
|
|||
return user_query
|
||||
|
||||
try:
|
||||
# Get the search space's strategic LLM instance
|
||||
llm = await get_strategic_llm(session, search_space_id)
|
||||
# Get the search space's document summary LLM instance
|
||||
llm = await get_document_summary_llm(session, search_space_id)
|
||||
if not llm:
|
||||
print(
|
||||
f"Warning: No strategic LLM configured for search space {search_space_id}. Using original query."
|
||||
f"Warning: No document summary LLM configured for search space {search_space_id}. Using original query."
|
||||
)
|
||||
return user_query
|
||||
|
||||
|
|
|
|||
|
|
@ -1,191 +0,0 @@
|
|||
import json
|
||||
from typing import Any
|
||||
|
||||
|
||||
class StreamingService:
|
||||
def __init__(self):
|
||||
self.terminal_idx = 1
|
||||
self.message_annotations = [
|
||||
{"type": "TERMINAL_INFO", "content": []},
|
||||
{"type": "SOURCES", "content": []},
|
||||
{"type": "ANSWER", "content": []},
|
||||
{"type": "FURTHER_QUESTIONS", "content": []},
|
||||
]
|
||||
|
||||
# DEPRECATED: This sends the full annotation array every time (inefficient)
|
||||
def _format_annotations(self) -> str:
|
||||
"""
|
||||
Format the annotations as a string
|
||||
|
||||
DEPRECATED: This method sends the full annotation state every time.
|
||||
Use the delta formatters instead for optimal streaming.
|
||||
|
||||
Returns:
|
||||
str: The formatted annotations string
|
||||
"""
|
||||
return f"8:{json.dumps(self.message_annotations)}\n"
|
||||
|
||||
def format_terminal_info_delta(self, text: str, message_type: str = "info") -> str:
|
||||
"""
|
||||
Format a single terminal info message as a delta annotation
|
||||
|
||||
Args:
|
||||
text: The terminal message text
|
||||
message_type: The message type (info, error, success, etc.)
|
||||
|
||||
Returns:
|
||||
str: The formatted annotation delta string
|
||||
"""
|
||||
message = {"id": self.terminal_idx, "text": text, "type": message_type}
|
||||
self.terminal_idx += 1
|
||||
|
||||
# Update internal state for reference
|
||||
self.message_annotations[0]["content"].append(message)
|
||||
|
||||
# Return only the delta annotation
|
||||
annotation = {"type": "TERMINAL_INFO", "data": message}
|
||||
return f"8:[{json.dumps(annotation)}]\n"
|
||||
|
||||
def format_sources_delta(self, sources: list[dict[str, Any]]) -> str:
|
||||
"""
|
||||
Format sources as a delta annotation
|
||||
|
||||
Args:
|
||||
sources: List of source objects
|
||||
|
||||
Returns:
|
||||
str: The formatted annotation delta string
|
||||
"""
|
||||
# Update internal state
|
||||
self.message_annotations[1]["content"] = sources
|
||||
|
||||
# Return only the delta annotation
|
||||
nodes = []
|
||||
|
||||
for group in sources:
|
||||
for source in group.get("sources", []):
|
||||
node = {
|
||||
"id": str(source.get("id", "")),
|
||||
"text": source.get("description", "").strip(),
|
||||
"url": source.get("url", ""),
|
||||
"metadata": {
|
||||
"title": source.get("title", ""),
|
||||
"source_type": group.get("type", ""),
|
||||
"group_name": group.get("name", ""),
|
||||
},
|
||||
}
|
||||
nodes.append(node)
|
||||
|
||||
annotation = {"type": "sources", "data": {"nodes": nodes}}
|
||||
return f"8:[{json.dumps(annotation)}]\n"
|
||||
|
||||
def format_answer_delta(self, answer_chunk: str) -> str:
|
||||
"""
|
||||
Format a single answer chunk as a delta annotation
|
||||
|
||||
Args:
|
||||
answer_chunk: The new answer chunk to add
|
||||
|
||||
Returns:
|
||||
str: The formatted annotation delta string
|
||||
"""
|
||||
# Update internal state by appending the chunk
|
||||
if isinstance(self.message_annotations[2]["content"], list):
|
||||
self.message_annotations[2]["content"].append(answer_chunk)
|
||||
else:
|
||||
self.message_annotations[2]["content"] = [answer_chunk]
|
||||
|
||||
# Return only the delta annotation with the new chunk
|
||||
annotation = {"type": "ANSWER", "content": [answer_chunk]}
|
||||
return f"8:[{json.dumps(annotation)}]\n"
|
||||
|
||||
def format_answer_annotation(self, answer_lines: list[str]) -> str:
|
||||
"""
|
||||
Format the complete answer as a replacement annotation
|
||||
|
||||
Args:
|
||||
answer_lines: Complete list of answer lines
|
||||
|
||||
Returns:
|
||||
str: The formatted annotation string
|
||||
"""
|
||||
# Update internal state
|
||||
self.message_annotations[2]["content"] = answer_lines
|
||||
|
||||
# Return the full answer annotation
|
||||
annotation = {"type": "ANSWER", "content": answer_lines}
|
||||
return f"8:[{json.dumps(annotation)}]\n"
|
||||
|
||||
def format_further_questions_delta(
|
||||
self, further_questions: list[dict[str, Any]]
|
||||
) -> str:
|
||||
"""
|
||||
Format further questions as a delta annotation
|
||||
|
||||
Args:
|
||||
further_questions: List of further question objects
|
||||
|
||||
Returns:
|
||||
str: The formatted annotation delta string
|
||||
"""
|
||||
# Update internal state
|
||||
self.message_annotations[3]["content"] = further_questions
|
||||
|
||||
# Return only the delta annotation
|
||||
annotation = {
|
||||
"type": "FURTHER_QUESTIONS",
|
||||
"data": [
|
||||
question.get("question", "")
|
||||
for question in further_questions
|
||||
if question.get("question", "") != ""
|
||||
],
|
||||
}
|
||||
return f"8:[{json.dumps(annotation)}]\n"
|
||||
|
||||
def format_text_chunk(self, text: str) -> str:
|
||||
"""
|
||||
Format a text chunk using the text stream part
|
||||
|
||||
Args:
|
||||
text: The text chunk to stream
|
||||
|
||||
Returns:
|
||||
str: The formatted text part string
|
||||
"""
|
||||
return f"0:{json.dumps(text)}\n"
|
||||
|
||||
def format_error(self, error_message: str) -> str:
|
||||
"""
|
||||
Format an error using the error stream part
|
||||
|
||||
Args:
|
||||
error_message: The error message
|
||||
|
||||
Returns:
|
||||
str: The formatted error part string
|
||||
"""
|
||||
return f"3:{json.dumps(error_message)}\n"
|
||||
|
||||
def format_completion(
|
||||
self, prompt_tokens: int = 156, completion_tokens: int = 204
|
||||
) -> str:
|
||||
"""
|
||||
Format a completion message
|
||||
|
||||
Args:
|
||||
prompt_tokens: Number of prompt tokens
|
||||
completion_tokens: Number of completion tokens
|
||||
|
||||
Returns:
|
||||
str: The formatted completion string
|
||||
"""
|
||||
total_tokens = prompt_tokens + completion_tokens
|
||||
completion_data = {
|
||||
"finishReason": "stop",
|
||||
"usage": {
|
||||
"promptTokens": prompt_tokens,
|
||||
"completionTokens": completion_tokens,
|
||||
"totalTokens": total_tokens,
|
||||
},
|
||||
}
|
||||
return f"d:{json.dumps(completion_data)}\n"
|
||||
|
|
@ -3,20 +3,28 @@ Streaming task for the new SurfSense deep agent chat.
|
|||
|
||||
This module streams responses from the deep agent using the Vercel AI SDK
|
||||
Data Stream Protocol (SSE format).
|
||||
|
||||
Supports loading LLM configurations from:
|
||||
- YAML files (negative IDs for global configs)
|
||||
- NewLLMConfig database table (positive IDs for user-created configs with prompt settings)
|
||||
"""
|
||||
|
||||
import json
|
||||
from collections.abc import AsyncGenerator
|
||||
|
||||
from langchain_core.messages import HumanMessage
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.agents.new_chat.chat_deepagent import create_surfsense_deep_agent
|
||||
from app.agents.new_chat.checkpointer import get_checkpointer
|
||||
from app.agents.new_chat.llm_config import (
|
||||
AgentConfig,
|
||||
create_chat_litellm_from_agent_config,
|
||||
create_chat_litellm_from_config,
|
||||
load_agent_config,
|
||||
load_llm_config_from_yaml,
|
||||
)
|
||||
from app.schemas.new_chat import ChatAttachment, ChatMessage
|
||||
from app.schemas.new_chat import ChatAttachment
|
||||
from app.services.connector_service import ConnectorService
|
||||
from app.services.new_streaming_service import VercelStreamingService
|
||||
|
||||
|
|
@ -44,7 +52,6 @@ async def stream_new_chat(
|
|||
chat_id: int,
|
||||
session: AsyncSession,
|
||||
llm_config_id: int = -1,
|
||||
messages: list[ChatMessage] | None = None,
|
||||
attachments: list[ChatAttachment] | None = None,
|
||||
) -> AsyncGenerator[str, None]:
|
||||
"""
|
||||
|
|
@ -71,17 +78,40 @@ async def stream_new_chat(
|
|||
current_text_id: str | None = None
|
||||
|
||||
try:
|
||||
# Load LLM config
|
||||
llm_config = load_llm_config_from_yaml(llm_config_id=llm_config_id)
|
||||
if not llm_config:
|
||||
yield streaming_service.format_error(
|
||||
f"Failed to load LLM config with id {llm_config_id}"
|
||||
)
|
||||
yield streaming_service.format_done()
|
||||
return
|
||||
# Load LLM config - supports both YAML (negative IDs) and database (positive IDs)
|
||||
agent_config: AgentConfig | None = None
|
||||
|
||||
if llm_config_id >= 0:
|
||||
# Positive ID: Load from NewLLMConfig database table
|
||||
agent_config = await load_agent_config(
|
||||
session=session,
|
||||
config_id=llm_config_id,
|
||||
search_space_id=search_space_id,
|
||||
)
|
||||
if not agent_config:
|
||||
yield streaming_service.format_error(
|
||||
f"Failed to load NewLLMConfig with id {llm_config_id}"
|
||||
)
|
||||
yield streaming_service.format_done()
|
||||
return
|
||||
|
||||
# Create ChatLiteLLM from AgentConfig
|
||||
llm = create_chat_litellm_from_agent_config(agent_config)
|
||||
else:
|
||||
# Negative ID: Load from YAML (global configs)
|
||||
llm_config = load_llm_config_from_yaml(llm_config_id=llm_config_id)
|
||||
if not llm_config:
|
||||
yield streaming_service.format_error(
|
||||
f"Failed to load LLM config with id {llm_config_id}"
|
||||
)
|
||||
yield streaming_service.format_done()
|
||||
return
|
||||
|
||||
# Create ChatLiteLLM from YAML config dict
|
||||
llm = create_chat_litellm_from_config(llm_config)
|
||||
# Create AgentConfig from YAML for consistency (uses defaults for prompt settings)
|
||||
agent_config = AgentConfig.from_yaml_config(llm_config)
|
||||
|
||||
# Create ChatLiteLLM instance
|
||||
llm = create_chat_litellm_from_config(llm_config)
|
||||
if not llm:
|
||||
yield streaming_service.format_error("Failed to create LLM instance")
|
||||
yield streaming_service.format_done()
|
||||
|
|
@ -93,13 +123,14 @@ async def stream_new_chat(
|
|||
# Get the PostgreSQL checkpointer for persistent conversation memory
|
||||
checkpointer = await get_checkpointer()
|
||||
|
||||
# Create the deep agent with checkpointer with podcast capability
|
||||
# Create the deep agent with checkpointer and configurable prompts
|
||||
agent = create_surfsense_deep_agent(
|
||||
llm=llm,
|
||||
search_space_id=search_space_id,
|
||||
db_session=session,
|
||||
connector_service=connector_service,
|
||||
checkpointer=checkpointer,
|
||||
agent_config=agent_config, # Pass prompt configuration
|
||||
)
|
||||
|
||||
# Build input with message history from frontend
|
||||
|
|
@ -180,7 +211,9 @@ async def stream_new_chat(
|
|||
analyze_step_id = next_thinking_step_id()
|
||||
last_active_step_id = analyze_step_id
|
||||
last_active_step_title = "Understanding your request"
|
||||
last_active_step_items = [f"Processing: {user_query[:80]}{'...' if len(user_query) > 80 else ''}"]
|
||||
last_active_step_items = [
|
||||
f"Processing: {user_query[:80]}{'...' if len(user_query) > 80 else ''}"
|
||||
]
|
||||
yield streaming_service.format_thinking_step(
|
||||
step_id=analyze_step_id,
|
||||
title="Understanding your request",
|
||||
|
|
@ -255,7 +288,9 @@ async def stream_new_chat(
|
|||
else str(tool_input)
|
||||
)
|
||||
last_active_step_title = "Searching knowledge base"
|
||||
last_active_step_items = [f"Query: {query[:100]}{'...' if len(query) > 100 else ''}"]
|
||||
last_active_step_items = [
|
||||
f"Query: {query[:100]}{'...' if len(query) > 100 else ''}"
|
||||
]
|
||||
yield streaming_service.format_thinking_step(
|
||||
step_id=tool_step_id,
|
||||
title="Searching knowledge base",
|
||||
|
|
@ -269,7 +304,9 @@ async def stream_new_chat(
|
|||
else str(tool_input)
|
||||
)
|
||||
last_active_step_title = "Fetching link preview"
|
||||
last_active_step_items = [f"URL: {url[:80]}{'...' if len(url) > 80 else ''}"]
|
||||
last_active_step_items = [
|
||||
f"URL: {url[:80]}{'...' if len(url) > 80 else ''}"
|
||||
]
|
||||
yield streaming_service.format_thinking_step(
|
||||
step_id=tool_step_id,
|
||||
title="Fetching link preview",
|
||||
|
|
@ -304,7 +341,9 @@ async def stream_new_chat(
|
|||
else str(tool_input)
|
||||
)
|
||||
last_active_step_title = "Scraping webpage"
|
||||
last_active_step_items = [f"URL: {url[:80]}{'...' if len(url) > 80 else ''}"]
|
||||
last_active_step_items = [
|
||||
f"URL: {url[:80]}{'...' if len(url) > 80 else ''}"
|
||||
]
|
||||
yield streaming_service.format_thinking_step(
|
||||
step_id=tool_step_id,
|
||||
title="Scraping webpage",
|
||||
|
|
@ -441,7 +480,9 @@ async def stream_new_chat(
|
|||
tool_call_id = f"call_{run_id[:32]}" if run_id else "call_unknown"
|
||||
|
||||
# Get the original tool step ID to update it (not create a new one)
|
||||
original_step_id = tool_step_ids.get(run_id, f"thinking-unknown-{run_id[:8]}")
|
||||
original_step_id = tool_step_ids.get(
|
||||
run_id, f"thinking-unknown-{run_id[:8]}"
|
||||
)
|
||||
|
||||
# Mark the tool thinking step as completed using the SAME step ID
|
||||
# Also add to completed set so we don't try to complete it again
|
||||
|
|
@ -452,7 +493,9 @@ async def stream_new_chat(
|
|||
if isinstance(tool_output, dict):
|
||||
result_len = tool_output.get("result_length", 0)
|
||||
if result_len > 0:
|
||||
result_info = f"Found relevant information ({result_len} chars)"
|
||||
result_info = (
|
||||
f"Found relevant information ({result_len} chars)"
|
||||
)
|
||||
# Include original query in completed items
|
||||
completed_items = [*last_active_step_items, result_info]
|
||||
yield streaming_service.format_thinking_step(
|
||||
|
|
@ -541,7 +584,7 @@ async def stream_new_chat(
|
|||
if isinstance(tool_output, dict)
|
||||
else "Podcast"
|
||||
)
|
||||
|
||||
|
||||
if podcast_status == "processing":
|
||||
completed_items = [
|
||||
f"Title: {podcast_title}",
|
||||
|
|
@ -566,7 +609,7 @@ async def stream_new_chat(
|
|||
]
|
||||
else:
|
||||
completed_items = last_active_step_items
|
||||
|
||||
|
||||
yield streaming_service.format_thinking_step(
|
||||
step_id=original_step_id,
|
||||
title="Generating podcast",
|
||||
|
|
@ -652,7 +695,9 @@ async def stream_new_chat(
|
|||
)
|
||||
# Send terminal message
|
||||
if isinstance(tool_output, dict):
|
||||
title = tool_output.get("title") or tool_output.get("alt", "Image")
|
||||
title = tool_output.get("title") or tool_output.get(
|
||||
"alt", "Image"
|
||||
)
|
||||
yield streaming_service.format_terminal_info(
|
||||
f"Image displayed: {title[:40]}{'...' if len(title) > 40 else ''}",
|
||||
"success",
|
||||
|
|
|
|||
|
|
@ -1,23 +0,0 @@
|
|||
"""
|
||||
Legacy podcast task for old chat system.
|
||||
|
||||
NOTE: The old Chat model has been removed. This module is kept for backwards
|
||||
compatibility but the generate_chat_podcast function will raise an error
|
||||
if called. Use generate_content_podcast_task in celery_tasks/podcast_tasks.py
|
||||
for new-chat podcast generation instead.
|
||||
"""
|
||||
|
||||
from app.db import Podcast # noqa: F401 - imported for backwards compatibility
|
||||
|
||||
|
||||
async def generate_chat_podcast(*args, **kwargs):
|
||||
"""
|
||||
Legacy function for generating podcasts from old chat system.
|
||||
|
||||
This function is deprecated as the old Chat model has been removed.
|
||||
Use generate_content_podcast_task for new-chat podcast generation.
|
||||
"""
|
||||
raise NotImplementedError(
|
||||
"generate_chat_podcast is deprecated. The old Chat model has been removed. "
|
||||
"Use generate_content_podcast_task for podcast generation from new-chat."
|
||||
)
|
||||
Loading…
Add table
Add a link
Reference in a new issue