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
3 changes: 2 additions & 1 deletion deeptutor/agents/chat/agentic_pipeline.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@
)
from deeptutor.services.llm.context_window import resolve_effective_context_window
from deeptutor.services.prompt import get_prompt_manager
from deeptutor.services.prompt.language import normalize_language
from deeptutor.tools.builtin import PARTNER_BUILTIN_TOOL_NAMES

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -195,7 +196,7 @@ def __init__(
temperature: float | None = None,
max_tokens: int | None = None,
) -> None:
self.language = "zh" if language.lower().startswith("zh") else "en"
self.language = normalize_language(language)
self.llm_config = get_llm_config()
self.binding = getattr(self.llm_config, "binding", None) or "openai"
self.model = getattr(self.llm_config, "model", None)
Expand Down
2 changes: 1 addition & 1 deletion deeptutor/agents/chat/chat_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ def __init__(
Initialize ChatAgent.

Args:
language: Language setting ('zh' | 'en')
language: Language setting (for example ``"en"``, ``"zh"``, or ``"es"``)
config: Optional configuration dictionary
max_history_tokens: Maximum tokens for conversation history
**kwargs: Additional arguments passed to BaseAgent
Expand Down
4 changes: 2 additions & 2 deletions deeptutor/agents/chat/prompt_blocks.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,15 @@

from deeptutor.capabilities.protocol import PromptBlock
from deeptutor.core.context import UnifiedContext
from deeptutor.services.prompt.language import append_language_directive
from deeptutor.services.prompt.language import append_language_directive, normalize_language


class ChatPromptAssembler:
"""Build system prompts from explicit, category-named blocks."""

def __init__(self, *, prompts: dict[str, Any], language: str) -> None:
self.prompts = prompts
self.language = "zh" if language.lower().startswith("zh") else "en"
self.language = normalize_language(language)

def system_prompt(
self,
Expand Down
9 changes: 5 additions & 4 deletions deeptutor/agents/notebook/analysis_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from deeptutor.core.trace import build_trace_metadata, derive_trace_metadata, new_call_id
from deeptutor.services.llm import clean_thinking_tags, get_llm_config, get_token_limit_kwargs
from deeptutor.services.llm import stream as llm_stream
from deeptutor.services.prompt.language import append_language_directive, normalize_language
from deeptutor.services.prompt.manager import get_prompt_manager
from deeptutor.utils.json_parser import parse_json_response

Expand All @@ -28,7 +29,7 @@ class NotebookAnalysisAgent:
"""Analyze selected notebook records before the main capability runs."""

def __init__(self, language: str = "en") -> None:
self.language = "zh" if str(language or "en").lower().startswith("zh") else "en"
self.language = normalize_language(language)
self.llm_config = get_llm_config()
self.model = getattr(self.llm_config, "model", None)
self.api_key = getattr(self.llm_config, "api_key", None)
Expand Down Expand Up @@ -296,13 +297,13 @@ def _stage_text(self, stage: str, field: str) -> str:
return str(section.get(field, "")).strip()

def _thinking_system_prompt(self) -> str:
return self._stage_text("thinking", "system")
return append_language_directive(self._stage_text("thinking", "system"), self.language)

def _acting_system_prompt(self) -> str:
return self._stage_text("acting", "system")
return append_language_directive(self._stage_text("acting", "system"), self.language)

def _observing_system_prompt(self) -> str:
return self._stage_text("observing", "system")
return append_language_directive(self._stage_text("observing", "system"), self.language)

def _thinking_prompt(self, user_question: str, records: list[dict[str, Any]]) -> str:
return self._stage_text("thinking", "user_template").format(
Expand Down
7 changes: 5 additions & 2 deletions deeptutor/agents/notebook/summarize_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

from deeptutor.services.llm import clean_thinking_tags, get_llm_config, get_token_limit_kwargs
from deeptutor.services.llm import stream as llm_stream
from deeptutor.services.prompt.language import append_language_directive, normalize_language
from deeptutor.services.prompt.manager import get_prompt_manager


Expand All @@ -20,7 +21,7 @@ class NotebookSummarizeAgent:
"""Generate concise summaries for notebook records."""

def __init__(self, language: str = "en") -> None:
self.language = "zh" if str(language or "en").lower().startswith("zh") else "en"
self.language = normalize_language(language)
self.llm_config = get_llm_config()
self.model = getattr(self.llm_config, "model", None)
self.api_key = getattr(self.llm_config, "api_key", None)
Expand Down Expand Up @@ -93,7 +94,9 @@ async def stream_summary(
yield chunk

def _system_prompt(self) -> str:
return str(self._prompts.get("system", "")).strip()
return append_language_directive(
str(self._prompts.get("system", "")).strip(), self.language
)

def _build_user_prompt(
self,
Expand Down
10 changes: 4 additions & 6 deletions deeptutor/api/routers/chat.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from deeptutor.agents.chat import ChatAgent, SessionManager
from deeptutor.core.context import UnifiedContext
from deeptutor.core.stream import StreamEventType
from deeptutor.i18n.languages import normalize_supported_language
from deeptutor.runtime.orchestrator import ChatOrchestrator
from deeptutor.services.config import PROJECT_ROOT, load_config_with_main
from deeptutor.services.llm.config import get_llm_config
Expand Down Expand Up @@ -108,12 +109,9 @@ async def websocket_chat(websocket: WebSocket):
while True:
data = await websocket.receive_json()
requested_language = str(data.get("language") or "").lower().strip()
language = (
"zh"
if requested_language.startswith("zh")
else "en"
if requested_language.startswith("en")
else get_response_language(default=config.get("system", {}).get("language", "en"))
language = normalize_supported_language(
requested_language
or get_response_language(default=config.get("system", {}).get("language", "en"))
)
message = data.get("message", "").strip()
session_id = data.get("session_id")
Expand Down
40 changes: 36 additions & 4 deletions deeptutor/api/routers/quiz_judge.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

from fastapi import APIRouter, WebSocket, WebSocketDisconnect

from deeptutor.i18n.languages import normalize_supported_language
from deeptutor.services.config import PROJECT_ROOT, load_config_with_main
from deeptutor.services.llm import stream as llm_stream
from deeptutor.services.settings.interface_settings import get_response_language
Expand Down Expand Up @@ -47,6 +48,17 @@
"- Speak directly to the learner's submission — do not give a generic lecture.\n"
"- Reply in English."
),
"es": (
"Eres un asistente docente riguroso y alentador que corrige la respuesta de un estudiante. "
"Usa la pregunta, la respuesta de referencia y la explicación para ofrecer una evaluación concreta.\n\n"
"Requisitos:\n"
"- Empieza con una línea que indique el veredicto: ✅ Correcta / ⚠️ Parcialmente correcta / ❌ Incorrecta, "
"junto con el motivo principal.\n"
"- Enumera después qué está bien, qué es incorrecto o falta y cómo corregirlo.\n"
"- Si existen varias respuestas razonables, reconoce los aciertos del estudiante.\n"
"- Habla directamente sobre la respuesta entregada; evita explicaciones genéricas.\n"
"- Responde siempre en español de España."
),
}


Expand Down Expand Up @@ -95,6 +107,27 @@ def _build_judge_user_prompt(
)
parts.append(f"{count_text},请结合图片中的文字/公式/草图一并判定。")
parts.append("请针对该学习者的具体作答给出 AI 评判。")
elif language == "es":
parts = [
f"Tipo de pregunta: {question_type or 'desconocido'}",
f"Pregunta:\n{question}",
]
if options_block:
parts.append(f"Opciones:\n{options_block}")
if correct_answer:
parts.append(f"Respuesta de referencia:\n{correct_answer}")
if explanation:
parts.append(f"Explicación de referencia:\n{explanation}")
parts.append(
"Respuesta del estudiante:\n"
+ (
user_answer.strip()
if user_answer and user_answer.strip()
else "(Solo se han enviado imágenes, sin respuesta escrita)"
)
)
if has_image:
parts.append(f"Imágenes adjuntas: {image_count}")
else:
parts = [
f"Question type: {question_type or 'unknown'}",
Expand Down Expand Up @@ -218,7 +251,7 @@ async def websocket_quiz_judge(websocket: WebSocket):
] | null,
"user_answer_image": str | null, # legacy single-image form
"image_filename": str | null, # legacy filename for the above
"language": "zh" | "en",
"language": "en" | "zh" | "es",
}

Server → Client (streaming):
Expand Down Expand Up @@ -275,12 +308,11 @@ async def safe_send(payload: dict[str, Any]) -> bool:
return

requested_language = (data.get("language") or "").strip().lower()
if requested_language not in ("zh", "en"):
if requested_language not in ("zh", "en", "es"):
requested_language = get_response_language(
default=_config.get("system", {}).get("language", "en")
)
if requested_language not in ("zh", "en"):
requested_language = "en"
requested_language = normalize_supported_language(requested_language)

user_answer = data.get("user_answer") or ""

Expand Down
10 changes: 5 additions & 5 deletions deeptutor/api/routers/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,8 +121,8 @@ class SidebarNavOrder(BaseModel):

class UISettings(BaseModel):
theme: Literal["light", "dark", "glass", "snow"] = "snow"
language: Literal["zh", "en"] = "en"
response_language: Literal["zh", "en"] = "en"
language: Literal["en", "zh", "es"] = "en"
response_language: Literal["en", "zh", "es"] = "en"
sidebar_description: Optional[str] = None
sidebar_nav_order: Optional[SidebarNavOrder] = None
code_block_theme: Optional[str] = None
Expand All @@ -144,8 +144,8 @@ class UISettingsUpdate(BaseModel):
# for exclude_unset partial merges, but an explicit value is still validated
# so PUT /ui cannot persist a theme/language the app can't render.
theme: Literal["light", "dark", "glass", "snow"] | None = None
language: Literal["zh", "en"] | None = None
response_language: Literal["zh", "en"] | None = None
language: Literal["en", "zh", "es"] | None = None
response_language: Literal["en", "zh", "es"] | None = None
sidebar_description: str | None = None
sidebar_nav_order: SidebarNavOrder | None = None
code_block_theme: str | None = None
Expand All @@ -166,7 +166,7 @@ class ThemeUpdate(BaseModel):


class LanguageUpdate(BaseModel):
language: Literal["zh", "en"]
language: Literal["en", "zh", "es"]


class SidebarDescriptionUpdate(BaseModel):
Expand Down
6 changes: 3 additions & 3 deletions deeptutor/api/routers/tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
from __future__ import annotations

import logging
from typing import Any, Literal
from typing import Any

from fastapi import APIRouter
from pydantic import BaseModel
Expand Down Expand Up @@ -61,9 +61,9 @@ class ToolHintsPayload(BaseModel):
class BuiltinToolPayload(BaseModel):
name: str
description: str
description_i18n: dict[Literal["en", "zh"], str] = {}
description_i18n: dict[str, str] = {}
parameters: list[ToolParameterPayload]
hints: dict[Literal["en", "zh"], ToolHintsPayload]
hints: dict[str, ToolHintsPayload]
aliases: list[str] = []
# True iff the user is allowed to switch this tool on/off from the
# /settings/tools UI. Locked-on tools (auto-mounted by the chat
Expand Down
1 change: 1 addition & 0 deletions deeptutor/api/utils/tool_options.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ def _describe(name: str) -> dict[str, Any]:
"description_i18n": {
"en": definition.description or "",
"zh": definition.description or "",
"es": definition.description or "",
},
}
)
Expand Down
7 changes: 4 additions & 3 deletions deeptutor/capabilities/explore_context/explorer.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
from deeptutor.services.llm import clean_thinking_tags, get_llm_config, get_token_limit_kwargs
from deeptutor.services.llm import stream as llm_stream
from deeptutor.services.llm.capabilities import threads_session_id
from deeptutor.services.prompt.language import append_language_directive, normalize_language

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -89,7 +90,7 @@ class ContextExplorer:
"""Investigate the turn's attached sources and return an objective briefing."""

def __init__(self, *, language: str, prompts: dict[str, Any]) -> None:
self.language = "zh" if str(language or "en").lower().startswith("zh") else "en"
self.language = normalize_language(language)
self._prompts = prompts or {}
cfg = get_llm_config()
self.model = getattr(cfg, "model", None)
Expand Down Expand Up @@ -156,7 +157,7 @@ async def _run_loop(
source_index: dict[str, str],
usage: Any | None,
) -> str:
system_prompt = self._t("loop.system")
system_prompt = append_language_directive(self._t("loop.system"), self.language)
user_template = self._t("loop.user_template")
if not system_prompt or not user_template:
logger.warning("explore_context loop prompts missing; using single pass")
Expand Down Expand Up @@ -381,7 +382,7 @@ async def _single_pass(
sources_text = self._render_source_blocks(source_index)
if not sources_text:
return ""
system_prompt = self._t("system")
system_prompt = append_language_directive(self._t("system"), self.language)
user_template = self._t("user_template")
if not system_prompt or not user_template:
logger.warning("explore_context single-pass prompts missing; skipping pre-pass")
Expand Down
2 changes: 1 addition & 1 deletion deeptutor/core/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ class UnifiedContext:
knowledge_bases: KB names to use for RAG.
attachments: Images / files sent with the message.
config_overrides: Per-request config tweaks (e.g. temperature).
language: UI / response language ("en" | "zh").
language: UI / response language (for example ``"en"``, ``"zh"``, or ``"es"``).
memory_context: Memory snapshot text injected into the system prompt.
persona_context: Selected persona's instructions, eagerly injected
into the system prompt (a persona must shape the voice from the
Expand Down
63 changes: 59 additions & 4 deletions deeptutor/core/i18n.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,11 @@

from typing import Any

from deeptutor.i18n.languages import normalize_supported_language


def _parse_language(language: str | None) -> str:
raw = (language or "en").strip().lower()
if raw.startswith("zh") or raw in {"cn", "chinese"}:
return "zh"
return "en"
return normalize_supported_language(language)


_MESSAGES: dict[str, dict[str, str]] = {
Expand Down Expand Up @@ -107,6 +106,62 @@ def _parse_language(language: str | None) -> str:
"sandbox.disabled_for_account": "你的账号已禁用代码执行。",
"sandbox.no_backend": "没有可用的沙箱后端",
},
"es": {
"api.content_required": "el contenido es obligatorio",
"api.invalid_channels_config": "La configuración de canales no es válida",
"api.partner_already_exists": "El compañero '{name}' ya existe",
"api.partner_not_found": "No se ha encontrado el compañero",
"api.partner_not_found_or_not_running": "No se ha encontrado el compañero o no está en ejecución",
"api.partner_not_running": "El compañero no está en ejecución",
"api.partner_stopped_start_required": "El compañero está detenido. Inícialo antes de chatear.",
"api.persona_already_exists": "La persona ya existe: {name}",
"api.persona_name_required": "El nombre de la persona es obligatorio",
"api.persona_not_found": "No se ha encontrado la persona: {name}",
"api.soul_already_exists": "El perfil '{name}' ya existe",
"api.soul_content_empty": "El contenido del perfil personalizado está vacío",
"api.soul_library_not_found": "No se ha encontrado el perfil '{name}' en la biblioteca",
"api.soul_not_found": "No se ha encontrado el perfil",
"api.tool_not_found": "No se ha encontrado la herramienta '{name}'",
"cli_apps.abi_mismatch": (
"La aplicación CLI {app!r} se instaló para {installed}, pero este entorno "
"usa {current}. Un administrador debe reinstalarla."
),
"cli_apps.args_required": (
"{tool} necesita un array 'args', con un argumento de la línea de comandos "
"por elemento."
),
"cli_apps.entry_admin_only": (
"Las aplicaciones CLI las instala un administrador; pídele que añada esta."
),
"cli_apps.install_in_progress": "Esa aplicación ya se está instalando.",
"cli_apps.not_in_catalog": "No hay ninguna aplicación CLI llamada {id!r} en el catálogo.",
"cli_apps.not_installed": (
"La aplicación CLI {app!r} ya no está instalada en este despliegue."
),
"cli_apps.still_running": "{app} sigue en ejecución ({seconds} s)",
"mcp.configure_command_or_url": "Servidor {name!r}: configura un comando (stdio) o una URL.",
"mcp.configure_before_testing": "Configura un comando (stdio) o una URL antes de probar.",
"mcp.server_error": "Servidor {name!r}: {error}",
"mcp.server_missing": "No hay ningún servidor llamado {name!r} en tu lista.",
"mcp.not_oauth": "Este servidor no usa OAuth; proporciona una credencial.",
"mcp.oauth_callback_incomplete": "La respuesta de autorización estaba incompleta.",
"mcp.oauth_callback_unknown": (
"Esa autorización ha caducado o ya se ha completado. Iníciala de nuevo."
),
"mcp.oauth_done": "Autorización completada. Ya puedes cerrar esta pestaña.",
"mcp.oauth_failed": "La autorización ha fallado.",
"mcp.catalog_entry_missing": "No hay ningún servicio MCP llamado {id!r} en el catálogo.",
"mcp.entry_admin_only": (
"Este servicio se ejecuta como un comando local y solo puede añadirlo un administrador."
),
"mcp.tool_not_available": (
"Esta herramienta no está disponible en esta conversación. "
"Solo se pueden usar las herramientas incluidas en el prompt."
),
"sandbox.command_blocked": "Error: el comando ha sido bloqueado por la protección de seguridad.",
"sandbox.disabled_for_account": "La ejecución de código está desactivada para tu cuenta.",
"sandbox.no_backend": "no hay ningún entorno aislado disponible",
},
}


Expand Down
Loading