Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions aperag/migration/sql/model_configs_init.sql
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
-- Model configuration initialization SQL script
-- Generated directly from configuration data on 2026-03-03 14:09:57
-- Generated directly from configuration data on 2026-03-03 14:16:57
-- This script populates llm_provider and llm_provider_models tables

BEGIN;
Expand Down Expand Up @@ -10236,6 +10236,6 @@ ON CONFLICT (provider_name, api, model) DO UPDATE SET

COMMIT;

-- Script completed. Generated on 2026-03-03 14:09:57
-- Script completed. Generated on 2026-03-03 14:16:57
-- Total providers: 9
-- Total models: 670
66 changes: 37 additions & 29 deletions models/generate_model_configs.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
# limitations under the License.

import json
import logging
import os
from datetime import datetime
from typing import Dict, List, Any, Optional
Expand All @@ -35,6 +36,10 @@
import requests
from litellm import model_cost

# Suppress litellm's verbose warnings and debug output
litellm.suppress_debug_info = True
logging.getLogger("LiteLLM").setLevel(logging.ERROR)

# --- Manual Override for Context Windows ---
# For models where litellm's data might be ambiguous or incorrect,
# we define the correct context window size here.
Expand Down Expand Up @@ -289,10 +294,19 @@ def generate_model_specs(models, provider, mode, blocklist=None, tag_rules=None)
def create_openai_config():
provider = "openai"

# Define blocklists
completion_blocklist = []
embedding_blocklist = []
rerank_blocklist = []
# Define blocklists - deprecated/instruct models not in litellm's model_prices_and_context_window.json
completion_blocklist = [
"babbage-002", "davinci-002", "ft:babbage-002", "ft:davinci-002",
"gpt-3.5-turbo-instruct", "gpt-3.5-turbo-instruct-0914",
]
embedding_blocklist = [
"babbage-002", "davinci-002", "ft:babbage-002", "ft:davinci-002",
"gpt-3.5-turbo-instruct", "gpt-3.5-turbo-instruct-0914",
]
rerank_blocklist = [
"babbage-002", "davinci-002", "ft:babbage-002", "ft:davinci-002",
"gpt-3.5-turbo-instruct", "gpt-3.5-turbo-instruct-0914",
]

# Define tag rules
completion_tag_rules = {
Expand Down Expand Up @@ -903,7 +917,13 @@ def create_openrouter_config():
if data is None:
try:
if os.path.exists(openrouter_file):
file_mtime = os.path.getmtime(openrouter_file)
file_age_days = (datetime.now().timestamp() - file_mtime) / 86400
file_date = datetime.fromtimestamp(file_mtime).strftime('%Y-%m-%d')
print(f"📁 Reading OpenRouter models from local file: {openrouter_file}")
print(f" (cached on {file_date}, {file_age_days:.0f} days ago)")
if file_age_days > 30:
print(f"⚠️ Warning: local cache is {file_age_days:.0f} days old — consider refreshing via VPN or proxy")
with open(openrouter_file, 'r', encoding='utf-8') as f:
data = json.load(f)
print("✅ Successfully loaded OpenRouter models from local file")
Expand Down Expand Up @@ -991,13 +1011,6 @@ def create_provider_config():
No need to pass parameters - block lists and tag rules are defined in each provider function.
"""

print("\n📋 Block List Usage:")
print("- Block lists and tag rules are now defined internally in each provider function")
print("- To modify block lists or tag rules, edit the provider functions directly")
print("- Each provider supports completion_blocklist, embedding_blocklist, and rerank_blocklist")
print("- Tag rules map tag names to model name patterns")
print()

# Generate provider configurations
result = [
create_openai_config(),
Expand Down Expand Up @@ -1164,33 +1177,28 @@ def main():
"""Main function to generate model configuration and SQL script"""
try:
print("Generating model configuration data...")
print("\n📋 Block List Usage:")
print("- Block lists and tag rules are now defined internally in each provider function")
print("- To modify block lists or tag rules, edit the provider functions directly")
print("- Each provider supports completion_blocklist, embedding_blocklist, and rerank_blocklist")
print("- Tag rules map tag names to model name patterns")
print(" (Block lists and tag rules are defined internally in each provider function)")
print()

providers_data = create_provider_config()

print("Generating SQL script...")
print("\n📊 Summary:")
total_models = 0
for p in providers_data:
completion = len(p.get('completion', []))
embedding = len(p.get('embedding', []))
rerank = len(p.get('rerank', []))
subtotal = completion + embedding + rerank
total_models += subtotal
print(f" {p['label']:<20} completion={completion:>4} embedding={embedding:>3} rerank={rerank:>3}")
print(f" {'TOTAL':<20} {total_models} models across {len(providers_data)} providers")

print("\nGenerating SQL script...")
sql_script = generate_sql_script(providers_data)

save_sql_to_file(sql_script)

print("✅ Model configuration SQL script generated successfully!")
print("\nTo execute the script:")
print(" psql -h <host> -U <user> -d <database> -f aperag/sql/model_configs_init.sql")
print("\nOr copy the contents and run in your PostgreSQL client.")

print("\n🔧 Usage Examples:")
print("1. To customize block lists or tag rules:")
print(" Edit the block list and tag rule variables in each provider function")
print("2. To add/remove models from block lists:")
print(" Modify the completion_blocklist, embedding_blocklist, or rerank_blocklist in provider functions")
print("3. To add/modify model tags:")
print(" Update the tag_rules dictionaries in provider functions")
print(" Example: completion_tag_rules = {'enable_for_collection': ['model1', 'model2'], 'free': ['model3']}")

except Exception as e:
print(f"❌ Error generating SQL script: {e}")
Expand Down
7 changes: 7 additions & 0 deletions web/src/app/workspace/menu-footer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
ChevronRight,
Key,
Logs,
MessageSquareText,
Package,
Settings,
} from 'lucide-react';
Expand Down Expand Up @@ -79,6 +80,12 @@ export const MenuFooter = () => {
<BatteryMedium /> {sidebar_workspace('quotas')}
</Link>
</DropdownMenuItem>

<DropdownMenuItem asChild>
<Link href="/workspace/prompts">
<MessageSquareText /> {sidebar_workspace('prompts')}
</Link>
</DropdownMenuItem>
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
Expand Down
29 changes: 29 additions & 0 deletions web/src/app/workspace/prompts/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import {
PageContainer,
PageContent,
PageDescription,
PageHeader,
PageTitle,
} from '@/components/page-container';
import { getServerApi } from '@/lib/api/server';
import { toJson } from '@/lib/utils';
import { getTranslations } from 'next-intl/server';
import { PromptSettings } from './prompt-settings';

export default async function Page() {
const serverApi = await getServerApi();
const res = await serverApi.defaultApi.promptsUserGet();
const data = res.data;
const page_prompts = await getTranslations('page_prompts');

return (
<PageContainer>
<PageHeader breadcrumbs={[{ title: page_prompts('metadata.title') }]} />
<PageContent>
<PageTitle>{page_prompts('metadata.title')}</PageTitle>
<PageDescription>{page_prompts('metadata.description')}</PageDescription>
<PromptSettings data={toJson(data)} />
</PageContent>
</PageContainer>
);
}
156 changes: 156 additions & 0 deletions web/src/app/workspace/prompts/prompt-settings.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
'use client';

import { PromptDetail, UserPromptsResponse } from '@/api';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from '@/components/ui/card';
import {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from '@/components/ui/dialog';
import { Textarea } from '@/components/ui/textarea';
import { apiClient } from '@/lib/api/client';
import { useTranslations } from 'next-intl';
import { useRouter } from 'next/navigation';
import { useCallback, useEffect, useState } from 'react';
import { toast } from 'sonner';

type PromptType = 'agent_system' | 'agent_query';

const PROMPT_TYPES: PromptType[] = ['agent_system', 'agent_query'];

interface PromptCardProps {
promptType: PromptType;
detail: PromptDetail | undefined;
onSaved: () => void;
}

const PromptCard = ({ promptType, detail, onSaved }: PromptCardProps) => {
const page_prompts = useTranslations('page_prompts');
const common_action = useTranslations('common.action');
const [content, setContent] = useState(detail?.content ?? '');
const [saving, setSaving] = useState(false);
const [resetOpen, setResetOpen] = useState(false);

useEffect(() => {
setContent(detail?.content ?? '');
}, [detail?.content]);

const handleSave = useCallback(async () => {
setSaving(true);
try {
await apiClient.defaultApi.promptsUserPut({
updateUserPromptsRequest: {
prompts: { [promptType]: content },
},
});
toast.success(page_prompts('toast.save_success'));
onSaved();
} finally {
setSaving(false);
}
}, [content, promptType, page_prompts, onSaved]);

const handleReset = useCallback(async () => {
await apiClient.defaultApi.promptsUserPromptTypeDelete({
promptType: promptType as never,
});
toast.success(page_prompts('toast.reset_success'));
setResetOpen(false);
onSaved();
}, [promptType, page_prompts, onSaved]);

const isCustomized = detail?.customized === true;

return (
<Card>
<CardHeader>
<div className="flex items-center justify-between gap-2">
<CardTitle>{page_prompts(`${promptType}.title` as never)}</CardTitle>
<Badge variant={isCustomized ? 'default' : 'secondary'}>
{isCustomized
? page_prompts('status.customized')
: page_prompts('status.default')}
</Badge>
</div>
<CardDescription>
{page_prompts(`${promptType}.description` as never)}
</CardDescription>
</CardHeader>

<CardContent>
<Textarea
className="min-h-[160px] max-h-[360px] font-mono text-sm resize-y"
value={content}
onChange={(e) => setContent(e.target.value)}
placeholder={detail?.content ?? ''}
/>
</CardContent>

<CardFooter className="justify-end gap-2">
{isCustomized && (
<Dialog open={resetOpen} onOpenChange={setResetOpen}>
<DialogTrigger asChild>
<Button variant="outline">
{page_prompts('action.reset')}
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>{page_prompts('action.reset_confirm')}</DialogTitle>
<DialogDescription>
{page_prompts('action.reset_confirm_description')}
</DialogDescription>
</DialogHeader>
<DialogFooter>
<DialogClose asChild>
<Button variant="outline">{common_action('cancel')}</Button>
</DialogClose>
<Button variant="destructive" onClick={handleReset}>
{page_prompts('action.reset')}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
)}
<Button onClick={handleSave} disabled={saving}>
{page_prompts('action.save')}
</Button>
</CardFooter>
</Card>
);
};

export const PromptSettings = ({ data }: { data: UserPromptsResponse }) => {
const router = useRouter();

const handleSaved = useCallback(() => {
setTimeout(router.refresh, 300);
}, [router]);

return (
<div className="flex flex-col gap-6">
{PROMPT_TYPES.map((promptType) => (
<PromptCard
key={promptType}
promptType={promptType}
detail={data[promptType]}
onSaved={handleSaved}
/>
))}
</div>
);
};
31 changes: 30 additions & 1 deletion web/src/i18n/en-US.json
Original file line number Diff line number Diff line change
Expand Up @@ -464,6 +464,34 @@
"prompt_template_document_content": "The content of the document from the collection.",
"prompt_template_number_of_question": "The number of questions to be generated."
},
"page_prompts": {
"metadata": {
"title": "Prompt Management",
"description": "Customize system prompts to adjust the AI assistant's behavior and conversation style. Changes override system defaults and can be reset at any time."
},
"agent_system": {
"title": "System Prompt",
"description": "Defines the AI assistant's persona, role, and behavioral guidelines. Affects the base behavior across all conversations."
},
"agent_query": {
"title": "Query Prompt",
"description": "The query template for each conversation. Controls how user questions and knowledge base context are combined before being sent to the AI."
},
"status": {
"customized": "Customized",
"default": "Using Default"
},
"action": {
"save": "Save",
"reset": "Reset to Default",
"reset_confirm": "Reset this prompt to system default?",
"reset_confirm_description": "This will delete your customization and restore the built-in system default prompt."
},
"toast": {
"save_success": "Prompt saved successfully",
"reset_success": "Prompt reset to default"
}
},
"page_quota": {
"metadata": {
"title": "Quotas",
Expand Down Expand Up @@ -522,6 +550,7 @@
"models": "Models",
"api_keys": "API Keys",
"audit_logs": "Audit Logs",
"quotas": "Quotas"
"quotas": "Quotas",
"prompts": "Prompt Management"
}
}
Loading
Loading