Skip to content
Open
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
5 changes: 4 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -88,12 +88,15 @@ TTS_BINDING_API_VERSION=
# ==============================================================================
# Optional: Enable web search capabilities

# [Optional] Provider: perplexity, tavily, serper, jina, exa
# [Optional] Provider: perplexity, tavily, serper, jina, exa, baidu, searxng
SEARCH_PROVIDER=perplexity

# [Optional] API key for your chosen search provider
SEARCH_API_KEY=pplx-xxx

# [Optional] Base URL for self-hosted search providers (e.g., SearXNG)
SEARCH_BASE_URL=

# ==============================================================================
# Cloud Deployment Configuration
# ==============================================================================
Expand Down
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ Contributing to DeepTutor 🚀
Thank you for your interest in contributing to DeepTutor! We are committed to building a smooth and robust intelligent learning companion, and we welcome developers of all skill levels to join us.
Join our community for discussion, support, and collaboration:
<p align="center">
<a href="https://discord.gg/zpP9cssj"><img src="https://img.shields.io/badge/Discord-Join_Community-5865F2?style=for-the-badge&logo=discord&logoColor=white" alt="Discord"></a>&nbsp;
<a href="https://discord.gg/eRsjPgMU4t"><img src="https://img.shields.io/badge/Discord-Join_Community-5865F2?style=for-the-badge&logo=discord&logoColor=white" alt="Discord"></a>&nbsp;
<a href="https://github.com/HKUDS/DeepTutor/issues/78"><img src="https://img.shields.io/badge/WeChat-Join_Group-07C160?style=for-the-badge&logo=wechat&logoColor=white" alt="WeChat"></a>&nbsp;
<a href="./Communication.md"><img src="https://img.shields.io/badge/Feishu-Join_Group-00D4AA?style=for-the-badge&logo=feishu&logoColor=white" alt="Feishu"></a>
</p>
Expand Down
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
[![License](https://img.shields.io/badge/License-AGPL--3.0-blue?style=flat-square)](LICENSE)

<p align="center">
<a href="https://discord.gg/zpP9cssj"><img src="https://img.shields.io/badge/Discord-Join_Community-5865F2?style=for-the-badge&logo=discord&logoColor=white" alt="Discord"></a>
<a href="https://discord.gg/eRsjPgMU4t"><img src="https://img.shields.io/badge/Discord-Join_Community-5865F2?style=for-the-badge&logo=discord&logoColor=white" alt="Discord"></a>
&nbsp;&nbsp;
<a href="./Communication.md"><img src="https://img.shields.io/badge/Feishu-Join_Group-00D4AA?style=for-the-badge&logo=feishu&logoColor=white" alt="Feishu"></a>
&nbsp;&nbsp;
Expand Down Expand Up @@ -287,8 +287,9 @@ cp .env.example .env
| `FRONTEND_PORT` | No | Frontend port (default: `3782`) |
| `NEXT_PUBLIC_API_BASE` | No | **Frontend API URL** - Set this for remote/LAN access (e.g., `http://192.168.1.100:8001`) |
| `TTS_*` | No | Text-to-Speech settings |
| `SEARCH_PROVIDER` | No | Search provider (options: `perplexity`, `tavily`, `serper`, `jina`, `exa`, `baidu`, default: `perplexity`) |
| `SEARCH_PROVIDER` | No | Search provider (options: `perplexity`, `tavily`, `serper`, `jina`, `exa`, `baidu`, `searxng`, default: `perplexity`) |
| `SEARCH_API_KEY` | No | Unified API key for all search providers |
| `SEARCH_BASE_URL` | No | Base URL for self-hosted search providers (e.g., `http://localhost:8888` for SearXNG) |

> 💡 **Remote Access**: If accessing from another device (e.g., `192.168.31.66:3782`), add to `.env`:
> ```bash
Expand Down
14 changes: 7 additions & 7 deletions assets/roster/forkers.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion assets/roster/stargazers.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions src/api/routers/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,11 +70,15 @@ class SearchConfigCreate(ConfigBase):
"""Search configuration for creation.

Uses unified SEARCH_API_KEY environment variable.
SearXNG provider also supports base_url configuration.
"""

api_key: str | Dict[str, str] = Field(
..., description="API key or {'use_env': 'SEARCH_API_KEY'}"
)
base_url: Optional[str | Dict[str, str]] = Field(
None, description="Base URL for self-hosted providers like SearXNG"
)


class ConfigUpdate(BaseModel):
Expand Down
6 changes: 5 additions & 1 deletion src/services/config/unified_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ class ConfigType(str, Enum):
],
ConfigType.EMBEDDING: ["openai", "azure_openai", "ollama", "jina", "cohere", "huggingface"],
ConfigType.TTS: ["openai", "azure_openai"],
ConfigType.SEARCH: ["perplexity", "tavily", "exa", "jina", "serper", "baidu"],
ConfigType.SEARCH: ["perplexity", "tavily", "exa", "jina", "serper", "baidu", "searxng"],
}

# Environment variable mappings for each service type
Expand Down Expand Up @@ -80,6 +80,7 @@ class ConfigType(str, Enum):
ConfigType.SEARCH: {
"provider": "SEARCH_PROVIDER",
"api_key": "SEARCH_API_KEY", # Unified API key for all providers
"base_url": "SEARCH_BASE_URL", # For self-hosted providers like SearXNG
},
}

Expand Down Expand Up @@ -246,6 +247,7 @@ def _build_stored_default_config(
**base_config,
"provider": _get_env_value(env_mapping.get("provider")) or "perplexity",
"api_key": {"use_env": "SEARCH_API_KEY"},
"base_url": {"use_env": "SEARCH_BASE_URL"},
}

return base_config
Expand Down Expand Up @@ -327,6 +329,7 @@ def _build_default_config(self, config_type: ConfigType) -> Dict[str, Any]:
"is_default": True,
"provider": provider,
"api_key": "***",
"base_url": _get_env_value(env_mapping.get("base_url")) or "",
}

return {"id": "default", "name": "Default (from .env)", "is_default": True}
Expand Down Expand Up @@ -374,6 +377,7 @@ def _get_default_config_resolved(self, config_type: ConfigType) -> Dict[str, Any
"id": "default",
"provider": provider,
"api_key": _get_env_value(env_mapping.get("api_key")) or "",
"base_url": _get_env_value(env_mapping.get("base_url")) or "",
}

return {"id": "default"}
Expand Down
23 changes: 19 additions & 4 deletions src/services/search/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
from typing import Any

from src.logging import get_logger
from src.services.config import PROJECT_ROOT, load_config_with_main
from src.services.config import PROJECT_ROOT, get_active_search_config, load_config_with_main

from .base import SEARCH_API_KEY_ENV, BaseSearchProvider
from .consolidation import CONSOLIDATION_TYPES, PROVIDER_TEMPLATES, AnswerConsolidator
Expand Down Expand Up @@ -153,9 +153,16 @@ def web_search(
"provider": "disabled",
}

# Determine provider: function arg > env var > config > default
# Get unified config for active search settings
unified_config = get_active_search_config() or {}

# Determine provider: function arg > unified config > env var > yaml config > default
provider_name = (
provider or os.environ.get("SEARCH_PROVIDER") or config.get("provider") or "perplexity"
provider
or unified_config.get("provider")
or os.environ.get("SEARCH_PROVIDER")
or config.get("provider")
or "perplexity"
).lower()

# Determine consolidation from config if not provided
Expand All @@ -172,8 +179,16 @@ def web_search(
provider_kwargs.setdefault("enable_deep_search", baidu_enable_deep_search)
provider_kwargs.setdefault("search_recency_filter", baidu_search_recency_filter)

# Pass api_key from unified config if available
if unified_config.get("api_key"):
provider_kwargs.setdefault("api_key", unified_config["api_key"])

# Pass base_url from unified config for providers that need it (e.g., SearXNG)
if provider_name == "searxng" and unified_config.get("base_url"):
provider_kwargs.setdefault("base_url", unified_config["base_url"])

# Get provider instance
search_provider = get_provider(provider_name)
search_provider = get_provider(provider_name, **provider_kwargs)

_logger.progress(f"[{search_provider.name}] Searching: {query[:50]}...")

Expand Down
66 changes: 63 additions & 3 deletions src/services/search/consolidation.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,50 @@
{% endfor %}
---
*{{ results|length }} academic papers found via Google Scholar*""",
# -------------------------------------------------------------------------
# SEARXNG TEMPLATE
# -------------------------------------------------------------------------
"searxng": """{% if answers %}
### Direct Answers
{% for answer in answers %}
{{ answer }}
{% endfor %}

---
{% endif %}
{% if infoboxes %}
{% for infobox in infoboxes %}
## {{ infobox.infobox }}{% if infobox.id %} ({{ infobox.id }}){% endif %}

{{ infobox.content }}
{% if infobox.urls %}
{% for url in infobox.urls[:3] %}
- [{{ url.title }}]({{ url.url }})
{% endfor %}
{% endif %}

---
{% endfor %}
{% endif %}
### Search Results for "{{ query }}"

{% for result in results[:max_results] %}
**[{{ loop.index }}] {{ result.title }}**
{{ result.snippet }}
{% if result.date %}*{{ result.date }}*{% endif %}
{% if result.attributes.engine %}*via {{ result.attributes.engine }}*{% endif %}
{{ result.url }}

{% endfor %}
{% if suggestions %}
---
*Suggestions: {% for s in suggestions[:5] %}{{ s }}{% if not loop.last %}, {% endif %}{% endfor %}*
{% endif %}
{% if corrections %}
*Did you mean: {% for c in corrections[:3] %}{{ c }}{% if not loop.last %}, {% endif %}{% endfor %}*
{% endif %}
---
*{{ results|length }} results from SearXNG metasearch*""",
}


Expand All @@ -157,6 +201,7 @@ class AnswerConsolidator:
"serper": "serper",
"jina": "jina",
"serper_scholar": "serper_scholar",
"searxng": "searxng",
}

def __init__(
Expand Down Expand Up @@ -317,6 +362,15 @@ def _build_provider_context(self, response: WebSearchResponse) -> dict[str, Any]
context["links"] = metadata.get("links", {})
context["images"] = metadata.get("images", {})

# -----------------------------------------------------------------
# SEARXNG-specific context
# -----------------------------------------------------------------
elif provider_lower == "searxng":
context["answers"] = metadata.get("answers", [])
context["infoboxes"] = metadata.get("infoboxes", [])
context["suggestions"] = metadata.get("suggestions", [])
context["corrections"] = metadata.get("corrections", [])

return context

def _consolidate_with_template(self, response: WebSearchResponse) -> str:
Expand All @@ -329,13 +383,19 @@ def _consolidate_with_template(self, response: WebSearchResponse) -> str:

# Build context with provider-specific fields
context = self._build_provider_context(response)
_logger.debug(
f"Context has {len(context.get('results', []))} results, {len(context.get('citations', []))} citations"
_logger.info(
f"Context: {len(context.get('results', []))} results, "
f"{len(context.get('citations', []))} citations, max_results={context.get('max_results')}"
)
if context.get("results"):
first_result = context["results"][0]
_logger.debug(
f"First result: title='{first_result.get('title', '')[:50]}', snippet='{first_result.get('snippet', '')[:100]}'..."
)

try:
rendered = template.render(**context)
_logger.debug("Template rendered successfully")
_logger.debug(f"Template rendered ({len(rendered)} chars)")
return rendered
except Exception as e:
_logger.error(f"Template rendering failed: {e}")
Expand Down
2 changes: 1 addition & 1 deletion src/services/search/providers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ def get_default_provider(**kwargs) -> BaseSearchProvider:


# Auto-import all providers to trigger registration
from . import baidu, exa, jina, perplexity, serper, tavily
from . import baidu, exa, jina, perplexity, searxng, serper, tavily

__all__ = [
"register_provider",
Expand Down
Loading