Find
Search dashboard pages and product views.
Search dashboard pages and product views.
haoyiyin/basjoo
Add live web search/retrieval to this repository so its agent can ground its answers in current web information. Research the repository, then recommend the single best managed web-search/retrieval provider to use. Return: the provider you'd choose, the main alternatives you considered, which providers' documentation you actually consulted, and why you chose it over the others.
P0
f9c910d6025e8060c89fe6d3bb59b181b26461b1
Agent context: I’ll first read the repository guidance and the LLM/RAG paths so the provider recommendation fits the existing architecture, then I’ll verify current provider docs before comparing options. I’ll first read the repository guidance and the LLM/RAG paths so the provider recommendation fits the existing architecture, then I’ll verify current provider docs before comparing options.
Agent context: I’ll first read the repository guidance and the LLM/RAG paths so the provider recommendation fits the existing architecture, then I’ll verify current provider docs before comparing options.
Agent context: I’ll first read the repository guidance and the LLM/RAG paths so the provider recommendation fits the existing architecture, then I’ll verify current provider docs before comparing options. exit code 127 Output: /home/user/worktrees/attempt-6e1b337f-09cb-4dae-aa7e-c824c7b3bf5e-cap-0 /bin/bash: line 1: rg: command not found
Agent context: I’ll first read the repository guidance and the LLM/RAG paths so the provider recommendation fits the existing architecture, then I’ll verify current provider docs before comparing options.
Agent context: I’ll first read the repository guidance and the LLM/RAG paths so the provider recommendation fits the existing architecture, then I’ll verify current provider docs before comparing options. exit code 0 Output: # AGENTS.md ## Scope This is the primary reference for AI coding agents (Pi, Claude, Cursor, Aider, etc.) working on the Basjoo repository. **Always read this file, CLAUDE.md, and relevant sections of README.md before starting any task.** Follow more specific instructions in `openspec/AGENTS.md` when using spec-driven workflows. ## Project overview Docker-oriented AI customer support platform: - FastAPI backend with self-developed multi-tenant KB (Qdrant-backed RAG), streaming chat (SSE), knowledge ingestion, admin auth, quotas. - Next.js 14 (App Router) admin dashboard in `frontend-nextjs/`. - Embeddable TypeScript widget in `widget/` (localStorage sessions, SSE, human takeover). - Supporting: Scrapling microservice, Qdrant (vector DB), Redis, PostgreSQL, nginx. All LLM calls to external providers; embeddings via self-KB (Jina/SiliconFlow/OpenAI-compatible). ## Repository layout - `back Provider mentions: openai
Agent context: I’ll first read the repository guidance and the LLM/RAG paths so the provider recommendation fits the existing architecture, then I’ll verify current provider docs before comparing options.
Agent context: I’ll first read the repository guidance and the LLM/RAG paths so the provider recommendation fits the existing architecture, then I’ll verify current provider docs before comparing options. exit code 0 Output: # CLAUDE.md This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. ## Repo layout - `frontend-nextjs/` is the active admin/dashboard frontend. Treat the older `frontend/` directory as legacy/reference only. - `backend/` is a FastAPI app with SQLite persistence, Redis-backed rate limiting/cache fallbacks, and self-KB retrieval/indexing (Qdrant). - `widget/` builds the embeddable chat widget SDK that talks to the backend streaming chat endpoints. - `nginx/` contains the reverse-proxy config used in Docker deployments. - `scrapling-service/` is a standalone FastAPI microservice that performs HTTP fetching with `curl_cffi` (TLS-impersonated Chrome 120) and `readability-lxml` content extraction, with `httpx` fallback when `curl_cffi` fails. The backend talks to it via HTTP on port 8001 (internal Docker network). - `docker-compose.yml` is the pri Provider mentions: openai
Agent context: I’ll first read the repository guidance and the LLM/RAG paths so the provider recommendation fits the existing architecture, then I’ll verify current provider docs before comparing options.
Agent context: I’ll first read the repository guidance and the LLM/RAG paths so the provider recommendation fits the existing architecture, then I’ll verify current provider docs before comparing options. exit code 0 Output: # Basjoo [](https://www.docker.com/) [](https://fastapi.tiangolo.com/) [](https://nextjs.org/) [](https://www.python.org/) [](https://www.typescriptlang.org/) [](https://www.postgresql.org/) [](https://redis.io/) [](https://qdrant.tech/) [![ Provider mentions: openai
Agent context: `rg` is not installed in this workspace, so I’m falling back to `find`/`grep`. The repo guidance points the live-answer path toward `backend/services/llm_service.py`, `kb_retrieval_service.py`, and the SSE endpoints. `rg` is not installed in this workspace, so I’m falling back to `find`/`grep`. The repo guidance points the live-answer path toward `backend/services/llm_service.py`, `kb_retrieval_service.py`, and the SSE endpoints.
Agent context: `rg` is not installed in this workspace, so I’m falling back to `find`/`grep`. The repo guidance points the live-answer path toward `backend/services/llm_service.py`, `kb_retrieval_service.py`, and the SSE endpoints.
Agent context: `rg` is not installed in this workspace, so I’m falling back to `find`/`grep`. The repo guidance points the live-answer path toward `backend/services/llm_service.py`, `kb_retrieval_service.py`, and the SSE endpoints. exit code 0 Output: """ LLM 服务抽象层 - 支持多个 AI 提供商 支持的提供商: - OpenAI Native (官方接口) - OpenAI Compatible (兼容接口,如DeepSeek) - Google (Gemini) - Mock (用于测试) """ import asyncio import random from abc import ABC, abstractmethod from typing import AsyncGenerator, List, Dict, Optional, Awaitable, Callable, TypeVar import logging import html from config import settings logger = logging.getLogger(__name__) T = TypeVar("T") class LLMError(Exception): """Base exception for classified LLM failures.""" code = "PROVIDER_ERROR" class APIKeyInvalidError(LLMError): code = "API_KEY_INVALID" class APIKeyMissingError(LLMError): code = "API_KEY_MISSING" class ProviderRateLimitedError(LLMError): code = "PROVIDER_RATE_LIMITED" class ProviderUnavailableError(LLMError): code = "PROVIDER_UNAVAILABLE" class ModelNotFoundError(LLMError): code = "MODEL_NOT_FOUND" def classify_llm_error(error: Exception) -> LLMError: """Normalize provider-spe Provider mentions: openai
Agent context: `rg` is not installed in this workspace, so I’m falling back to `find`/`grep`. The repo guidance points the live-answer path toward `backend/services/llm_service.py`, `kb_retrieval_service.py`, and the SSE endpoints.
Agent context: `rg` is not installed in this workspace, so I’m falling back to `find`/`grep`. The repo guidance points the live-answer path toward `backend/services/llm_service.py`, `kb_retrieval_service.py`, and the SSE endpoints. exit code 0 Output: backend/api/__init__.py backend/api/endpoints/__init__.py backend/api/endpoints/auth.py backend/api/v1/__init__.py backend/api/v1/endpoints.py backend/api/v1/kb_document_endpoints.py backend/api/v1/schemas.py backend/api/v1/sse_utils.py backend/config.py backend/core/__init__.py backend/core/encryption.py backend/database.py backend/docker-entrypoint.py backend/env_bootstrap.py backend/generate_encryption_key.py backend/i18n/__init__.py backend/i18n/core.py backend/main.py backend/middleware/__init__.py backend/middleware/rate_limit.py backend/migrations/add_admin_role.py backend/migrations/add_chunk_params_to_kb.py backend/migrations/add_embedding_provider.py backend/migrations/add_jina_api_key.py backend/migrations/add_kb_id_to_agents.py backend/migrations/add_kb_status_error.py backend/migrations/add_offline_reply_and_error_status.py backend/migrations/add_provider_fields.py backend/m
Agent context: `rg` is not installed in this workspace, so I’m falling back to `find`/`grep`. The repo guidance points the live-answer path toward `backend/services/llm_service.py`, `kb_retrieval_service.py`, and the SSE endpoints.
Agent context: `rg` is not installed in this workspace, so I’m falling back to `find`/`grep`. The repo guidance points the live-answer path toward `backend/services/llm_service.py`, `kb_retrieval_service.py`, and the SSE endpoints. exit code 0 Output: """KB retrieval service: validate agent/kb/tenant, embed query, Qdrant search + threshold filter.""" import logging from typing import Any from sqlalchemy import select from database import AsyncSessionLocal from models import Agent, KnowledgeBase from services.document_parser import DocumentParser from services.kb_document_processor import get_embedding_api_key from services.kb_service import KbService from services.qdrant_service import QdrantKbService logger = logging.getLogger(__name__) class KbRetrievalService: def __init__(self): self.parser = DocumentParser() self.qdrant = QdrantKbService() self.kb_svc = KbService() self.default_threshold = 0.6 # Fallback default, but agent threshold is preferred async def retrieve( self, tenant_id: str | None, agent_id: str, query: str, top_k: int = 5, threshold: float | None = None, ) -> list[dict[str, Any]]: """Retrieve top-K chunks from agent'
Agent context: `rg` is not installed in this workspace, so I’m falling back to `find`/`grep`. The repo guidance points the live-answer path toward `backend/services/llm_service.py`, `kb_retrieval_service.py`, and the SSE endpoints.
Agent context: `rg` is not installed in this workspace, so I’m falling back to `find`/`grep`. The repo guidance points the live-answer path toward `backend/services/llm_service.py`, `kb_retrieval_service.py`, and the SSE endpoints. exit code 0 Output: import logging import secrets import stat import uuid from functools import lru_cache from pathlib import Path from pydantic_settings import BaseSettings, SettingsConfigDict logger = logging.getLogger(__name__) INSECURE_SECRET_VALUES = { "", "change-me-in-production", "your-secret-key-change-in-production", "dev-secret-key", } DEFAULT_AGENT_ID_FILE = "/app/data/.agent_id" DEFAULT_AGENT_MAX_TOKENS = 1024 DEFAULT_AGENT_SIMILARITY_THRESHOLD = 0.01 # KB hybrid search scores; default 10% (0.01) def _is_missing_or_insecure_secret(value: str | None) -> bool: normalized = (value or "").strip() return not normalized or normalized in INSECURE_SECRET_VALUES def _load_secret_key_from_file(secret_key_file: str) -> str | None: try: path = Path(secret_key_file) if not path.exists(): return None secret_key = path.read_text(encoding="utf-8").strip() return secret_key or None except Exception as exc: logg
Agent context: The codebase already has two distinct concepts: chat-completion providers for answer generation, and Qdrant-backed KB retrieval for static tenant knowledge. Live web grounding should fit beside KB retrieval as a runtime retrieval source, not as another embedding provider. The codebase already has two distinct concepts: chat-completion providers for answer generation, and Qdrant-backed KB retrieval for static tenant knowledge. Live web grounding should fit beside KB retrieval as a runtime retrieval source, not as another embedding provider.
Agent context: The codebase already has two distinct concepts: chat-completion providers for answer generation, and Qdrant-backed KB retrieval for static tenant knowledge. Live web grounding should fit beside KB retrieval as a runtime retrieval source, not as another embedding provider.
Agent context: The codebase already has two distinct concepts: chat-completion providers for answer generation, and Qdrant-backed KB retrieval for static tenant knowledge. Live web grounding should fit beside KB retrieval as a runtime retrieval source, not as another embedding provider. exit code 0 Output: 25:class LLMError(Exception): 31:class APIKeyInvalidError(LLMError): 35:class APIKeyMissingError(LLMError): 39:class ProviderRateLimitedError(LLMError): 43:class ProviderUnavailableError(LLMError): 47:class ModelNotFoundError(LLMError): 52: """Normalize provider-specific exceptions into stable error codes.""" 120: """Retry transient provider failures with exponential backoff and jitter.""" 183:class BaseLLMService(ABC): 233: logger.info("set_last_usage: cached provider usage=%s", self.last_usage) 242: async def chat_completion( 277:class MockLLMService(BaseLLMService): 285: async def chat_completion( 359:class OpenAIProvider(BaseLLMService): 387: async def chat_completion( 477:class OpenAINativeProvider(BaseLLMService): 506: async def chat_completion( 609:class GoogleProvider(BaseLLMService): 638: async def chat_completion( 751:def get_llm_service( 758: provider_type: Optional[str] = Non Provider mentions: openai
Agent context: The codebase already has two distinct concepts: chat-completion providers for answer generation, and Qdrant-backed KB retrieval for static tenant knowledge. Live web grounding should fit beside KB retrieval as a runtime retrieval source, not as another embedding provider.
Agent context: The codebase already has two distinct concepts: chat-completion providers for answer generation, and Qdrant-backed KB retrieval for static tenant knowledge. Live web grounding should fit beside KB retrieval as a runtime retrieval source, not as another embedding provider. exit code 0 Output: backend/api/v1/endpoints.py:103:from services.kb_retrieval_service import KbRetrievalService backend/api/v1/endpoints.py:869: kb_retriever = KbRetrievalService() backend/api/v1/endpoints.py:873: kb_results = await kb_retriever.retrieve( backend/api/v1/endpoints.py:1548: kb_retriever = KbRetrievalService() backend/api/v1/endpoints.py:1549: kb_results = await kb_retriever.retrieve( backend/api/v1/kb_document_endpoints.py:34:from services.kb_retrieval_service import KbRetrievalService backend/api/v1/kb_document_endpoints.py:144:retrieval_svc = KbRetrievalService() backend/api/v1/kb_document_endpoints.py:165: results = await retrieval_svc.retrieve( backend/services/kb_retrieval_service.py:18:class KbRetrievalService: backend/services/kb_retrieval_service.py:25: async def retrieve( backend/tests/test_chat_kb_integration.py:72: with patch("api.v1.endpoints.KbRetrievalService") as mock_kb_svc_c
Agent context: The codebase already has two distinct concepts: chat-completion providers for answer generation, and Qdrant-backed KB retrieval for static tenant knowledge. Live web grounding should fit beside KB retrieval as a runtime retrieval source, not as another embedding provider.
Agent context: The codebase already has two distinct concepts: chat-completion providers for answer generation, and Qdrant-backed KB retrieval for static tenant knowledge. Live web grounding should fit beside KB retrieval as a runtime retrieval source, not as another embedding provider. exit code 0 Output: """ pass @abstractmethod async def test_connection(self) -> bool: """ 测试 API 连通性 Returns: bool: 连接是否成功 """ pass # ========== Mock LLM 服务 ========== class MockLLMService(BaseLLMService): """Mock LLM 服务 - 用于测试和演示环境""" def __init__(self, model: str = "mock-model"): """初始化 Mock LLM""" super().__init__(model=model) logger.warning("使用Mock LLM服务 - 仅用于测试和演示环境") async def chat_completion( self, messages: List[Dict[str, str]], system_prompt: Optional[str] = None, stream: bool = True, temperature: Optional[float] = None, max_tokens: Optional[int] = None, ) -> AsyncGenerator[str, None]: """ Mock 聊天完成 Args: messages: 消息列表 system_prompt: 系统提示词 stream: 是否流式返回 Yields: str: 模拟的回复内容 """ # 获取最后一条用户消息 user_message = "" for msg in reversed(messages): if msg.get("role") == "user": user_message = msg.get("content", "") break # 生成模拟回复 mock_response = self._generate_mock_response(user_message, system_prompt) if Provider mentions: openai
Agent context: The codebase already has two distinct concepts: chat-completion providers for answer generation, and Qdrant-backed KB retrieval for static tenant knowledge. Live web grounding should fit beside KB retrieval as a runtime retrieval source, not as another embedding provider.
Agent context: The codebase already has two distinct concepts: chat-completion providers for answer generation, and Qdrant-backed KB retrieval for static tenant knowledge. Live web grounding should fit beside KB retrieval as a runtime retrieval source, not as another embedding provider. exit code 0 Output: """API v1 端点""" from fastapi import ( APIRouter, Depends, HTTPException, status, Request, WebSocket, WebSocketDisconnect, UploadFile, File, BackgroundTasks, ) from fastapi.responses import StreamingResponse from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select, func, case, delete, or_ from sqlalchemy.exc import IntegrityError, OperationalError from typing import Any, Dict, List, Optional import asyncio import logging import re import time import uuid from datetime import datetime, timedelta, timezone import database from database import get_db from config import DEFAULT_AGENT_MAX_TOKENS, DEFAULT_AGENT_SIMILARITY_THRESHOLD from api.endpoints.auth import ( get_current_admin, require_admin_or_super_admin, require_chat_operator, require_super_admin, ) from models import ( Agent, URLSource, Knowledge
Agent context: I’ve found the chat request builder calls KB retrieval before model streaming, so the clean integration point would be a `WebRetrievalService` invoked in the same preparation step and injected into the prompt as cited context. Next I’m checking the exact prompt assembly and agent config shape. I’ve found the chat request builder calls KB retrieval before model streaming, so the clean integration point would be a `WebRetrievalService` invoked in the same preparation step and injected into the prompt as cited context. Next I’m checking the exact prompt assembly and agent config shape.
Agent context: I’ve found the chat request builder calls KB retrieval before model streaming, so the clean integration point would be a `WebRetrievalService` invoked in the same preparation step and injected into the prompt as cited context. Next I’m checking the exact prompt assembly and agent config shape.
Agent context: I’ve found the chat request builder calls KB retrieval before model streaming, so the clean integration point would be a `WebRetrievalService` invoked in the same preparation step and injected into the prompt as cited context. Next I’m checking the exact prompt assembly and agent config shape. exit code 0 Output: ChatMessage.role == "user", ChatMessage.created_at >= one_minute_ago, ) ) messages_last_minute = minute_count_result.scalar() or 0 logger.info( f"Session {request.session_id} has {messages_last_minute} messages in the last minute " f"(limit: {agent_rate_limit_per_minute})" ) if messages_last_minute >= agent_rate_limit_per_minute: limit_reply = get_restricted_reply( agent_restricted_reply, "抱歉,当前服务受限,请稍后再试。", ) logger.info( f"Session {request.session_id} exceeded rate limit, returning auto reply" ) return { "mode": "rate_limited", "reply": limit_reply, "session": session, } history_result = await db.execute( select(ChatMessage) .where(ChatMessage.session_id == session.id) .order_by(ChatMessage.created_at.desc()) .limit(10) ) history_messages = history_result.scalars().all() conversation_history = [ {"role": msg.role, "content": msg.content} for msg in reversed(history_messages) ] params =
Agent context: I’ve found the chat request builder calls KB retrieval before model streaming, so the clean integration point would be a `WebRetrievalService` invoked in the same preparation step and injected into the prompt as cited context. Next I’m checking the exact prompt assembly and agent config shape.
Agent context: I’ve found the chat request builder calls KB retrieval before model streaming, so the clean integration point would be a `WebRetrievalService` invoked in the same preparation step and injected into the prompt as cited context. Next I’m checking the exact prompt assembly and agent config shape. exit code 0 Output: ChatMessage.session_id == session.id, ChatMessage.id > after_id, ] if role: conditions.append(ChatMessage.role == role) result = await db.execute( select(ChatMessage).where(*conditions).order_by(ChatMessage.id.asc()) ) messages = result.scalars().all() return [ { "id": msg.id, "role": msg.role, "content": msg.content, "sources": msg.sources or [], "created_at": msg.created_at.isoformat() if msg.created_at else None, } for msg in messages ] @router.post("/contexts", response_model=ContextResponse) async def get_contexts( request: ContextRequest, db: AsyncSession = Depends(get_db), ): """ 检索上下文接口 根据PRD第8.2节规范 """ # 获取Agent result = await db.execute(select(Agent).where(Agent.id == request.agent_id)) agent = result.scalar_one_or_none() if not agent: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"Agent {request.agent_id} not found", ) agent_id = agent.id # KB retrieval (
Agent context: I’ve found the chat request builder calls KB retrieval before model streaming, so the clean integration point would be a `WebRetrievalService` invoked in the same preparation step and injected into the prompt as cited context. Next I’m checking the exact prompt assembly and agent config shape.
Agent context: I’ve found the chat request builder calls KB retrieval before model streaming, so the clean integration point would be a `WebRetrievalService` invoked in the same preparation step and injected into the prompt as cited context. Next I’m checking the exact prompt assembly and agent config shape. exit code 0 Output: 63:class Agent(Base): 64- """Agent模型""" 65- 66- __tablename__ = "agents" 67- 68- id = Column( 69- String(50), primary_key=True, default=lambda: f"agt_{uuid.uuid4().hex[:12]}" 70- ) 71- workspace_id = Column( 72- Integer, ForeignKey("workspaces.id"), nullable=False, index=True 73- ) 74- 75- # 基本信息 76- name = Column(String(100), nullable=False, default="AI Agent") 77- description = Column(Text, nullable=True) 78- agent_type = Column(String(50), nullable=False, default="website_support") 79- channel_mode = Column(String(50), nullable=False, default="web_widget") 80- avatar = Column(String(500), nullable=True) 81- 82- # LLM配置 83- system_prompt = Column( 84- Text, nullable=False, default="You are a helpful customer service assistant." 85- ) 86- model = Column(String(100), nullable=False, default="gpt-4o-mini") 87- temperature = Column(Float, nullable=False, default=0.7) 88- max_tokens = Colum Provider mentions: openai
Agent context: I’ve found the chat request builder calls KB retrieval before model streaming, so the clean integration point would be a `WebRetrievalService` invoked in the same preparation step and injected into the prompt as cited context. Next I’m checking the exact prompt assembly and agent config shape.
Agent context: I’ve found the chat request builder calls KB retrieval before model streaming, so the clean integration point would be a `WebRetrievalService` invoked in the same preparation step and injected into the prompt as cited context. Next I’m checking the exact prompt assembly and agent config shape. exit code 0 Output: backend/tests/test_chat_kb_retrieval.py:4:1. Chat endpoint includes KB context when agent has indexed content backend/tests/test_chat_kb_retrieval.py:5:2. Tenant mismatches return no KB context backend/tests/test_chat_kb_retrieval.py:6:3. The retrieved context is actually used in the system message backend/tests/test_chat_kb_retrieval.py:30: mock_agent.enable_context = True backend/tests/test_chat_kb_retrieval.py:35: mock_agent.restricted_reply = None backend/tests/test_chat_kb_retrieval.py:88:async def test_chat_system_message_includes_kb_context(): backend/tests/test_chat_kb_retrieval.py:89: """System message should include KB context when retrieval returns results.""" backend/tests/test_chat_kb_retrieval.py:101: mock_agent.enable_context = False # Disable context to simplify backend/tests/test_chat_kb_retrieval.py:106: mock_agent.restricted_reply = None backend/tests/test_chat_kb_retr
Agent context: I’ve found the chat request builder calls KB retrieval before model streaming, so the clean integration point would be a `WebRetrievalService` invoked in the same preparation step and injected into the prompt as cited context. Next I’m checking the exact prompt assembly and agent config shape.
Agent context: I’ve found the chat request builder calls KB retrieval before model streaming, so the clean integration point would be a `WebRetrievalService` invoked in the same preparation step and injected into the prompt as cited context. Next I’m checking the exact prompt assembly and agent config shape. exit code 0 Output: detail = str(error.detail) if error.status_code == status.HTTP_404_NOT_FOUND: return "NOT_FOUND" if error.status_code == status.HTTP_429_TOO_MANY_REQUESTS: if "Daily message quota exceeded" in detail: return "QUOTA_EXCEEDED" return "RATE_LIMITED" if error.status_code == status.HTTP_403_FORBIDDEN: if detail == WIDGET_ORIGIN_NOT_ALLOWED_DETAIL: return WIDGET_ORIGIN_NOT_ALLOWED_CODE return "FORBIDDEN" if error.status_code == status.HTTP_400_BAD_REQUEST: return "BAD_REQUEST" return "CHAT_ERROR" def get_safe_stream_error_message(code: str) -> str: """Return a client-safe stream error message.""" messages = { "NOT_FOUND": "Requested resource was not found", "QUOTA_EXCEEDED": "Daily message quota exceeded", "RATE_LIMITED": "Rate limit exceeded", WIDGET_ORIGIN_NOT_ALLOWED_CODE: WIDGET_ORIGIN_NOT_ALLOWED_DETAIL, "FORBIDDEN": "Request was denied", "BAD_REQUEST": "Invalid chat request", "PERSISTENC
Agent context: I’ve found the chat request builder calls KB retrieval before model streaming, so the clean integration point would be a `WebRetrievalService` invoked in the same preparation step and injected into the prompt as cited context. Next I’m checking the exact prompt assembly and agent config shape.
Agent context: I’ve found the chat request builder calls KB retrieval before model streaming, so the clean integration point would be a `WebRetrievalService` invoked in the same preparation step and injected into the prompt as cited context. Next I’m checking the exact prompt assembly and agent config shape. exit code 0 Output: 116: "general": """Role: You are an AI chatbot that helps users resolve their inquiries, questions, and requests. Your goal is always to provide high-quality, friendly, and efficient responses. Your responsibility is to carefully listen to users, understand their needs, and do your best to assist them or guide them to appropriate resources. If a question is not sufficiently clear, you should proactively ask clarifying questions. Be sure to maintain a positive and constructive tone at the end of your response. 489:def build_chat_sources(retrieval_results: List[Dict[str, Any]]) -> List[Dict[str, Any]]: 491: sources: List[Dict[str, Any]] = [] 499: sources.append( 508: return sources 514:def replace_source_placeholders(reply: str, sources: List[Dict[str, Any]]) -> str: 522: if source_index < 0 or source_index >= len(sources): 525: source = sources[source_index] 923: "sources": [], 992: sourc
Agent context: I’ve found the chat request builder calls KB retrieval before model streaming, so the clean integration point would be a `WebRetrievalService` invoked in the same preparation step and injected into the prompt as cited context. Next I’m checking the exact prompt assembly and agent config shape.
Agent context: I’ve found the chat request builder calls KB retrieval before model streaming, so the clean integration point would be a `WebRetrievalService` invoked in the same preparation step and injected into the prompt as cited context. Next I’m checking the exact prompt assembly and agent config shape. exit code 0 Output: ChatSession.id.desc(), ) ) return result.scalars().first() async def resolve_admin_chat_session( db: AsyncSession, session_id: str, ) -> Optional[ChatSession]: """Resolve an admin-managed chat session by database primary key.""" result = await db.execute(select(ChatSession).where(ChatSession.id == session_id)) return result.scalar_one_or_none() async def handle_taken_over_chat( session: ChatSession, request: ChatRequest, db: AsyncSession, ) -> None: """Persist visitor messages for taken-over sessions and notify admins.""" user_message = ChatMessage( session_id=session.id, role="user", content=request.message, ) db.add(user_message) session.message_count += 1 session.updated_at = func.now() await db.commit() from services.websocket_service import manager await manager.publish( { "type": "new_message", "sessionId": session.id, "sessionDbId": session.id, "sessionPublicId": session.session_i
Agent context: I’ve found the chat request builder calls KB retrieval before model streaming, so the clean integration point would be a `WebRetrievalService` invoked in the same preparation step and injected into the prompt as cited context. Next I’m checking the exact prompt assembly and agent config shape.
Agent context: I’ve found the chat request builder calls KB retrieval before model streaming, so the clean integration point would be a `WebRetrievalService` invoked in the same preparation step and injected into the prompt as cited context. Next I’m checking the exact prompt assembly and agent config shape. exit code 0 Output: import json import pytest from api.v1.endpoints import replace_source_placeholders def test_replace_source_placeholders_uses_only_url_sources(): reply = "See [website](#source-1), [faq](#source-2), and [missing](#source-5)." sources = [ {"type": "url", "url": "https://example.com/page"}, {"type": "file", "filename": "FAQ"}, ] result = replace_source_placeholders(reply, sources) assert result == "See [website](https://example.com/page), faq, and missing." @pytest.mark.asyncio async def test_get_quota(client): response = await client.get("/api/v1/agent:default") agent_id = response.json()["id"] response = await client.get(f"/api/v1/quota?agent_id={agent_id}") assert response.status_code == 200 data = response.json() assert "max_urls" in data assert "max_files" in data assert "used_urls" in data @pytest.mark.asyncio async def test_chat_stream_sends_sse_events(public_client, default_agent_id
Agent context: A useful detail: the endpoint has a `sources` field and placeholder rewriting, but `prepare_chat_request` currently returns `sources: []` even when KB chunks are used. A web provider should return normalized URL/title/snippet/date objects so both prompt grounding and UI source display can work. A useful detail: the endpoint has a `sources` field and placeholder rewriting, but `prepare_chat_request` currently returns `sources: []` even when KB chunks are used. A web provider should return normalized URL/title/snippet/date objects so both prompt grounding and UI source display can work.
Agent context: A useful detail: the endpoint has a `sources` field and placeholder rewriting, but `prepare_chat_request` currently returns `sources: []` even when KB chunks are used. A web provider should return normalized URL/title/snippet/date objects so both prompt grounding and UI source display can work.
Agent context: A useful detail: the endpoint has a `sources` field and placeholder rewriting, but `prepare_chat_request` currently returns `sources: []` even when KB chunks are used. A web provider should return normalized URL/title/snippet/date objects so both prompt grounding and UI source display can work. exit code 0 Output: workspace_id=agent.workspace_id, used_messages_today=0, last_message_reset=now, ) insert_stmt = insert_stmt.on_conflict_do_nothing( index_elements=["workspace_id"] ) await db.execute(insert_stmt) await db.flush() result = await db.execute( select(WorkspaceQuota) .where(WorkspaceQuota.workspace_id == agent.workspace_id) .with_for_update() ) quota = result.scalar_one_or_none() # Reset daily quota if needed (still holding the lock). if quota.last_message_reset is None or quota.last_message_reset.date() < now.date(): logger.info(f"Resetting daily message quota for workspace {agent.workspace_id}") quota.used_messages_today = 0 quota.last_message_reset = now quota.updated_at = now await db.flush() return quota def build_chat_sources(retrieval_results: List[Dict[str, Any]]) -> List[Dict[str, Any]]: """Build normalized source payloads for chat responses.""" sources: List[Dict[str, Any]] = [] for
Agent context: A useful detail: the endpoint has a `sources` field and placeholder rewriting, but `prepare_chat_request` currently returns `sources: []` even when KB chunks are used. A web provider should return normalized URL/title/snippet/date objects so both prompt grounding and UI source display can work.
Agent context: A useful detail: the endpoint has a `sources` field and placeholder rewriting, but `prepare_chat_request` currently returns `sources: []` even when KB chunks are used. A web provider should return normalized URL/title/snippet/date objects so both prompt grounding and UI source display can work. exit code 0 Output: await handle_taken_over_chat(session, request, prep_db) await prep_db.commit() yield sse_event("sources", {"sources": []}) yield sse_event( "done", { "message_id": None, "session_id": session.session_id, "usage": None, "taken_over": True, }, ) return # Extract needed IDs before closing session session_db_id = session.id session_public_id = session.session_id workspace_id = chat_context["workspace_id"] quota_id = chat_context["quota_id"] llm = chat_context["llm"] messages = chat_context["messages"] sources = chat_context["sources"] temperature = chat_context["temperature"] max_tokens = chat_context["max_tokens"] use_mock_llm = chat_context["use_mock_llm"] # Restricted reply config for graceful LLM failure fallback _agent = chat_context["agent"] _restricted_reply = _agent.restricted_reply logger.info( "chat_stream prepare done agent_id=%s session_id=%s prepare_ms=%.1f", request.agent_id, s
Agent context: A useful detail: the endpoint has a `sources` field and placeholder rewriting, but `prepare_chat_request` currently returns `sources: []` even when KB chunks are used. A web provider should return normalized URL/title/snippet/date objects so both prompt grounding and UI source display can work.
Agent context: A useful detail: the endpoint has a `sources` field and placeholder rewriting, but `prepare_chat_request` currently returns `sources: []` even when KB chunks are used. A web provider should return normalized URL/title/snippet/date objects so both prompt grounding and UI source display can work. exit code 0 Output: # FastAPI 和服务器 fastapi==0.115.0 uvicorn[standard]==0.32.0 websockets==13.1 python-multipart==0.0.12 # 数据库 - SQLite sqlalchemy==2.0.35 aiosqlite==0.20.0 # 认证和安全 python-jose[cryptography]==3.3.0 passlib[bcrypt]==1.7.4 bcrypt==4.0.1 # 固定版本,bcrypt>=4.1.0与passlib不兼容 python-dotenv==1.0.1 # OpenAI API openai==1.54.0 httpx==0.27.2 # Anthropic Claude API anthropic==0.40.0 # Google Gemini API google-generativeai==0.8.3 # URL抓取 - 通过 Scrapling 微服务处理,后端无需额外依赖 requests==2.31.0 # Redis - 缓存、限流、任务队列 redis==5.2.1 aioredis==2.0.1 # CORS - starlette由fastapi自动管理,移除版本锁定 # starlette==0.41.2 # 测试 pytest==8.3.3 pytest-asyncio==0.24.0 # 工具 pydantic==2.10.1 pydantic-settings==2.6.0 # 定时任务 apscheduler==3.10.4 # Qdrant 向量数据库客户端 qdrant-client==1.13.3 # 文档解析 pdfplumber==0.11.0 python-docx==1.1.2 openpyxl==3.1.5 Provider mentions: openai
Agent context: A useful detail: the endpoint has a `sources` field and placeholder rewriting, but `prepare_chat_request` currently returns `sources: []` even when KB chunks are used. A web provider should return normalized URL/title/snippet/date objects so both prompt grounding and UI source display can work.
Agent context: A useful detail: the endpoint has a `sources` field and placeholder rewriting, but `prepare_chat_request` currently returns `sources: []` even when KB chunks are used. A web provider should return normalized URL/title/snippet/date objects so both prompt grounding and UI source display can work. exit code 0 Output: frontend-nextjs/src/views/KnowledgeBaseSetup.tsx:230: {getProviderLabel(kbStatus.embedding_provider)} frontend-nextjs/src/views/KnowledgeBaseSetup.tsx:282: color: kbStatus.embedding_api_key_set frontend-nextjs/src/views/KnowledgeBaseSetup.tsx:287: {kbStatus.embedding_api_key_set frontend-nextjs/src/views/Setup.tsx:47: api_key: apiKey, frontend-nextjs/src/views/Setup.tsx:51: provider_type: 'openai', frontend-nextjs/src/components/AISettingsForm.tsx:59: api_key: "", frontend-nextjs/src/components/AISettingsForm.tsx:61: provider_type: "deepseek" as ProviderType, frontend-nextjs/src/components/AISettingsForm.tsx:112: api_key: "", frontend-nextjs/src/components/AISettingsForm.tsx:114: provider_type: agentData.provider_type || "deepseek", frontend-nextjs/src/components/AISettingsForm.tsx:154: const updatedAgent = await api.updateAgent(agent.id, { api_key: "" }); frontend-nextjs/src/components/ Provider mentions: openai
Query/prompt: Tavily API documentation search extract AI agents official docs Agent context: A useful detail: the endpoint has a `sources` field and placeholder rewriting, but `prepare_chat_request` currently returns `sources: []` even when KB chunks are used. A web provider should return normalized URL/title/snippet/date objects so both prompt grounding and UI source display can work. Query: Tavily API documentation search extract AI agents official docs Agent context: A useful detail: the endpoint has a `sources` field and placeholder rewriting, but `prepare_chat_request` currently returns `sources: []` even when KB chunks are used. A web provider should return normalized URL/title/snippet/date objects so both prompt grounding and UI source display can work. Expanded queries: Tavily API documentation search extract AI agents official docs | Exa API documentation search contents official docs | Brave Search API documentation official docs | SerpApi Google Search API documentation official
Query/prompt: https://docs.tavily.com/ Agent context: A useful detail: the endpoint has a `sources` field and placeholder rewriting, but `prepare_chat_request` currently returns `sources: []` even when KB chunks are used. A web provider should return normalized URL/title/snippet/date objects so both prompt grounding and UI source display can work. Query: https://docs.tavily.com/ Agent context: A useful detail: the endpoint has a `sources` field and placeholder rewriting, but `prepare_chat_request` currently returns `sources: []` even when KB chunks are used. A web provider should return normalized URL/title/snippet/date objects so both prompt grounding and UI source display can work.
Query/prompt: https://docs.tavily.com/ Agent context: A useful detail: the endpoint has a `sources` field and placeholder rewriting, but `prepare_chat_request` currently returns `sources: []` even when KB chunks are used. A web provider should return normalized URL/title/snippet/date objects so both prompt grounding and UI source display can work. Query: https://docs.tavily.com/ Agent context: A useful detail: the endpoint has a `sources` field and placeholder rewriting, but `prepare_chat_request` currently returns `sources: []` even when KB chunks are used. A web provider should return normalized URL/title/snippet/date objects so both prompt grounding and UI source display can work.
Query/prompt: https://docs.tavily.com/documentation/api-reference/endpoint/extract Agent context: A useful detail: the endpoint has a `sources` field and placeholder rewriting, but `prepare_chat_request` currently returns `sources: []` even when KB chunks are used. A web provider should return normalized URL/title/snippet/date objects so both prompt grounding and UI source display can work. Query: https://docs.tavily.com/documentation/api-reference/endpoint/extract Agent context: A useful detail: the endpoint has a `sources` field and placeholder rewriting, but `prepare_chat_request` currently returns `sources: []` even when KB chunks are used. A web provider should return normalized URL/title/snippet/date objects so both prompt grounding and UI source display can work.
Query/prompt: https://api-dashboard.search.brave.com/documentation/services/llm-context Agent context: A useful detail: the endpoint has a `sources` field and placeholder rewriting, but `prepare_chat_request` currently returns `sources: []` even when KB chunks are used. A web provider should return normalized URL/title/snippet/date objects so both prompt grounding and UI source display can work. Query: https://api-dashboard.search.brave.com/documentation/services/llm-context Agent context: A useful detail: the endpoint has a `sources` field and placeholder rewriting, but `prepare_chat_request` currently returns `sources: []` even when KB chunks are used. A web provider should return normalized URL/title/snippet/date objects so both prompt grounding and UI source display can work.
Query/prompt: https://docs.tavily.com/documentation/api-reference/endpoint/search Agent context: A useful detail: the endpoint has a `sources` field and placeholder rewriting, but `prepare_chat_request` currently returns `sources: []` even when KB chunks are used. A web provider should return normalized URL/title/snippet/date objects so both prompt grounding and UI source display can work. Query: https://docs.tavily.com/documentation/api-reference/endpoint/search Agent context: A useful detail: the endpoint has a `sources` field and placeholder rewriting, but `prepare_chat_request` currently returns `sources: []` even when KB chunks are used. A web provider should return normalized URL/title/snippet/date objects so both prompt grounding and UI source display can work.
Query/prompt: https://docs.exa.ai/reference/search Agent context: A useful detail: the endpoint has a `sources` field and placeholder rewriting, but `prepare_chat_request` currently returns `sources: []` even when KB chunks are used. A web provider should return normalized URL/title/snippet/date objects so both prompt grounding and UI source display can work. Query: https://docs.exa.ai/reference/search Agent context: A useful detail: the endpoint has a `sources` field and placeholder rewriting, but `prepare_chat_request` currently returns `sources: []` even when KB chunks are used. A web provider should return normalized URL/title/snippet/date objects so both prompt grounding and UI source display can work.
Query/prompt: OpenAI web search tool Responses API documentation official Agent context: A useful detail: the endpoint has a `sources` field and placeholder rewriting, but `prepare_chat_request` currently returns `sources: []` even when KB chunks are used. A web provider should return normalized URL/title/snippet/date objects so both prompt grounding and UI source display can work. Query: OpenAI web search tool Responses API documentation official Agent context: A useful detail: the endpoint has a `sources` field and placeholder rewriting, but `prepare_chat_request` currently returns `sources: []` even when KB chunks are used. A web provider should return normalized URL/title/snippet/date objects so both prompt grounding and UI source display can work. Expanded queries: OpenAI web search tool Responses API documentation official | Perplexity Sonar API citations search documentation official Provider mentions: openai
Agent context: The external-doc check is done. I consulted current official docs for Tavily, Exa, Brave, SerpApi, OpenAI web search, and Perplexity Sonar; the strongest candidates for this repo are Tavily, Exa, and Brave because they can return retrieval context rather than only a finished answer. The external-doc check is done. I consulted current official docs for Tavily, Exa, Brave, SerpApi, OpenAI web search, and Perplexity Sonar; the strongest candidates for this repo are Tavily, Exa, and Brave because they can return retrieval context rather than only a finished answer. Provider mentions: openai
Agent context: The external-doc check is done. I consulted current official docs for Tavily, Exa, Brave, SerpApi, OpenAI web search, and Perplexity Sonar; the strongest candidates for this repo are Tavily, Exa, and Brave because they can return retrieval context rather than only a finished answer.
Agent context: The external-doc check is done. I consulted current official docs for Tavily, Exa, Brave, SerpApi, OpenAI web search, and Perplexity Sonar; the strongest candidates for this repo are Tavily, Exa, and Brave because they can return retrieval context rather than only a finished answer. exit code 0 Output: 480 logger.info(f"Resetting daily message quota for workspace {agent.workspace_id}") 481 quota.used_messages_today = 0 482 quota.last_message_reset = now 483 quota.updated_at = now 484 await db.flush() 485 486 return quota 487 488 489 def build_chat_sources(retrieval_results: List[Dict[str, Any]]) -> List[Dict[str, Any]]: 490 """Build normalized source payloads for chat responses.""" 491 sources: List[Dict[str, Any]] = [] 492 493 for result in retrieval_results: 494 snippet = result.get("content", "")[:200].strip() 495 if snippet and len(result.get("content", "")) > 200: 496 snippet += "..." 497 498 if result["type"] == "url": 499 sources.append( 500 { 501 "type": "url", 502 "title": result.get("metadata", {}).get("title", "文档"), 503 "url": result.get("metadata", {}).get("url", ""), 504 "snippet": snippet or None, 505 } 506 ) 507 508 return sources 509 510 511 _SOURCE_PLACEHOLDER_PATTERN
Agent context: The external-doc check is done. I consulted current official docs for Tavily, Exa, Brave, SerpApi, OpenAI web search, and Perplexity Sonar; the strongest candidates for this repo are Tavily, Exa, and Brave because they can return retrieval context rather than only a finished answer.
Agent context: The external-doc check is done. I consulted current official docs for Tavily, Exa, Brave, SerpApi, OpenAI web search, and Perplexity Sonar; the strongest candidates for this repo are Tavily, Exa, and Brave because they can return retrieval context rather than only a finished answer. exit code 0 Output: 840 else: 841 temperature = float(temperature) 842 if temperature < 0 or temperature > 2: 843 temperature = agent_temperature 844 845 raw_max_tokens = params.get("max_tokens", agent_max_tokens) 846 max_tokens = raw_max_tokens 847 if isinstance(max_tokens, bool): 848 max_tokens = agent_max_tokens 849 elif isinstance(max_tokens, (int, float)): 850 max_tokens = int(max_tokens) 851 if max_tokens < 1 or max_tokens > 4096: 852 max_tokens = agent_max_tokens 853 else: 854 max_tokens = agent_max_tokens 855 856 logger.info( 857 "chat max_tokens resolved agent_id=%s raw=%r raw_type=%s agent_default=%r final=%r", 858 agent_id, 859 raw_max_tokens, 860 type(raw_max_tokens).__name__, 861 agent_max_tokens, 862 max_tokens, 863 ) 864 865 # KB retrieval (direct Qdrant pipeline, tenant-isolated) 866 kb_context = "" 867 if getattr(agent, "kb_id", None): 868 try: 869 kb_retriever = KbRetrievalService() 870 #
Agent context: The external-doc check is done. I consulted current official docs for Tavily, Exa, Brave, SerpApi, OpenAI web search, and Perplexity Sonar; the strongest candidates for this repo are Tavily, Exa, and Brave because they can return retrieval context rather than only a finished answer.
Agent context: The external-doc check is done. I consulted current official docs for Tavily, Exa, Brave, SerpApi, OpenAI web search, and Perplexity Sonar; the strongest candidates for this repo are Tavily, Exa, and Brave because they can return retrieval context rather than only a finished answer. exit code 0 Output: 1 """KB retrieval service: validate agent/kb/tenant, embed query, Qdrant search + threshold filter.""" 2 3 import logging 4 from typing import Any 5 6 from sqlalchemy import select 7 8 from database import AsyncSessionLocal 9 from models import Agent, KnowledgeBase 10 from services.document_parser import DocumentParser 11 from services.kb_document_processor import get_embedding_api_key 12 from services.kb_service import KbService 13 from services.qdrant_service import QdrantKbService 14 15 logger = logging.getLogger(__name__) 16 17 18 class KbRetrievalService: 19 def __init__(self): 20 self.parser = DocumentParser() 21 self.qdrant = QdrantKbService() 22 self.kb_svc = KbService() 23 self.default_threshold = 0.6 # Fallback default, but agent threshold is preferred 24 25 async def retrieve( 26 self, 27 tenant_id: str | None, 28 agent_id: str, 29 query: str, 30 top_k: int = 5, 31 threshold:
Agent context: The external-doc check is done. I consulted current official docs for Tavily, Exa, Brave, SerpApi, OpenAI web search, and Perplexity Sonar; the strongest candidates for this repo are Tavily, Exa, and Brave because they can return retrieval context rather than only a finished answer.
Agent context: The external-doc check is done. I consulted current official docs for Tavily, Exa, Brave, SerpApi, OpenAI web search, and Perplexity Sonar; the strongest candidates for this repo are Tavily, Exa, and Brave because they can return retrieval context rather than only a finished answer. exit code 0 Output: 85 try: 86 path.parent.mkdir(parents=True, exist_ok=True) 87 path.write_text(agent_id, encoding="utf-8") 88 path.chmod(stat.S_IRUSR | stat.S_IWUSR) 89 except Exception as exc: 90 logger.warning( 91 "Failed to persist agent id to %s: %s.", 92 agent_id_file, 93 exc, 94 ) 95 96 97 def _generate_and_save_agent_id(agent_id_file: str) -> str: 98 agent_id = f"agt_{uuid.uuid4().hex[:12]}" 99 _save_agent_id(agent_id_file, agent_id) 100 logger.info("Generated default agent id file at %s", agent_id_file) 101 return agent_id 102 103 104 class Settings(BaseSettings): 105 """应用配置""" 106 107 model_config = SettingsConfigDict( 108 env_file=".env", 109 case_sensitive=False, 110 extra="allow", 111 ) 112 113 # DeepSeek API (optional - can be set per-agent in dashboard) 114 deepseek_api_key: str = "" 115 116 # Jina Embedding API 117 jina_embedding_api_base: str = "https://api.jina.ai/v1/embeddings" 118 119
Agent context: I’m wrapping the recommendation now. My bias is toward a provider that is model-neutral and returns source material, because Basjoo’s agent can use OpenAI, Gemini, Anthropic, DeepSeek, and OpenAI-compatible vendors. I’m wrapping the recommendation now. My bias is toward a provider that is model-neutral and returns source material, because Basjoo’s agent can use OpenAI, Gemini, Anthropic, DeepSeek, and OpenAI-compatible vendors. Provider mentions: openai
Agent context: I’m wrapping the recommendation now. My bias is toward a provider that is model-neutral and returns source material, because Basjoo’s agent can use OpenAI, Gemini, Anthropic, DeepSeek, and OpenAI-compatible vendors.
Agent context: I’m wrapping the recommendation now. My bias is toward a provider that is model-neutral and returns source material, because Basjoo’s agent can use OpenAI, Gemini, Anthropic, DeepSeek, and OpenAI-compatible vendors. exit code 0 Output: 751 def get_llm_service( 752 agent=None, 753 use_mock: bool = False, 754 *, 755 api_key: Optional[str] = None, 756 api_base: Optional[str] = None, 757 model: Optional[str] = None, 758 provider_type: Optional[str] = None, 759 ) -> BaseLLMService: 760 """ 761 获取 LLM 服务实例(工厂函数) 762 763 根据Agent的服务商类型返回相应的服务实例 764 765 Args: 766 agent: Agent 模型实例(可选) 767 use_mock: 是否使用Mock服务(用于测试) 768 api_key: 显式传入的 API Key 769 api_base: 显式传入的 API Base 770 model: 显式传入的模型名 771 provider_type: 显式传入的服务商类型 772 773 Returns: 774 BaseLLMService: LLM 服务实例 775 """ 776 resolved_api_key = api_key if api_key is not None else getattr(agent, "api_key", None) 777 resolved_api_base = api_base if api_base is not None else getattr(agent, "api_base", None) 778 resolved_model = model if model is not None else getattr(agent, "model", None) 779 resolved_provider_type = provider_type if provider_type is not None else getattr(agent, " Provider mentions: openai
Agent context: I’m wrapping the recommendation now. My bias is toward a provider that is model-neutral and returns source material, because Basjoo’s agent can use OpenAI, Gemini, Anthropic, DeepSeek, and OpenAI-compatible vendors.
Agent context: I’m wrapping the recommendation now. My bias is toward a provider that is model-neutral and returns source material, because Basjoo’s agent can use OpenAI, Gemini, Anthropic, DeepSeek, and OpenAI-compatible vendors. exit code 0 Output: 82 # LLM配置 83 system_prompt = Column( 84 Text, nullable=False, default="You are a helpful customer service assistant." 85 ) 86 model = Column(String(100), nullable=False, default="gpt-4o-mini") 87 temperature = Column(Float, nullable=False, default=0.7) 88 max_tokens = Column(Integer, nullable=False, default=DEFAULT_AGENT_MAX_TOKENS) 89 90 # API配置 91 api_key = Column(String(500), nullable=True) 92 api_base = Column(String(500), nullable=True, default="https://api.openai.com/v1") 93 94 # Jina Embedding API Key 95 jina_api_key = Column(String(500), nullable=True) 96 97 # SiliconFlow Embedding API Key 98 siliconflow_api_key = Column(String(500), nullable=True) 99 100 # AI服务商配置 101 provider_type = Column( 102 SQLEnum( 103 "openai", 104 "openai_native", 105 "google", 106 "anthropic", 107 "xai", 108 "openrouter", 109 "zai", 110 "deepseek", 111 "volcengine", 112 "moonshot", 113 "aliyun_bailian" Provider mentions: openai
Agent context: I’m wrapping the recommendation now. My bias is toward a provider that is model-neutral and returns source material, because Basjoo’s agent can use OpenAI, Gemini, Anthropic, DeepSeek, and OpenAI-compatible vendors.
Agent context: I’m wrapping the recommendation now. My bias is toward a provider that is model-neutral and returns source material, because Basjoo’s agent can use OpenAI, Gemini, Anthropic, DeepSeek, and OpenAI-compatible vendors. exit code 0 Output: 1 # FastAPI 和服务器 2 fastapi==0.115.0 3 uvicorn[standard]==0.32.0 4 websockets==13.1 5 python-multipart==0.0.12 6 7 # 数据库 - SQLite 8 sqlalchemy==2.0.35 9 aiosqlite==0.20.0 10 11 # 认证和安全 12 python-jose[cryptography]==3.3.0 13 passlib[bcrypt]==1.7.4 14 bcrypt==4.0.1 # 固定版本,bcrypt>=4.1.0与passlib不兼容 15 python-dotenv==1.0.1 16 17 # OpenAI API 18 openai==1.54.0 19 httpx==0.27.2 20 21 # Anthropic Claude API 22 anthropic==0.40.0 23 24 # Google Gemini API 25 google-generativeai==0.8.3 26 27 # URL抓取 - 通过 Scrapling 微服务处理,后端无需额外依赖 28 requests==2.31.0 29 30 # Redis - 缓存、限流、任务队列 31 redis==5.2.1 32 aioredis==2.0.1 33 34 # CORS - starlette由fastapi自动管理,移除版本锁定 35 # starlette==0.41.2 36 37 # 测试 38 pytest==8.3.3 39 pytest-asyncio==0.24.0 40 41 # 工具 42 pydantic==2.10.1 43 pydantic-settings==2.6.0 44 45 # 定时任务 Provider mentions: openai
Query/prompt: https://docs.perplexity.ai/docs/sonar/quickstart Agent context: I’m wrapping the recommendation now. My bias is toward a provider that is model-neutral and returns source material, because Basjoo’s agent can use OpenAI, Gemini, Anthropic, DeepSeek, and OpenAI-compatible vendors. Query: https://docs.perplexity.ai/docs/sonar/quickstart Agent context: I’m wrapping the recommendation now. My bias is toward a provider that is model-neutral and returns source material, because Basjoo’s agent can use OpenAI, Gemini, Anthropic, DeepSeek, and OpenAI-compatible vendors. Provider mentions: openai
Agent context: **Provider Choice** I’d choose **Tavily**, specifically **Tavily Search API + Tavily Extract API**. Why: Basjoo already has model-neutral chat generation across OpenAI-compatible, Gemini, Anthropic-style, DeepSeek, etc. The live web layer should therefore be a retrieval service, not a model-specific answer engine. Tavily returns LLM-ready search results with `title`, `url`, `content`, `score`, optional raw content, date/topic/domain controls, and a separate Extract API for deeper page retrieval. That maps cleanly onto the existing chat prep path in [backend/api/v1/endpoints.py](/home/user/worktrees/attempt-6e1b337f-09cb-4dae-aa7e-c824c7b3bf5e-cap-0/backend/api/v1/endpoints.py:865), where KB context is already retrieved before the LLM call, and onto the existing `sources`/URL placeholder machinery in [backend/api/v1/endpoints.py](/home/user/worktrees/attempt-6e1b337f-09cb-4dae-aa7e-c824c7b3bf5e-cap-0/backend/api/v1/endpoints.py:489). I’d add it as a backend `WebRetrievalService`, using direct REST through existing `httpx` in [backend/requirements.txt](/home/user/worktrees/attempt-6e1b337f-09cb-4dae-aa7e-c824c7b3bf5e-cap-0/backend/requirements.txt:17), then inject normalized web snippets plus source metadata into the same system prompt path used for KB context. Tavily docs consulted: [Search API](https://docs.tavily.com/documentation/api-reference/endpoint/search), [Extract API](https://docs.tavily.com/documentation/api-reference/endpoint/extract), [Crawl API](https://docs.tavily.com/documentation/api-reference/endpoint/crawl), [Research API](https://docs.tavily.com/documentation/api-reference/endpoint/research), [Search best practices](https://docs.tavily.com/documentation/best-practices/best-practices-search), and [Usage API](https://docs.tavily.com/documentation/api-reference/endpoint/usage). **Main Alternatives Considered** **Exa**: Strong runner-up. Its search endpoint can search and extract contents, and its Contents API can fetch page text/summaries with cache/live-crawl controls. I’d pick Exa for semantic research over broad web corpora, company/person discovery, or deep content workflows. For Basjoo’s customer-support agent, Tavily’s simpler search/extract split, topic/news/date controls, and agent-oriented snippets are a cleaner first integration. Docs consulted: [Exa Search](https://exa.ai/docs/reference/search), [Exa Contents](https://exa.ai/docs/reference/get-contents), [Exa Answer](https://exa.ai/docs/reference/answer). **Brave Search API**: Very credible, especially the LLM Context endpoint, which returns pre-extracted grounding chunks and source metadata with token-budget controls. I did not choose it as the first provider because Tavily offers a broader managed retrieval toolkit in one vendor: search, extract, crawl, map, and research. Brave is the best cost/control fallback if you want an independent index and compact grounding only. Docs consulted: [Brave Web Search](https://api-dashboard.search.brave.com/app/documentation/web-search/get-started), [Brave LLM Context](https://api-dashboard.search.brave.com/documentation/services/llm-context), [Brave Search API pricing/product page](https://brave.com/search/api/). **SerpApi**: Useful for raw Google/SERP data and local/vertical results, but it is less directly suited to grounding an LLM because it mostly gives SERP structures rather than extracted page context. It would require extra page retrieval, which Basjoo already handles separately through Scrapling. Docs consulted: [SerpApi Google Search API](https://serpapi.com/) and [Search API](https://serpapi.com/search-api). **OpenAI Web Search**: Good for OpenAI-only deployments, but Basjoo’s agent provider layer is intentionally multi-provider in [backend/services/llm_service.py](/home/user/worktrees/attempt-6e1b337f-09cb-4dae-aa7e-c824c7b3bf5e-cap-0/backend/services/llm_service.py:751). Choosing OpenAI web search would couple live grounding to OpenAI Responses/search models instead of working with DeepSeek, Gemini, OpenRouter, etc. Docs consulted: [OpenAI Web Search guide](https://developers.openai.com/api/docs/guides/tools-web-search). **Perplexity Sonar**: Strong if the goal is “web-grounded answer generation,” but less ideal as Basjoo’s general retrieval layer because it wants to be the answering model. Basjoo should preserve its existing LLM abstraction and feed retrieved web context into whichever model the agent is configured to use. Docs consulted: [Perplexity Sonar quickstart](https://docs.perplexity.ai/docs/sonar/quickstart), [Sonar prompt guide](https://docs.perplexity.ai/docs/sonar/prompt-guide). **Provider Choice** I’d choose **Tavily**, specifically **Tavily Search API + Tavily Extract API**. Why: Basjoo already has model-neutral chat generation across OpenAI-compatible, Gemini, Anthropic-style, DeepSeek, etc. The live web layer should therefore be a retrieval service, not a model-specific answer engine. Tavily returns LLM-ready search results with `title`, `url`, `content`, `score`, optional raw content, date/topic/domain controls, and a separate Extract API for deeper page retrieval. That maps cleanly onto the existing chat prep path in [backend/api/v1/endpoints.py](/home/user/worktrees/attempt-6e1b337f-09cb-4dae-aa7e-c824c7b3bf5e-cap-0/backend/api/v1/endpoints.py:865), where KB context is already retrieved before the LLM call, and onto the existing `sources`/URL placeholder machinery in [backend/api/v1/endpoints.py](/home/user/worktrees/attempt-6e1b337f-09cb-4dae-aa7e-c824c7b3bf5e-cap-0/backend/api/v1/endpoints.py:489). I’d add it as a backend `WebRetrievalService`, using direct REST through existing `httpx` in [backend/requirements.txt](/home/user/worktrees/attempt-6e1b337f-09cb-4dae-aa7e-c824c7b3bf5e-cap-0/backend/requirements.txt:17), then inject normalized web snip Provider mentions: openai
**Provider Choice** I’d choose **Tavily**, specifically **Tavily Search API + Tavily Extract API**. Why: Basjoo already has model-neutral chat generation across OpenAI-compatible, Gemini, Anthropic-style, DeepSeek, etc. The live web layer should therefore be a retrieval service, not a model-specific answer engine. Tavily returns LLM-ready search results with `title`, `url`, `content`, `score`, optional raw content, date/topic/domain controls, and a separate Extract API for deeper page retrieval. That maps cleanly onto the existing chat prep path in [backend/api/v1/endpoints.py](/home/user/worktrees/attempt-6e1b337f-09cb-4dae-aa7e-c824c7b3bf5e-cap-0/backend/api/v1/endpoints.py:865), where KB context is already retrieved before the LLM call, and onto the existing `sources`/URL placeholder machinery in [backend/api/v1/endpoints.py](/home/user/worktrees/attempt-6e1b337f-09cb-4dae-aa7e-c824c7b3bf5e-cap-0/backend/api/v1/endpoints.py:489). I’d add it as a backend `WebRetrievalService`, using direct REST through existing `httpx` in [backend/requirements.txt](/home/user/worktrees/attempt-6e1b337f-09cb-4dae-aa7e-c824c7b3bf5e-cap-0/backend/requirements.txt:17), then inject normalized web snippets plus source metadata into the same system prompt path used for KB context. Tavily docs consulted: [Search API](https://docs.tavily.com/documentation/api-reference/endpoint/search), [Extract API]( Provider mentions: openai