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
37 changes: 34 additions & 3 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,7 @@ COPY --from=frontend-builder /app/web/public/ ./web/public/

# Copy application source code
COPY deeptutor/ ./deeptutor/
COPY deeptutor_compat/ ./deeptutor_compat/
COPY deeptutor_cli/ ./deeptutor_cli/
COPY scripts/ ./scripts/
COPY pyproject.toml ./
Expand All @@ -180,7 +181,7 @@ RUN mkdir -p \
data/user/logs \
data/knowledge_bases

# Create supervisord configuration for running both services
# Create supervisord configuration for running backend + frontend + compat gateway
# Log output goes to stdout/stderr so docker logs can capture them
RUN mkdir -p /etc/supervisor/conf.d

Expand Down Expand Up @@ -213,6 +214,18 @@ stdout_logfile_maxbytes=0
stderr_logfile=/dev/fd/2
stderr_logfile_maxbytes=0
environment=NODE_ENV="production"

[program:compat-gateway]
command=/bin/bash /app/start-compat-gateway.sh
directory=/app
autostart=true
autorestart=true
startsecs=2
stdout_logfile=/dev/fd/1
stdout_logfile_maxbytes=0
stderr_logfile=/dev/fd/2
stderr_logfile_maxbytes=0
environment=PYTHONPATH="/app",PYTHONUNBUFFERED="1"
EOF

RUN sed -i 's/\r$//' /etc/supervisor/conf.d/deeptutor.conf
Expand Down Expand Up @@ -279,6 +292,20 @@ EOF

RUN sed -i 's/\r$//' /app/start-frontend.sh && chmod +x /app/start-frontend.sh

# Create compat gateway startup script (single-port entrypoint for dynamic_router)
RUN cat > /app/start-compat-gateway.sh <<'EOF'
#!/bin/bash
set -e

GATEWAY_PORT=${GATEWAY_PORT:-3000}

echo "[Compat] 🚪 Starting compat gateway on port ${GATEWAY_PORT}..."

exec python -m uvicorn deeptutor_compat.gateway:app --host 0.0.0.0 --port ${GATEWAY_PORT}
EOF

RUN sed -i 's/\r$//' /app/start-compat-gateway.sh && chmod +x /app/start-compat-gateway.sh

# Create entrypoint script
RUN cat > /app/entrypoint.sh <<'EOF'
#!/bin/bash
Expand All @@ -291,9 +318,13 @@ echo "============================================"
# Set default ports if not provided
export BACKEND_PORT=${BACKEND_PORT:-8001}
export FRONTEND_PORT=${FRONTEND_PORT:-3782}
export GATEWAY_PORT=${GATEWAY_PORT:-3000}
export DEEPTUTOR_WORKSPACE_ROOT=${DEEPTUTOR_WORKSPACE_ROOT:-/workspace/deeptutor}

echo "📌 Backend Port: ${BACKEND_PORT}"
echo "📌 Frontend Port: ${FRONTEND_PORT}"
echo "📌 Gateway Port: ${GATEWAY_PORT}"
echo "📌 DeepTutor Data Root: ${DEEPTUTOR_WORKSPACE_ROOT}"

# Check for required environment variables
if [ -z "$LLM_API_KEY" ]; then
Expand Down Expand Up @@ -329,11 +360,11 @@ EOF
RUN sed -i 's/\r$//' /app/entrypoint.sh && chmod +x /app/entrypoint.sh

# Expose ports
EXPOSE 8001 3782
EXPOSE 3000 8001 3782

# Health check
HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \
CMD curl -f http://localhost:${BACKEND_PORT:-8001}/ || exit 1
CMD curl -f http://localhost:${GATEWAY_PORT:-3000}/health || exit 1

# Set entrypoint
ENTRYPOINT ["/app/entrypoint.sh"]
Expand Down
6 changes: 5 additions & 1 deletion deeptutor/agents/chat/agentic_pipeline.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
)
from deeptutor.services.prompt import get_prompt_manager
from deeptutor.services.prompt.language import append_language_directive
from deeptutor.services.prompt.oxca_brand import apply_student_guardrail
from deeptutor.tools.builtin import BUILTIN_TOOL_NAMES
from deeptutor.utils.json_parser import parse_json_response

Expand Down Expand Up @@ -1416,7 +1417,10 @@ def _responding_system_prompt(self, enabled_tools: list[str]) -> str:
tool_list=tool_list or self._fallback_empty_tool_list(),
rag_hint=rag_hint,
)
return append_language_directive(system_prompt, self.language)
return apply_student_guardrail(
append_language_directive(system_prompt, self.language),
self.language,
)

def _acting_user_prompt(self, context: UnifiedContext, thinking_text: str) -> str:
return self._t(
Expand Down
8 changes: 6 additions & 2 deletions deeptutor/agents/chat/chat_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
from deeptutor.agents.base_agent import BaseAgent
from deeptutor.runtime.registry.tool_registry import get_tool_registry
from deeptutor.services.prompt.language import append_language_directive
from deeptutor.services.prompt.oxca_brand import apply_student_guardrail


class ChatAgent(BaseAgent):
Expand Down Expand Up @@ -247,8 +248,11 @@ def build_messages(
messages = []

system_parts = [
append_language_directive(
self.get_prompt("system", "You are a helpful AI assistant."),
apply_student_guardrail(
append_language_directive(
self.get_prompt("system", "You are OxCa's AI learning assistant."),
self.language,
),
self.language,
)
]
Expand Down
7 changes: 4 additions & 3 deletions deeptutor/agents/chat/prompts/en/agentic_chat.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -99,18 +99,19 @@ observing:
# ---------------------------------------------------------------------------
responding:
system: |-
You are DeepTutor's final response stage. Use the observation and tool evidence to provide a clear, direct, well-structured answer to the user.
You are OxCa's AI tutor delivering the final answer to the student. Use the observation and tool evidence to provide a clear, direct, well-structured answer.

Requirements:
1. Output only the final user-facing answer.
2. Do not reveal the internal chain, reasoning, or tool orchestration.
3. Naturally integrate evidence or limits surfaced by the tools.
3. Never mention DeepTutor, HKU, or any underlying platform or engine.
4. Naturally integrate evidence or limits surfaced by the tools.
{rag_hint}
Tool context for this turn:
{tool_list}
# Injected at {rag_hint} when RAG is enabled
rag_hint: |-
4. If RAG was used, follow the observation's judgment on how to weigh retrieved content vs. general knowledge. When the retrieved content is highly relevant, ground your answer in it; when only partially relevant, supplement with your own knowledge.
5. If RAG was used, follow the observation's judgment on how to weigh retrieved content vs. general knowledge. When the retrieved content is highly relevant, ground your answer in it; when only partially relevant, supplement with your own knowledge.
user: |-
User request:
{user_message}
Expand Down
2 changes: 1 addition & 1 deletion deeptutor/agents/chat/prompts/en/chat_agent.yaml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Chat Agent Prompts (English)

system: |
You are DeepTutor, an intelligent AI learning assistant developed by the Data Intelligence Lab at HKU.
You are OxCa's AI learning assistant (牛剑通途), an intelligent tutor that helps students learn STEM subjects and prepare for exams.

Your capabilities:
- Help students understand complex concepts across STEM subjects
Expand Down
7 changes: 4 additions & 3 deletions deeptutor/agents/chat/prompts/zh/agentic_chat.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -99,18 +99,19 @@ observing:
# ---------------------------------------------------------------------------
responding:
system: |-
你是 DeepTutor 的最终回答阶段。请根据 observation 和工具结果,给用户一个清晰、直接、结构良好的正式答复。
你是 OxCa 的 AI 导师,正在给学生输出最终答复。请根据 observation 和工具结果,给出清晰、直接、结构良好的正式答复。

要求:
1. 只输出面向用户的正式回答。
2. 不要暴露内部链路、思考过程或工具编排。
3. 若工具结果提供了证据或限制,请自然融入答案。
3. 不要提及 DeepTutor、港大或任何底层平台/引擎。
4. 若工具结果提供了证据或限制,请自然融入答案。
{rag_hint}
本轮工具背景:
{tool_list}
# 当 RAG 工具被启用时,插入到 system 的 {rag_hint} 占位处
rag_hint: |-
4. 若使用了知识库检索,请根据 observation 的判断,合理权衡检索内容与通识。当检索内容与问题高度相关时,以其为核心依据;部分相关时,可结合自身知识补充。
5. 若使用了知识库检索,请根据 observation 的判断,合理权衡检索内容与通识。当检索内容与问题高度相关时,以其为核心依据;部分相关时,可结合自身知识补充。
user: |-
用户问题:
{user_message}
Expand Down
2 changes: 1 addition & 1 deletion deeptutor/agents/chat/prompts/zh/chat_agent.yaml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Chat Agent Prompts (中文)

system: |
你是 DeepTutor,一个由香港大学数据智能实验室开发的智能 AI 学习助手。
你是 OxCa(牛剑通途)的 AI 学习助手,帮助学生理解 STEM 学科并备考

你的能力:
- 帮助学生理解 STEM 领域的复杂概念
Expand Down
7 changes: 6 additions & 1 deletion deeptutor/agents/question/agents/idea_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,13 +49,18 @@ async def process(
existing_concentrations: list[str] | None = None,
batch_number: int | None = None,
attachments: list[Any] | None = None,
history_context: str = "",
) -> dict[str, Any]:
"""
Build grounded question templates in a single pass.
"""
batch_size = max(1, min(int(num_ideas or 1), BATCH_SIZE))
trace_id = f"batch-{batch_number}" if batch_number is not None else "ideation"
if self.enable_rag and self.kb_name:
attached_history = str(history_context or "").strip()
if attached_history:
knowledge_context = attached_history
retrievals: list[dict[str, Any]] = []
elif self.enable_rag and self.kb_name:
retrievals = await self._retrieve_context(
user_topic,
trace_id=trace_id,
Expand Down
1 change: 1 addition & 0 deletions deeptutor/agents/question/coordinator.py
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,7 @@ async def generate_from_topic(
existing_concentrations=existing_concentrations,
batch_number=batch_number,
attachments=attachments,
history_context=history_context,
)
batch_templates = idea_result.get("templates", [])
if not isinstance(batch_templates, list):
Expand Down
2 changes: 1 addition & 1 deletion deeptutor/agents/question/prompts/en/answer_now.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
system: |
You are DeepTutor's question generator. The user is waiting, so produce
You are OxCa's quiz generator. The user is waiting, so produce
a complete question set in one shot using the context already gathered.
Output strictly the JSON schema {"questions": [{"question_id": "q_1",
"question": "...", "question_type": "choice|written|coding", "options":
Expand Down
2 changes: 1 addition & 1 deletion deeptutor/agents/question/prompts/zh/answer_now.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
system: |
你是 DeepTutor 的题目生成器。用户已经在等待,请基于现有信息直接输出一组题目。
你是 OxCa 的题目生成器。用户已经在等待,请基于现有信息直接输出一组题目。
严格输出 JSON:{"questions": [{"question_id": "q_1",
"question": "...", "question_type": "choice|written|coding",
"options": {"A": "..."}, "correct_answer": "...",
Expand Down
2 changes: 1 addition & 1 deletion deeptutor/agents/research/prompts/en/answer_now.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
system: |
You are DeepTutor's research-report writer. The user is waiting, so
You are OxCa's research-report writer. The user is waiting, so
produce a structured research report right now from whatever evidence
has streamed so far (rephrase, decompose, search hits, notes/outline,
...). Do not retrieve more evidence and do not call tools. If coverage
Expand Down
2 changes: 1 addition & 1 deletion deeptutor/agents/research/prompts/zh/answer_now.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
system: |
你是 DeepTutor 的研究报告写作组件。用户已经在等待,
你是 OxCa 的研究报告写作组件。用户已经在等待,
请根据当前已经收集到的研究 trace(包括 rephrase、decompose、检索结果、
笔记/纲要等)直接输出一篇结构清晰的研究报告。
不要再继续检索或调用工具。如果证据稀薄,请在报告中标注信息覆盖度。
Expand Down
2 changes: 1 addition & 1 deletion deeptutor/agents/solve/prompts/en/answer_now.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
system: |
You are the writer component of DeepTutor. The user is already waiting,
You are OxCa's answer writer. The user is already waiting,
so produce the final user-facing answer right now using only the partial
reasoning trace that has streamed so far. Do not plan further or call
tools, and do not mention internal stages. If something is still
Expand Down
2 changes: 1 addition & 1 deletion deeptutor/agents/solve/prompts/zh/answer_now.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
system: |
你是 DeepTutor 的写作组件。用户已经在等待,
你是 OxCa 的写作组件。用户已经在等待,
你必须基于当前已经收集到的推理与工具调用轨迹直接输出最终答复。
不要再做新的规划或调用工具,不要提到内部阶段。
如果信息仍有缺口,请诚实说明不确定之处,但仍尽可能给出当前最有用的回答。
Expand Down
2 changes: 1 addition & 1 deletion deeptutor/agents/visualize/prompts/en/answer_now.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
system: |
You are DeepTutor's visualization code generator. The user is waiting,
You are OxCa's visualization code generator. The user is waiting,
so emit the final renderable code in one shot. Output strictly the JSON
{"render_type": "svg|chartjs|mermaid", "code": "..."}, where ``code`` is
the renderable source (SVG markup, Chart.js JS, or Mermaid DSL).
Expand Down
2 changes: 1 addition & 1 deletion deeptutor/agents/visualize/prompts/zh/answer_now.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
system: |
你是 DeepTutor 的可视化代码生成器。用户已经在等待,
你是 OxCa 的可视化代码生成器。用户已经在等待,
请直接输出最终可渲染的代码。
严格输出 JSON:{"render_type": "svg|chartjs|mermaid",
"code": "..."}。
Expand Down
4 changes: 2 additions & 2 deletions deeptutor/api/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,7 @@ async def lifespan(app: FastAPI):


app = FastAPI(
title="DeepTutor API",
title="OxCa Learning API",
version="1.0.0",
lifespan=lifespan,
# Disable automatic trailing slash redirects to prevent protocol downgrade issues
Expand Down Expand Up @@ -364,7 +364,7 @@ async def selective_access_log(request, call_next):

@app.get("/")
async def root():
return {"message": "Welcome to DeepTutor API"}
return {"message": "OxCa learning API"}


if __name__ == "__main__":
Expand Down
22 changes: 21 additions & 1 deletion deeptutor/api/routers/question_notebook.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,8 @@ class CategoryAddRequest(BaseModel):

class UpsertEntryRequest(BaseModel):
session_id: str
"""When the session does not exist yet (e.g. OxCa Levels practice), create it."""
session_title: str | None = None
question_id: str
question: str
question_type: str = ""
Expand All @@ -81,14 +83,32 @@ class UpsertEntryRequest(BaseModel):
is_correct: bool = False


class EnsureSessionRequest(BaseModel):
session_id: str
title: str = Field(default="Practice notebook", min_length=1, max_length=100)


# ── Entry endpoints ──────────────────────────────────────────────


@router.post("/sessions/ensure")
async def ensure_notebook_session(payload: EnsureSessionRequest):
"""Create an empty chat session used to group notebook entries (Levels practice)."""
store = get_sqlite_session_store()
if await store.get_session(payload.session_id) is None:
await store.create_session(title=payload.title[:100], session_id=payload.session_id)
return {"session_id": payload.session_id}


@router.post("/entries/upsert")
async def upsert_single_entry(payload: UpsertEntryRequest):
store = get_sqlite_session_store()
if await store.get_session(payload.session_id) is None:
title = (payload.session_title or "Practice notebook").strip() or "Practice notebook"
await store.create_session(title=title[:100], session_id=payload.session_id)
entry_payload = payload.model_dump(exclude={"session_title"})
try:
await store.upsert_notebook_entries(payload.session_id, [payload.model_dump()])
await store.upsert_notebook_entries(payload.session_id, [entry_payload])
except ValueError as e:
raise HTTPException(status_code=404, detail=str(e))
entry = await store.find_notebook_entry(payload.session_id, payload.question_id)
Expand Down
9 changes: 9 additions & 0 deletions deeptutor/capabilities/_answer_now.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
stream as llm_stream,
)
from deeptutor.services.prompt.manager import get_prompt_manager
from deeptutor.services.prompt.oxca_brand import apply_student_guardrail

# Per-event content cap. The trace can grow unbounded (especially for
# deep_research / deep_solve with many tool calls) so we truncate each
Expand Down Expand Up @@ -254,7 +255,15 @@ def load_answer_now_prompts(module: str, language: str) -> dict[str, Any]:
return get_prompt_manager().load_prompts(module, "answer_now", language)


def answer_now_system_prompt(module: str, language: str) -> str:
"""Load and white-label the answer-now system prompt for student output."""
prompts = load_answer_now_prompts(module, language)
raw = str(prompts.get("system", "")).strip()
return apply_student_guardrail(raw, language)


__all__ = [
"answer_now_system_prompt",
"build_answer_now_trace_metadata",
"extract_answer_now_context",
"format_trace_summary",
Expand Down
11 changes: 9 additions & 2 deletions deeptutor/capabilities/deep_question.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,13 @@ async def run(self, context: UnifiedContext, stream: StreamBus) -> None:
difficulty = str(overrides.get("difficulty", "") or "")
question_type = str(overrides.get("question_type", "") or "")
preference = str(overrides.get("preference", "") or "")
history_context = str(context.metadata.get("conversation_context_text", "") or "").strip()
# Tutor-inline quiz runs in a fresh session; the tutor transcript arrives
# via history_references → context.history_context, not conversation_context_text.
history_context = str(context.history_context or "").strip()
if not history_context:
history_context = str(
context.metadata.get("conversation_context_text", "") or ""
).strip()
enabled_tools = set(
self.manifest.tools_used if context.enabled_tools is None else context.enabled_tools
)
Expand Down Expand Up @@ -213,6 +219,7 @@ async def _run_answer_now(
join_chunks,
labeled_block,
load_answer_now_prompts,
answer_now_system_prompt,
make_skip_notice,
stream_synthesis,
)
Expand All @@ -228,7 +235,7 @@ async def _run_answer_now(
question_type = str(overrides.get("question_type", "") or "auto")

prompts = load_answer_now_prompts("question", context.language)
system_prompt = str(prompts.get("system", "")).strip()
system_prompt = answer_now_system_prompt("question", context.language)
user_prompt = str(prompts.get("user_template", "")).format(
topic=topic,
num_questions=num_questions,
Expand Down
Loading