Skip to content
GitLab
Projects
Groups
Snippets
/
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Sign in
Toggle navigation
Menu
Open sidebar
math_tutor_dev
public_math_tutor
Commits
d9c0e760
Commit
d9c0e760
authored
Jul 01, 2026
by
Kantz
Browse files
tinking reimplemented
parent
c9c7f945
Changes
6
Hide whitespace changes
Inline
Side-by-side
math-tutor/README.md
View file @
d9c0e760
...
...
@@ -61,6 +61,7 @@ Backend variables in `math-tutor/backend/.env`:
-
`POSTGRES_URL`
-
`FRONTEND_URL`
-
`LLM_PROVIDER`
-
`LLM_THINKING_ENABLED`
-
`EMBEDDING_PROVIDER`
Depending on the provider, you also need more variables from
`backend/.env-example`
, for example:
...
...
math-tutor/backend/.env-example
View file @
d9c0e760
...
...
@@ -7,6 +7,8 @@ DAILY_LLM_TOKEN_LIMIT="500000"
FRONTEND_URL="http://frontend:3000"
LLM_THINKING_ENABLED="False" # "True" or "False"
LLM_TOOL_USE_ENABLED="False" # "True" or "False"
MCP_SHARED_SECRET=""
MCP_BASE_URL=""
...
...
math-tutor/backend/app/config.py
View file @
d9c0e760
...
...
@@ -56,6 +56,13 @@ def get_llm_tool_use_enabled() -> bool:
normalized
=
value
.
strip
().
lower
()
return
normalized
==
"true"
def
get_llm_thinking_enabled
()
->
bool
:
value
=
os
.
getenv
(
"LLM_THINKING_ENABLED"
,
"false"
)
normalized
=
value
.
strip
().
lower
()
return
normalized
==
"true"
def
get_embedding_provider
()
->
str
:
value
=
os
.
getenv
(
"EMBEDDING_PROVIDER"
)
if
value
is
not
None
and
value
.
strip
():
...
...
math-tutor/backend/app/deterministic_services/llm_client.py
View file @
d9c0e760
...
...
@@ -64,6 +64,14 @@ def _extract_total_tokens(response: object) -> int:
return
int
(
prompt
)
+
int
(
completion
)
def
_get_message_field
(
message
:
object
,
field
:
str
)
->
str
:
if
isinstance
(
message
,
dict
):
return
message
.
get
(
field
,
""
)
or
""
if
hasattr
(
message
,
field
):
return
getattr
(
message
,
field
)
or
""
return
""
# ---
# Quota
# ---
...
...
@@ -147,6 +155,7 @@ def _chat_openai_compatible(
kwargs
=
{
"messages"
:
messages
,
"model"
:
settings
.
model
}
if
settings
.
temperature
is
not
None
:
kwargs
[
"temperature"
]
=
settings
.
temperature
kwargs
[
"reasoning_effort"
]
=
"medium"
if
config
.
get_llm_thinking_enabled
()
else
"none"
response
=
client
.
chat
.
completions
.
create
(
**
kwargs
)
message
=
response
.
choices
[
0
].
message
if
response
.
choices
else
{}
return
{
"raw"
:
response
,
"message"
:
message
}
...
...
@@ -167,6 +176,7 @@ def _chat_mistral(
kwargs
[
"temperature"
]
=
settings
.
temperature
if
settings
.
timeout
is
not
None
:
kwargs
[
"timeout_ms"
]
=
int
(
settings
.
timeout
*
1000
)
kwargs
[
"reasoning_effort"
]
=
"medium"
if
config
.
get_llm_thinking_enabled
()
else
"none"
with
Mistral
(
api_key
=
settings
.
api_key
)
as
client
:
response
=
client
.
chat
.
complete
(
**
kwargs
)
...
...
@@ -181,7 +191,11 @@ def _chat_ollama(
settings
=
config
.
get_ollama_settings
()
client
=
ollama
.
Client
(
host
=
settings
.
base_url
,
timeout
=
settings
.
timeout
)
kwargs
:
dict
=
{
"model"
:
settings
.
model
,
"messages"
:
messages
}
kwargs
:
dict
=
{
"model"
:
settings
.
model
,
"messages"
:
messages
,
"think"
:
config
.
get_llm_thinking_enabled
(),
}
if
settings
.
keepalive
:
kwargs
[
"keep_alive"
]
=
settings
.
keepalive
if
settings
.
temperature
is
not
None
:
...
...
@@ -207,11 +221,11 @@ def _run_provider_chat(
def
get_message_content
(
result
:
dict
|
object
)
->
str
:
message
=
result
.
get
(
"message"
)
if
isinstance
(
result
,
dict
)
else
result
if
isinstance
(
message
,
dict
):
return
message
.
get
(
"content"
,
""
)
or
""
if
hasattr
(
message
,
"content"
)
:
return
getattr
(
message
,
"content"
)
or
"
"
return
""
content
=
_get_message_field
(
message
,
"content"
)
thinking
=
_get_message_field
(
message
,
"thinking"
).
strip
()
if
thinking
:
return
f
"
{
thinking
}
</think>
\n\n
{
content
}
"
return
content
def
chat
(
...
...
math-tutor/backend/app/deterministic_services/llm_tool_client.py
View file @
d9c0e760
...
...
@@ -227,6 +227,7 @@ async def _run_openai_compatible_tool_loop(
"model"
:
settings
.
model
,
"messages"
:
request_messages
,
"tools"
:
chat_tools
,
"reasoning_effort"
:
"medium"
if
config
.
get_llm_thinking_enabled
()
else
"none"
,
}
if
settings
.
temperature
is
not
None
:
request_kwargs
[
"temperature"
]
=
settings
.
temperature
...
...
@@ -293,6 +294,7 @@ async def _run_ollama_tool_loop(
"model"
:
settings
.
model
,
"messages"
:
request_messages
,
"tools"
:
chat_tools
,
"think"
:
config
.
get_llm_thinking_enabled
(),
}
if
settings
.
keepalive
is
not
None
:
request_kwargs
[
"keep_alive"
]
=
settings
.
keepalive
...
...
math-tutor/backend/test/test_llm_provider.py
View file @
d9c0e760
...
...
@@ -121,6 +121,36 @@ class _FakeOpenAIClient:
self
.
chat
=
types
.
SimpleNamespace
(
completions
=
completions
)
class
_FakeSyncOpenAIClient
:
def
__init__
(
self
,
response
:
object
)
->
None
:
self
.
calls
:
list
[
dict
]
=
[]
self
.
chat
=
types
.
SimpleNamespace
(
completions
=
types
.
SimpleNamespace
(
create
=
self
.
_create
)
)
self
.
_response
=
response
def
_create
(
self
,
**
kwargs
):
self
.
calls
.
append
(
kwargs
)
return
self
.
_response
class
_FakeMistralClient
:
calls
:
list
[
dict
]
=
[]
def
__init__
(
self
,
**
kwargs
)
->
None
:
self
.
chat
=
types
.
SimpleNamespace
(
complete
=
self
.
_complete
)
def
__enter__
(
self
):
return
self
def
__exit__
(
self
,
*
args
)
->
None
:
return
None
def
_complete
(
self
,
**
kwargs
):
self
.
calls
.
append
(
kwargs
)
return
types
.
SimpleNamespace
(
choices
=
[
types
.
SimpleNamespace
(
message
=
{
"content"
:
"done"
})])
class
_FakeToolResult
:
def
__init__
(
self
,
...
...
@@ -175,6 +205,13 @@ class LLMProviderConfigTest(unittest.TestCase):
with
patch
.
dict
(
os
.
environ
,
{},
clear
=
True
):
self
.
assertFalse
(
config
.
get_llm_tool_use_enabled
())
def
test_get_llm_thinking_enabled_reads_boolean
(
self
)
->
None
:
"""Verifies that thinking mode is controlled by the env flag."""
with
patch
.
dict
(
os
.
environ
,
{},
clear
=
True
):
self
.
assertFalse
(
config
.
get_llm_thinking_enabled
())
with
patch
.
dict
(
os
.
environ
,
{
"LLM_THINKING_ENABLED"
:
"true"
},
clear
=
True
):
self
.
assertTrue
(
config
.
get_llm_thinking_enabled
())
def
test_get_gwdg_chat_settings_reads_gwdg_keys
(
self
)
->
None
:
"""Verifies that GWDG chat settings are loaded from the expected env vars."""
env
=
{
...
...
@@ -415,6 +452,69 @@ class LLMClientProviderTest(unittest.TestCase):
tool_chat
.
assert_called_once_with
(
"ollama"
,
MESSAGES
)
ollama_chat
.
assert_not_called
()
def
test_ollama_chat_disables_thinking_but_formats_it_when_returned
(
self
)
->
None
:
"""Checks that returned Ollama thinking still reaches the existing UI format."""
calls
=
[]
class
FakeOllamaClient
:
def
__init__
(
self
,
**
kwargs
)
->
None
:
calls
.
append
({
"client"
:
kwargs
})
def
chat
(
self
,
model
=
""
,
messages
=
None
,
*
,
think
=
None
):
kwargs
=
{
"model"
:
model
,
"messages"
:
messages
,
"think"
:
think
}
calls
.
append
({
"chat"
:
kwargs
})
return
{
"message"
:
{
"content"
:
"answer"
,
"thinking"
:
"work"
}}
settings
=
config
.
OllamaSettings
(
base_url
=
"http://ollama"
,
model
=
"qwen3"
,
timeout
=
None
,
keepalive
=
None
,
temperature
=
None
,
)
with
patch
.
dict
(
os
.
environ
,
{
"LLM_THINKING_ENABLED"
:
"false"
}),
patch
.
object
(
config
,
"get_ollama_settings"
,
return_value
=
settings
),
patch
.
object
(
llm_client
.
ollama
,
"Client"
,
FakeOllamaClient
):
result
=
llm_client
.
_chat_ollama
(
MESSAGES
)
self
.
assertFalse
(
calls
[
1
][
"chat"
][
"think"
])
self
.
assertEqual
(
llm_client
.
get_message_content
(
result
),
"work</think>
\n\n
answer"
)
def
test_openai_compatible_chat_sets_reasoning_effort_from_env
(
self
)
->
None
:
"""Checks that OpenAI-compatible chat maps thinking mode to reasoning effort."""
settings
=
config
.
OpenAIChatSettings
(
base_url
=
"https://example.com/v1"
,
api_key
=
"key"
,
model
=
"model"
,
timeout
=
30.0
,
temperature
=
None
,
)
fake_client
=
_FakeSyncOpenAIClient
(
_make_openai_response
({
"content"
:
"done"
}))
with
patch
.
dict
(
os
.
environ
,
{
"LLM_THINKING_ENABLED"
:
"true"
}),
patch
.
object
(
llm_client
,
"OpenAI"
,
return_value
=
fake_client
):
llm_client
.
_chat_openai_compatible
(
MESSAGES
,
settings
)
self
.
assertEqual
(
fake_client
.
calls
[
0
][
"reasoning_effort"
],
"medium"
)
def
test_mistral_chat_sets_reasoning_effort_from_env
(
self
)
->
None
:
"""Checks that Mistral chat maps thinking mode to reasoning effort."""
settings
=
config
.
MistralChatSettings
(
api_key
=
"key"
,
model
=
"mistral"
,
timeout
=
None
,
temperature
=
None
,
)
_FakeMistralClient
.
calls
=
[]
with
patch
.
dict
(
os
.
environ
,
{
"LLM_THINKING_ENABLED"
:
"false"
}),
patch
.
object
(
llm_client
,
"Mistral"
,
_FakeMistralClient
):
llm_client
.
_chat_mistral
(
MESSAGES
,
settings
)
self
.
assertEqual
(
_FakeMistralClient
.
calls
[
0
][
"reasoning_effort"
],
"none"
)
def
test_selected_gwdg_config_error_happens_before_quota
(
self
)
->
None
:
"""Checks that missing GWDG config fails before any quota accounting runs."""
with
patch
.
dict
(
...
...
Write
Preview
Supports
Markdown
0%
Try again
or
attach a new file
.
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment