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
435 changes: 227 additions & 208 deletions archon-ui-main/src/components/settings/RAGSettings.tsx

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions python/src/server/api_routes/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from .knowledge_api import router as knowledge_router
from .mcp_api import router as mcp_router
from .projects_api import router as projects_router
from .providers_api import router as providers_router
from .settings_api import router as settings_router

__all__ = [
Expand All @@ -23,4 +24,5 @@
"projects_router",
"agent_chat_router",
"internal_router",
"providers_router",
]
69 changes: 52 additions & 17 deletions python/src/server/api_routes/knowledge_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
from fastapi import APIRouter, File, Form, HTTPException, UploadFile
from pydantic import BaseModel

# Basic validation - simplified inline version

# Import unified logging
from ..config.logfire_config import get_logger, safe_logfire_error, safe_logfire_info
from ..services.crawler_manager import get_crawler
Expand Down Expand Up @@ -62,26 +64,59 @@ async def _validate_provider_api_key(provider: str = None) -> None:
logger.info("🔑 Starting API key validation...")

try:
# Basic provider validation
if not provider:
provider = "openai"
else:
# Simple provider validation
allowed_providers = {"openai", "ollama", "google", "openrouter", "anthropic", "grok"}
if provider not in allowed_providers:
raise HTTPException(
status_code=400,
detail={
"error": "Invalid provider name",
"message": f"Provider '{provider}' not supported",
"error_type": "validation_error"
}
)

logger.info(f"🔑 Testing {provider.title()} API key with minimal embedding request...")

# Test API key with minimal embedding request - this will fail if key is invalid
from ..services.embeddings.embedding_service import create_embedding
test_result = await create_embedding(text="test")

if not test_result:
logger.error(f"❌ {provider.title()} API key validation failed - no embedding returned")
raise HTTPException(
status_code=401,
detail={
"error": f"Invalid {provider.title()} API key",
"message": f"Please verify your {provider.title()} API key in Settings.",
"error_type": "authentication_failed",
"provider": provider
}
)
# Basic sanitization for logging
safe_provider = provider[:20] # Limit length
logger.info(f"🔑 Testing {safe_provider.title()} API key with minimal embedding request...")

try:
# Test API key with minimal embedding request using provider-scoped configuration
from ..services.embeddings.embedding_service import create_embedding

test_result = await create_embedding(text="test", provider=provider)

if not test_result:
logger.error(
f"❌ {provider.title()} API key validation failed - no embedding returned"
)
raise HTTPException(
status_code=401,
detail={
"error": f"Invalid {provider.title()} API key",
"message": f"Please verify your {provider.title()} API key in Settings.",
"error_type": "authentication_failed",
"provider": provider,
},
)
except Exception as e:
logger.error(
f"❌ {provider.title()} API key validation failed: {e}",
exc_info=True,
)
raise HTTPException(
status_code=401,
detail={
"error": f"Invalid {provider.title()} API key",
"message": f"Please verify your {provider.title()} API key in Settings. Error: {str(e)[:100]}",
"error_type": "authentication_failed",
"provider": provider,
},
)

logger.info(f"✅ {provider.title()} API key validation successful")

Expand Down
154 changes: 154 additions & 0 deletions python/src/server/api_routes/providers_api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
"""
Provider status API endpoints for testing connectivity

Handles server-side provider connectivity testing without exposing API keys to frontend.
"""

import httpx
from fastapi import APIRouter, HTTPException, Path

from ..config.logfire_config import logfire
from ..services.credential_service import credential_service
# Provider validation - simplified inline version

router = APIRouter(prefix="/api/providers", tags=["providers"])


async def test_openai_connection(api_key: str) -> bool:
"""Test OpenAI API connectivity"""
try:
async with httpx.AsyncClient(timeout=10.0) as client:
response = await client.get(
"https://api.openai.com/v1/models",
headers={"Authorization": f"Bearer {api_key}"}
)
return response.status_code == 200
except Exception as e:
logfire.warning(f"OpenAI connectivity test failed: {e}")
return False


async def test_google_connection(api_key: str) -> bool:
"""Test Google AI API connectivity"""
try:
async with httpx.AsyncClient(timeout=10.0) as client:
response = await client.get(
"https://generativelanguage.googleapis.com/v1/models",
headers={"x-goog-api-key": api_key}
)
return response.status_code == 200
except Exception:
logfire.warning("Google AI connectivity test failed")
return False


async def test_anthropic_connection(api_key: str) -> bool:
"""Test Anthropic API connectivity"""
try:
async with httpx.AsyncClient(timeout=10.0) as client:
response = await client.get(
"https://api.anthropic.com/v1/models",
headers={
"x-api-key": api_key,
"anthropic-version": "2023-06-01"
}
)
return response.status_code == 200
except Exception as e:
logfire.warning(f"Anthropic connectivity test failed: {e}")
return False


async def test_openrouter_connection(api_key: str) -> bool:
"""Test OpenRouter API connectivity"""
try:
async with httpx.AsyncClient(timeout=10.0) as client:
response = await client.get(
"https://openrouter.ai/api/v1/models",
headers={"Authorization": f"Bearer {api_key}"}
)
return response.status_code == 200
except Exception as e:
logfire.warning(f"OpenRouter connectivity test failed: {e}")
return False


async def test_grok_connection(api_key: str) -> bool:
"""Test Grok API connectivity"""
try:
async with httpx.AsyncClient(timeout=10.0) as client:
response = await client.get(
"https://api.x.ai/v1/models",
headers={"Authorization": f"Bearer {api_key}"}
)
return response.status_code == 200
except Exception as e:
logfire.warning(f"Grok connectivity test failed: {e}")
return False


PROVIDER_TESTERS = {
"openai": test_openai_connection,
"google": test_google_connection,
"anthropic": test_anthropic_connection,
"openrouter": test_openrouter_connection,
"grok": test_grok_connection,
}


@router.get("/{provider}/status")
async def get_provider_status(
provider: str = Path(
...,
description="Provider name to test connectivity for",
regex="^[a-z0-9_]+$",
max_length=20
)
):
"""Test provider connectivity using server-side API key (secure)"""
try:
# Basic provider validation
allowed_providers = {"openai", "ollama", "google", "openrouter", "anthropic", "grok"}
if provider not in allowed_providers:
raise HTTPException(
status_code=400,
detail=f"Invalid provider '{provider}'. Allowed providers: {sorted(allowed_providers)}"
)

# Basic sanitization for logging
safe_provider = provider[:20] # Limit length
logfire.info(f"Testing {safe_provider} connectivity server-side")

if provider not in PROVIDER_TESTERS:
raise HTTPException(
status_code=400,
detail=f"Provider '{provider}' not supported for connectivity testing"
)

# Get API key server-side (never expose to client)
key_name = f"{provider.upper()}_API_KEY"
api_key = await credential_service.get_credential(key_name, decrypt=True)

if not api_key or not isinstance(api_key, str) or not api_key.strip():
logfire.info(f"No API key configured for {safe_provider}")
return {"ok": False, "reason": "no_key"}

# Test connectivity using server-side key
tester = PROVIDER_TESTERS[provider]
is_connected = await tester(api_key)

logfire.info(f"{safe_provider} connectivity test result: {is_connected}")
return {
"ok": is_connected,
"reason": "connected" if is_connected else "connection_failed",
"provider": provider # Echo back validated provider name
}

except HTTPException:
# Re-raise HTTP exceptions (they're already properly formatted)
raise
except Exception as e:
# Basic error sanitization for logging
safe_error = str(e)[:100] # Limit length
logfire.error(f"Error testing {provider[:20]} connectivity: {safe_error}")
raise HTTPException(status_code=500, detail={"error": "Internal server error during connectivity test"})
2 changes: 2 additions & 0 deletions python/src/server/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
from .api_routes.ollama_api import router as ollama_router
from .api_routes.progress_api import router as progress_router
from .api_routes.projects_api import router as projects_router
from .api_routes.providers_api import router as providers_router

# Import modular API routers
from .api_routes.settings_api import router as settings_router
Expand Down Expand Up @@ -186,6 +187,7 @@ async def skip_health_check_logs(request, call_next):
app.include_router(agent_chat_router)
app.include_router(internal_router)
app.include_router(bug_report_router)
app.include_router(providers_router)


# Root endpoint
Expand Down
11 changes: 7 additions & 4 deletions python/src/server/services/crawling/code_extraction_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ async def extract_and_store_code_examples(
source_id: str,
progress_callback: Callable | None = None,
cancellation_check: Callable[[], None] | None = None,
provider: str | None = None,
) -> int:
"""
Extract code examples from crawled documents and store them.
Expand Down Expand Up @@ -204,7 +205,7 @@ async def summary_progress(data: dict):

# Generate summaries for code blocks
summary_results = await self._generate_code_summaries(
all_code_blocks, summary_callback, cancellation_check
all_code_blocks, summary_callback, cancellation_check, provider
)

# Prepare code examples for storage
Expand All @@ -223,7 +224,7 @@ async def storage_progress(data: dict):

# Store code examples in database
return await self._store_code_examples(
storage_data, url_to_full_document, storage_callback
storage_data, url_to_full_document, storage_callback, provider
)

async def _extract_code_blocks_from_documents(
Expand Down Expand Up @@ -1523,6 +1524,7 @@ async def _generate_code_summaries(
all_code_blocks: list[dict[str, Any]],
progress_callback: Callable | None = None,
cancellation_check: Callable[[], None] | None = None,
provider: str | None = None,
) -> list[dict[str, str]]:
"""
Generate summaries for all code blocks.
Expand Down Expand Up @@ -1587,7 +1589,7 @@ async def wrapped_callback(data: dict):

try:
results = await generate_code_summaries_batch(
code_blocks_for_summaries, max_workers, progress_callback=summary_progress_callback
code_blocks_for_summaries, max_workers, progress_callback=summary_progress_callback, provider=provider
)

# Ensure all results are valid dicts
Expand Down Expand Up @@ -1667,6 +1669,7 @@ async def _store_code_examples(
storage_data: dict[str, list[Any]],
url_to_full_document: dict[str, str],
progress_callback: Callable | None = None,
provider: str | None = None,
) -> int:
"""
Store code examples in the database.
Expand Down Expand Up @@ -1709,7 +1712,7 @@ async def storage_callback(data: dict):
batch_size=20,
url_to_full_document=url_to_full_document,
progress_callback=storage_progress_callback,
provider=None, # Use configured provider
provider=provider,
)

# Report completion of code extraction/storage phase
Expand Down
12 changes: 12 additions & 0 deletions python/src/server/services/crawling/crawling_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -474,12 +474,24 @@ async def code_progress_callback(data: dict):
)

try:
# Extract provider from request or use credential service default
provider = request.get("provider")
if not provider:
try:
from ..credential_service import credential_service
provider_config = await credential_service.get_active_provider("llm")
provider = provider_config.get("provider", "openai")
except Exception as e:
logger.warning(f"Failed to get provider from credential service: {e}, defaulting to openai")
provider = "openai"

code_examples_count = await self.doc_storage_ops.extract_and_store_code_examples(
crawl_results,
storage_results["url_to_full_document"],
storage_results["source_id"],
code_progress_callback,
self._check_cancellation,
provider,
)
except RuntimeError as e:
# Code extraction failed, continue crawl with warning
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -351,6 +351,7 @@ async def extract_and_store_code_examples(
source_id: str,
progress_callback: Callable | None = None,
cancellation_check: Callable[[], None] | None = None,
provider: str | None = None,
) -> int:
"""
Extract code examples from crawled documents and store them.
Expand All @@ -361,12 +362,13 @@ async def extract_and_store_code_examples(
source_id: The unique source_id for all documents
progress_callback: Optional callback for progress updates
cancellation_check: Optional function to check for cancellation
provider: Optional LLM provider to use for code summaries

Returns:
Number of code examples stored
"""
result = await self.code_extraction_service.extract_and_store_code_examples(
crawl_results, url_to_full_document, source_id, progress_callback, cancellation_check
crawl_results, url_to_full_document, source_id, progress_callback, cancellation_check, provider
)

return result
Loading