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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,8 @@ htmlcov/
nosetests.xml
coverage.xml
develop/
web/playwright-report/
web/test-results/

# ============================================
# Jupyter Notebook
Expand Down
4 changes: 1 addition & 3 deletions deeptutor/api/routers/attachments.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,7 @@ def _content_disposition(filename: str, *, disposition: str = "inline") -> str:
# Quotes / backslashes break the simple-quoted-string form; collapse them.
ascii_fallback = ascii_fallback.replace('"', "_").replace("\\", "_")
encoded = quote(filename, safe="")
return (
f'{disposition}; filename="{ascii_fallback}"; filename*=UTF-8\'\'{encoded}'
)
return f"{disposition}; filename=\"{ascii_fallback}\"; filename*=UTF-8''{encoded}"


@router.get("/{session_id}/{attachment_id}/{filename:path}")
Expand Down
4 changes: 1 addition & 3 deletions deeptutor/api/routers/knowledge.py
Original file line number Diff line number Diff line change
Expand Up @@ -168,9 +168,7 @@ def _save_uploaded_files(
except OSError:
pass

error_message = (
f"Validation failed for file '{original_filename}': {format_exception_message(e)}"
)
error_message = f"Validation failed for file '{original_filename}': {format_exception_message(e)}"
logger.error(error_message, exc_info=True)
raise HTTPException(status_code=400, detail=error_message) from e
except Exception:
Expand Down
4 changes: 1 addition & 3 deletions deeptutor/api/routers/notebook.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,7 @@ class AddRecordRequest(BaseModel):
"""Add record request"""

notebook_ids: list[str]
record_type: Literal[
"solve", "question", "research", "chat", "co_writer", "tutorbot"
]
record_type: Literal["solve", "question", "research", "chat", "co_writer", "tutorbot"]
title: str
summary: str = ""
user_query: str
Expand Down
10 changes: 6 additions & 4 deletions deeptutor/api/routers/question.py
Original file line number Diff line number Diff line change
Expand Up @@ -486,12 +486,14 @@ async def log_pusher():

# Log additional context if available
try:
if "result" in locals():
local_vars = locals()
if "result" in local_vars:
result_value = local_vars["result"]
logger.error(
f"Result type: {type(result)}, result keys: {result.keys() if isinstance(result, dict) else 'N/A'}"
f"Result type: {type(result_value)}, result keys: {result_value.keys() if isinstance(result_value, dict) else 'N/A'}"
)
if isinstance(result, dict) and "validation" in result:
validation = result["validation"]
if isinstance(result_value, dict) and "validation" in result_value:
validation = result_value["validation"]
logger.error(f"Validation type: {type(validation)}")
if isinstance(validation, dict):
logger.error(f"Validation keys: {validation.keys()}")
Expand Down
1 change: 1 addition & 0 deletions deeptutor/api/routers/solve.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"""

import asyncio
from pathlib import Path
import re
from typing import Any

Expand Down
4 changes: 3 additions & 1 deletion deeptutor/knowledge/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -253,7 +253,9 @@ def update_kb_status(
# permanently carrying a "completed" progress banner.
kb_config.pop("progress", None)
if progress is not None:
kb_config["last_completed_at"] = progress.get("timestamp") or datetime.now().isoformat()
kb_config["last_completed_at"] = (
progress.get("timestamp") or datetime.now().isoformat()
)
elif progress is not None:
kb_config["progress"] = progress

Expand Down
4 changes: 3 additions & 1 deletion deeptutor/knowledge/progress_tracker.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,9 @@ def _save_progress(self, progress: dict):
f.flush()
temp_progress_file.replace(self.progress_file)
except Exception as e:
_get_logger().warning("Failed to persist progress snapshot for '%s': %s", self.kb_name, e)
_get_logger().warning(
"Failed to persist progress snapshot for '%s': %s", self.kb_name, e
)

def update(
self,
Expand Down
4 changes: 3 additions & 1 deletion deeptutor/services/config/context_window_detection.py
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,9 @@ async def _detect_from_models_endpoint(
async with session.get(url, headers=headers) as response:
if response.status != 200:
if on_log is not None:
on_log(f"`GET {url}` returned HTTP {response.status}; skipping metadata detection.")
on_log(
f"`GET {url}` returned HTTP {response.status}; skipping metadata detection."
)
return None
payload = await response.json()
except Exception as exc:
Expand Down
5 changes: 1 addition & 4 deletions deeptutor/services/config/test_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -215,10 +215,7 @@ async def _test_llm(self, run: TestRun, catalog: dict[str, Any]) -> None:
)
run.emit(
"context_window",
(
f"Context window set to {detection.context_window} tokens "
f"({detection.source})."
),
(f"Context window set to {detection.context_window} tokens ({detection.source})."),
context_window=detection.context_window,
source=detection.source,
detail=detection.detail,
Expand Down
22 changes: 16 additions & 6 deletions deeptutor/services/llm/factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
from __future__ import annotations

import asyncio
import contextlib
from collections.abc import AsyncGenerator, Mapping
import contextlib
from types import SimpleNamespace
from typing import Any, TypedDict

Expand Down Expand Up @@ -184,12 +184,16 @@ def _resolve_call_config(


def _capability_binding(config: LLMConfig, provider_spec: Any) -> str:
backend = getattr(provider_spec, "backend", "openai_compat") if provider_spec else "openai_compat"
backend = (
getattr(provider_spec, "backend", "openai_compat") if provider_spec else "openai_compat"
)
if backend == "anthropic":
return "anthropic"
if backend == "azure_openai":
return "azure_openai"
return getattr(provider_spec, "name", None) or config.provider_name or config.binding or "openai"
return (
getattr(provider_spec, "name", None) or config.provider_name or config.binding or "openai"
)


def _build_messages(
Expand Down Expand Up @@ -312,7 +316,9 @@ async def complete(
image_data=image_data,
)
retry_delays = _build_retry_delays(max_retries, retry_delay, exponential_backoff)
extra_kwargs = _sanitize_call_kwargs(binding=capability_binding, model=config.model, kwargs=kwargs)
extra_kwargs = _sanitize_call_kwargs(
binding=capability_binding, model=config.model, kwargs=kwargs
)

try:
response = await provider.chat_with_retry(
Expand All @@ -325,7 +331,9 @@ async def complete(
raise map_error(exc, provider=config.provider_name) from exc

if response.finish_reason == "error":
raise map_error(RuntimeError(response.content or "LLM request failed"), provider=config.provider_name)
raise map_error(
RuntimeError(response.content or "LLM request failed"), provider=config.provider_name
)
return response.content or ""


Expand Down Expand Up @@ -366,7 +374,9 @@ async def stream(
image_data=image_data,
)
retry_delays = _build_retry_delays(max_retries, retry_delay, exponential_backoff)
extra_kwargs = _sanitize_call_kwargs(binding=capability_binding, model=config.model, kwargs=kwargs)
extra_kwargs = _sanitize_call_kwargs(
binding=capability_binding, model=config.model, kwargs=kwargs
)

queue: asyncio.Queue[str | BaseException | None] = asyncio.Queue()
saw_output = False
Expand Down
1 change: 1 addition & 0 deletions deeptutor/services/llm/provider_core/anthropic_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ def __init__(
self._supports_prompt_caching = supports_prompt_caching

from anthropic import AsyncAnthropic

from deeptutor.services.llm.utils import sanitize_url

client_kw: dict[str, Any] = {"max_retries": 0}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,9 @@ async def _timed_stream():
stream_iter = stream.__aiter__()
while True:
try:
yield await asyncio.wait_for(stream_iter.__anext__(), timeout=idle_timeout_s)
yield await asyncio.wait_for(
stream_iter.__anext__(), timeout=idle_timeout_s
)
except StopAsyncIteration:
break

Expand Down
2 changes: 1 addition & 1 deletion deeptutor/services/llm/provider_core/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@

from abc import ABC, abstractmethod
import asyncio
from collections.abc import Awaitable, Callable, Sequence
from dataclasses import dataclass, field
import json
from typing import Any
from collections.abc import Awaitable, Callable, Sequence

from loguru import logger

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@

from __future__ import annotations

import time
from collections.abc import Awaitable, Callable
import time
from typing import Any

import httpx
Expand Down Expand Up @@ -66,7 +66,9 @@ async def _exchange_token(self) -> str:
)

timeout = httpx.Timeout(20.0, connect=20.0)
async with httpx.AsyncClient(timeout=timeout, follow_redirects=True, trust_env=True) as client:
async with httpx.AsyncClient(
timeout=timeout, follow_redirects=True, trust_env=True
) as client:
response = await client.get(
DEFAULT_COPILOT_TOKEN_URL,
headers={
Expand Down
16 changes: 12 additions & 4 deletions deeptutor/services/llm/provider_core/openai_codex_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,20 @@
from __future__ import annotations

import asyncio
from collections.abc import Awaitable, Callable
import hashlib
import json
from collections.abc import Awaitable, Callable
from typing import Any

import httpx
from loguru import logger

from deeptutor.services.llm.provider_core.base import LLMProvider, LLMResponse, ToolCallRequest
from deeptutor.services.llm.provider_core.openai_responses import consume_sse, convert_messages, convert_tools
from deeptutor.services.llm.provider_core.openai_responses import (
consume_sse,
convert_messages,
convert_tools,
)

DEFAULT_CODEX_URL = "https://chatgpt.com/backend-api/codex/responses"
DEFAULT_ORIGINATOR = "DeepTutor"
Expand Down Expand Up @@ -138,7 +142,9 @@ def _strip_model_prefix(model: str) -> str:

def _build_headers(account_id: Any, token: Any) -> dict[str, str]:
if not token:
raise RuntimeError("OpenAI Codex is not logged in. Run `deeptutor provider login openai-codex`.")
raise RuntimeError(
"OpenAI Codex is not logged in. Run `deeptutor provider login openai-codex`."
)
headers = {
"Authorization": f"Bearer {token}",
"OpenAI-Beta": "responses=experimental",
Expand All @@ -163,7 +169,9 @@ async def _request_codex(
async with client.stream("POST", url, headers=headers, json=body) as response:
if response.status_code != 200:
text = await response.aread()
raise RuntimeError(_friendly_error(response.status_code, text.decode("utf-8", "ignore")))
raise RuntimeError(
_friendly_error(response.status_code, text.decode("utf-8", "ignore"))
)
return await consume_sse(response, on_content_delta)


Expand Down
28 changes: 15 additions & 13 deletions deeptutor/services/llm/provider_core/openai_compat_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@
import asyncio
from collections.abc import Awaitable, Callable
import hashlib
import time
import secrets
import string
import time
from typing import TYPE_CHECKING, Any
import uuid

Expand Down Expand Up @@ -50,9 +50,7 @@
_RESPONSES_FAILURE_THRESHOLD = 2
_RESPONSES_PROBE_INTERVAL_S = 300.0
_THINKING_STYLE_MAP = {
"thinking_type": lambda enabled: {
"thinking": {"type": "enabled" if enabled else "disabled"}
},
"thinking_type": lambda enabled: {"thinking": {"type": "enabled" if enabled else "disabled"}},
"enable_thinking": lambda enabled: {"enable_thinking": enabled},
"reasoning_split": lambda enabled: {"reasoning_split": enabled},
}
Expand Down Expand Up @@ -713,9 +711,9 @@ async def chat(
try:
return self._parse(await self._client.chat.completions.create(**request_kwargs))
except Exception as exc:
if request_kwargs.get("response_format") is not None and self._is_response_format_error(
exc
):
if request_kwargs.get(
"response_format"
) is not None and self._is_response_format_error(exc):
binding = self._provider_name or (self._spec.name if self._spec else "openai")
disable_response_format_at_runtime(binding, request_kwargs.get("model"))
retry_kwargs = dict(request_kwargs)
Expand Down Expand Up @@ -786,9 +784,13 @@ async def _timed_stream():
except StopAsyncIteration:
break

content, tool_calls, finish_reason, usage, reasoning_content = (
await consume_sdk_stream(_timed_stream(), on_content_delta)
)
(
content,
tool_calls,
finish_reason,
usage,
reasoning_content,
) = await consume_sdk_stream(_timed_stream(), on_content_delta)
self._record_responses_success(model, reasoning_effort)
return LLMResponse(
content=content or None,
Expand All @@ -809,9 +811,9 @@ async def _timed_stream():
try:
stream = await self._client.chat.completions.create(**request_kwargs)
except Exception as exc:
if request_kwargs.get("response_format") is not None and self._is_response_format_error(
exc
):
if request_kwargs.get(
"response_format"
) is not None and self._is_response_format_error(exc):
binding = self._provider_name or (self._spec.name if self._spec else "openai")
disable_response_format_at_runtime(binding, request_kwargs.get("model"))
retry_kwargs = dict(request_kwargs)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,9 +74,7 @@ def convert_user_message(content: Any) -> dict[str, Any]:
elif item.get("type") == "image_url":
url = (item.get("image_url") or {}).get("url")
if url:
converted.append(
{"type": "input_image", "image_url": url, "detail": "auto"}
)
converted.append({"type": "input_image", "image_url": url, "detail": "auto"})
if converted:
return {"role": "user", "content": converted}
return {"role": "user", "content": [{"type": "input_text", "text": ""}]}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@

from __future__ import annotations

import json
from collections.abc import Awaitable, Callable
import json
from typing import Any, AsyncGenerator

import httpx
Expand Down
4 changes: 3 additions & 1 deletion deeptutor/services/rag/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,9 @@ async def _generate_queries(self, context: str, n: int) -> list[str]:
)
raw = await complete(prompt, system_prompt="You are a search query generator.")
lines = [
l.strip().lstrip("0123456789.-) ") for l in raw.strip().split("\n") if l.strip()
line.strip().lstrip("0123456789.-) ")
for line in raw.strip().split("\n")
if line.strip()
]
return lines[:n] if lines else [context[:200]]
except Exception:
Expand Down
4 changes: 1 addition & 3 deletions deeptutor/services/session/turn_runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -600,9 +600,7 @@ async def _run_turn(self, execution: _TurnExecution) -> None:

from deeptutor.utils.document_extractor import extract_documents_from_records

document_texts, attachment_records = extract_documents_from_records(
attachment_records
)
document_texts, attachment_records = extract_documents_from_records(attachment_records)
attachments = [
Attachment(
type=r.get("type", "file"),
Expand Down
Loading
Loading