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
d8327473
Commit
d8327473
authored
Feb 12, 2026
by
Kantz
Browse files
auräumaktion
parent
d58cbed7
Changes
10
Hide whitespace changes
Inline
Side-by-side
math-tutor/README.md
View file @
d8327473
...
@@ -15,7 +15,11 @@ cd math-tutor/frontend
...
@@ -15,7 +15,11 @@ cd math-tutor/frontend
npm
install
npm
install
npm
run
dev
npm
run
dev
```
```
Um es im Netzwerk verfügbar zu machen mit: npm run dev -- --host 0.0.0.0
To make it accessible over the network.
Add the frontend- and backend-adress in the .env file in the frontend- and backend-folder.
python -m uvicorn app.main:app --reload --host 0.0.0.0 --port 8000
npm run dev -- --host 0.0.0.0
## Database (pgvector) setup
## Database (pgvector) setup
Ensure
`POSTGRES_URL`
and embedding env vars are in
`backend/.env`
:
Ensure
`POSTGRES_URL`
and embedding env vars are in
`backend/.env`
:
...
@@ -51,7 +55,7 @@ python -m scripts.retrieval_cli query --q "Was ist eine Teilmenge?" --k 8 --expa
...
@@ -51,7 +55,7 @@ python -m scripts.retrieval_cli query --q "Was ist eine Teilmenge?" --k 8 --expa
## Configuration
## Configuration
System prompt (LLM)
:
Differend Orchestrators
:
-
Edit
`math-tutor/backend/app/api/chat.py`
and update
`SYSTEM_PROMPT`
.
-
Edit
`math-tutor/backend/app/api/chat.py`
and update
`SYSTEM_PROMPT`
.
Retrieval settings:
Retrieval settings:
...
...
math-tutor/backend/app/api/chat.py
View file @
d8327473
from
__future__
import
annotations
from
__future__
import
annotations
from
logging
import
config
from
typing
import
List
,
Optional
from
typing
import
List
,
Optional
import
logging
import
logging
...
@@ -8,9 +9,12 @@ from fastapi import APIRouter, HTTPException, Path, Query
...
@@ -8,9 +9,12 @@ from fastapi import APIRouter, HTTPException, Path, Query
from
pydantic
import
BaseModel
,
Field
from
pydantic
import
BaseModel
,
Field
from
app.deterministic_services
import
session_store
from
app.deterministic_services
import
session_store
from
app.deterministic_services.orchestrators
import
orchestrator_tutor
as
orchestrator
import
app.config
as
config
from
app.deterministic_services.orchestrators
import
orchestrator_QA
if
config
.
get_orchestrator
()
==
"tutor"
:
from
app.deterministic_services.orchestrators
import
orchestrator_tutor
as
orchestrator
else
:
from
app.deterministic_services.orchestrators
import
orchestrator_QA
as
orchestrator
router
=
APIRouter
()
router
=
APIRouter
()
...
...
math-tutor/backend/app/api/context.py
View file @
d8327473
...
@@ -6,7 +6,6 @@ from fastapi import APIRouter, HTTPException
...
@@ -6,7 +6,6 @@ from fastapi import APIRouter, HTTPException
from
pydantic
import
BaseModel
,
Field
from
pydantic
import
BaseModel
,
Field
from
app.deterministic_services
import
context_store
from
app.deterministic_services
import
context_store
from
app.deterministic_services
import
Source
router
=
APIRouter
()
router
=
APIRouter
()
...
...
math-tutor/backend/app/api/health.py
View file @
d8327473
from
__future__
import
annotations
from
__future__
import
annotations
import
logging
import
logging
import
os
from
typing
import
Any
,
Dict
from
typing
import
Any
,
Dict
import
httpx
import
httpx
import
psycopg
import
psycopg
from
fastapi
import
APIRouter
from
fastapi
import
APIRouter
import
app.config
as
config
router
=
APIRouter
()
router
=
APIRouter
()
logger
=
logging
.
getLogger
(
__name__
)
logger
=
logging
.
getLogger
(
__name__
)
def
_check_ollama
()
->
dict
:
def
_check_ollama
()
->
dict
:
base_url
=
os
.
getenv
(
"OLLAMA_URL"
,
"http://localhost:11434"
)
.
rstrip
(
"/"
)
base_url
=
config
.
get_ollama_settings
().
base_url
.
rstrip
(
"/"
)
url
=
f
"
{
base_url
}
/api/tags"
url
=
f
"
{
base_url
}
/api/tags"
try
:
try
:
with
httpx
.
Client
(
timeout
=
5.0
)
as
client
:
with
httpx
.
Client
(
timeout
=
5.0
)
as
client
:
...
@@ -34,14 +35,13 @@ def _normalize_openai_models_url(base_url: str) -> str:
...
@@ -34,14 +35,13 @@ def _normalize_openai_models_url(base_url: str) -> str:
def
_check_openai
()
->
dict
:
def
_check_openai
()
->
dict
:
base_url
=
os
.
getenv
(
"OPENAI_BASE_URL"
)
settings
=
config
.
get_openai_base_settings
()
api_key
=
os
.
getenv
(
"OPENAI_API_KEY"
)
if
not
settings
:
if
not
base_url
or
not
api_key
:
return
{
"status"
:
"missing_config"
}
return
{
"status"
:
"missing_config"
}
url
=
_normalize_openai_models_url
(
base_url
)
url
=
_normalize_openai_models_url
(
settings
.
base_url
)
try
:
try
:
headers
=
{
"Authorization"
:
f
"Bearer
{
api_key
}
"
}
headers
=
{
"Authorization"
:
f
"Bearer
{
settings
.
api_key
}
"
}
with
httpx
.
Client
(
timeout
=
5.0
)
as
client
:
with
httpx
.
Client
(
timeout
=
5.0
)
as
client
:
response
=
client
.
get
(
url
,
headers
=
headers
)
response
=
client
.
get
(
url
,
headers
=
headers
)
if
response
.
status_code
in
(
401
,
403
):
if
response
.
status_code
in
(
401
,
403
):
...
@@ -53,8 +53,9 @@ def _check_openai() -> dict:
...
@@ -53,8 +53,9 @@ def _check_openai() -> dict:
def
_check_postgres
()
->
dict
:
def
_check_postgres
()
->
dict
:
pg_url
=
os
.
getenv
(
"POSTGRES_URL"
)
try
:
if
not
pg_url
:
pg_url
=
config
.
get_postgres_url
()
except
ValueError
:
return
{
"status"
:
"missing_config"
}
return
{
"status"
:
"missing_config"
}
try
:
try
:
with
psycopg
.
connect
(
pg_url
,
connect_timeout
=
5
)
as
conn
:
with
psycopg
.
connect
(
pg_url
,
connect_timeout
=
5
)
as
conn
:
...
...
math-tutor/backend/app/api/retrieval.py
deleted
100644 → 0
View file @
d58cbed7
from
__future__
import
annotations
from
__future__
import
annotations
import
os
from
pathlib
import
Path
from
typing
import
List
,
Optional
from
fastapi
import
APIRouter
,
HTTPException
from
pydantic
import
BaseModel
,
Field
from
app
import
config
from
app.deterministic_services.embeddings
import
OpenAILikeEmbeddings
from
app.deterministic_services
import
vector_store
router
=
APIRouter
()
def
_get_embedder
()
->
OpenAILikeEmbeddings
:
settings
=
config
.
get_embedding_settings
()
return
OpenAILikeEmbeddings
(
base_url
=
settings
.
base_url
,
api_key
=
settings
.
api_key
,
model
=
settings
.
model
,
target_dim
=
settings
.
target_dim
,
)
class
InitDbRequest
(
BaseModel
):
pg_url
:
Optional
[
str
]
=
None
class
IngestRequest
(
BaseModel
):
pg_url
:
Optional
[
str
]
=
None
base_dir
:
str
=
Field
(
default
=
"markdown"
)
class
QueryRequest
(
BaseModel
):
pg_url
:
Optional
[
str
]
=
None
query
:
str
=
Field
(...,
min_length
=
1
)
k
:
int
=
4
expand_links
:
bool
=
True
section_index
:
Optional
[
int
]
=
None
subsection_index
:
Optional
[
int
]
=
None
source_type_filter
:
Optional
[
List
[
str
]]
=
None
neighbor_expand
:
int
=
0
@
router
.
post
(
"/api/retrieval/init-db"
)
def
init_db
(
request
:
InitDbRequest
)
->
dict
:
pg_url
=
request
.
pg_url
or
os
.
getenv
(
"POSTGRES_URL"
)
if
not
pg_url
:
raise
HTTPException
(
status_code
=
500
,
detail
=
"Missing POSTGRES_URL"
)
vector_store
.
init_db
(
pg_url
)
return
{
"status"
:
"ok"
}
@
router
.
post
(
"/api/retrieval/ingest"
)
def
ingest
(
request
:
IngestRequest
)
->
dict
:
pg_url
=
request
.
pg_url
or
os
.
getenv
(
"POSTGRES_URL"
)
if
not
pg_url
:
raise
HTTPException
(
status_code
=
500
,
detail
=
"Missing POSTGRES_URL"
)
base_dir
=
Path
(
request
.
base_dir
)
if
not
base_dir
.
exists
():
raise
HTTPException
(
status_code
=
400
,
detail
=
"base_dir does not exist"
)
embedder
=
_get_embedder
()
docs
=
vector_store
.
load_docs
(
base_dir
)
embeddings
=
vector_store
.
embed_documents
(
embedder
,
[
doc
.
markdown
for
doc
in
docs
])
upserted
=
vector_store
.
upsert_docs
(
pg_url
,
docs
,
embeddings
)
return
{
"status"
:
"ok"
,
"upserted"
:
upserted
}
@
router
.
post
(
"/api/retrieval/query"
)
def
query
(
request
:
QueryRequest
)
->
dict
:
pg_url
=
request
.
pg_url
or
os
.
getenv
(
"POSTGRES_URL"
)
if
not
pg_url
:
raise
HTTPException
(
status_code
=
500
,
detail
=
"Missing POSTGRES_URL"
)
embedder
=
_get_embedder
()
result
=
vector_store
.
retrieve
(
pg_url
=
pg_url
,
embedder
=
embedder
,
query
=
request
.
query
,
k
=
request
.
k
,
section_index
=
request
.
section_index
,
subsection_index
=
request
.
subsection_index
,
source_type_filter
=
request
.
source_type_filter
,
expand_links
=
request
.
expand_links
,
neighbor_expand
=
request
.
neighbor_expand
,
)
return
result
@
router
.
get
(
"/api/retrieval/sections"
)
def
sections
(
pg_url
:
Optional
[
str
]
=
None
)
->
list
[
dict
]:
url
=
pg_url
or
os
.
getenv
(
"POSTGRES_URL"
)
if
not
url
:
raise
HTTPException
(
status_code
=
500
,
detail
=
"Missing POSTGRES_URL"
)
return
vector_store
.
list_sections
(
url
)
@
router
.
get
(
"/api/retrieval/subsections"
)
def
subsections
(
pg_url
:
Optional
[
str
]
=
None
,
section_index
:
Optional
[
int
]
=
None
)
->
list
[
dict
]:
url
=
pg_url
or
os
.
getenv
(
"POSTGRES_URL"
)
if
not
url
:
raise
HTTPException
(
status_code
=
500
,
detail
=
"Missing POSTGRES_URL"
)
return
vector_store
.
list_subsections
(
url
,
section_index
)
math-tutor/backend/app/config.py
View file @
d8327473
...
@@ -14,6 +14,10 @@ class EmbeddingSettings(BaseModel):
...
@@ -14,6 +14,10 @@ class EmbeddingSettings(BaseModel):
model
:
str
model
:
str
target_dim
:
int
=
1024
target_dim
:
int
=
1024
def
get_orchestrator
()
->
str
:
return
os
.
getenv
(
"ORCHESTRATOR"
,
"qa"
).
lower
()
def
get_embedding_settings
()
->
EmbeddingSettings
:
def
get_embedding_settings
()
->
EmbeddingSettings
:
embedding_type
=
os
.
getenv
(
"EMBEDDING_TYPE"
,
"openai-like"
)
embedding_type
=
os
.
getenv
(
"EMBEDDING_TYPE"
,
"openai-like"
)
if
embedding_type
==
"sentence-transformer"
:
if
embedding_type
==
"sentence-transformer"
:
...
@@ -56,6 +60,11 @@ class OpenAIChatSettings:
...
@@ -56,6 +60,11 @@ class OpenAIChatSettings:
timeout
:
float
|
None
timeout
:
float
|
None
temperature
:
float
|
None
temperature
:
float
|
None
@
dataclass
(
frozen
=
True
)
class
OpenAIBaseSettings
:
base_url
:
str
api_key
:
str
@
dataclass
(
frozen
=
True
)
@
dataclass
(
frozen
=
True
)
class
MathpixSettings
:
class
MathpixSettings
:
app_id
:
str
app_id
:
str
...
@@ -91,17 +100,26 @@ def get_ollama_settings() -> OllamaSettings:
...
@@ -91,17 +100,26 @@ def get_ollama_settings() -> OllamaSettings:
temperature
=
_read_float
(
os
.
getenv
(
"OLLAMA_TEMPERATURE"
)),
temperature
=
_read_float
(
os
.
getenv
(
"OLLAMA_TEMPERATURE"
)),
)
)
def
get_openai_base_settings
()
->
OpenAIBaseSettings
|
None
:
base_url
=
os
.
getenv
(
"OPENAI_BASE_URL"
)
api_key
=
os
.
getenv
(
"OPENAI_API_KEY"
)
if
not
base_url
or
not
api_key
:
return
None
return
OpenAIBaseSettings
(
base_url
=
base_url
,
api_key
=
api_key
,
)
def
get_openai_chat_settings
()
->
OpenAIChatSettings
|
None
:
def
get_openai_chat_settings
()
->
OpenAIChatSettings
|
None
:
model
=
os
.
getenv
(
"OPENAI_CHAT_MODEL"
)
model
=
os
.
getenv
(
"OPENAI_CHAT_MODEL"
)
if
not
model
:
if
not
model
:
return
None
return
None
base_url
=
os
.
getenv
(
"OPENAI_BASE_URL"
)
base_settings
=
get_openai_base_settings
()
api_key
=
os
.
getenv
(
"OPENAI_API_KEY"
)
if
not
base_settings
:
if
not
base_url
or
not
api_key
:
raise
ValueError
(
"Missing OPENAI_BASE_URL or OPENAI_API_KEY for chat"
)
raise
ValueError
(
"Missing OPENAI_BASE_URL or OPENAI_API_KEY for chat"
)
return
OpenAIChatSettings
(
return
OpenAIChatSettings
(
base_url
=
base_url
,
base_url
=
base_
settings
.
base_
url
,
api_key
=
api_key
,
api_key
=
base_settings
.
api_key
,
model
=
model
,
model
=
model
,
timeout
=
_read_float
(
os
.
getenv
(
"OPENAI_CHAT_TIMEOUT"
)),
timeout
=
_read_float
(
os
.
getenv
(
"OPENAI_CHAT_TIMEOUT"
)),
temperature
=
_read_float
(
os
.
getenv
(
"OPENAI_CHAT_TEMPERATURE"
)),
temperature
=
_read_float
(
os
.
getenv
(
"OPENAI_CHAT_TEMPERATURE"
)),
...
...
math-tutor/backend/app/deterministic_services/orchestrators/orchestrator_QA.py
View file @
d8327473
from
app.deterministic_services
import
context_store
,
retrieval_service
,
tool_logging
from
app.deterministic_services
import
referenz_decoder
from
app.LLM_services
import
qa_LLM
from
typing
import
List
from
typing
import
List
from
app.deterministic_services
import
context_store
,
tool_logging
,
vector_store
,
Source
,
referenz_decoder
from
app.LLM_services
import
qa_LLM
from
app.deterministic_services.embeddings
import
EmbeddingFactory
import
app.config
as
config
def
_is_new_chat
(
messages
:
list
[
dict
])
->
bool
:
def
_is_new_chat
(
messages
:
list
[
dict
])
->
bool
:
return
not
any
(
m
.
get
(
"role"
)
==
"assistant"
for
m
in
messages
)
return
not
any
(
m
.
get
(
"role"
)
==
"assistant"
for
m
in
messages
)
...
@@ -13,7 +15,7 @@ def _append_tool_log(tool_log: list[dict], name: str, args: dict, response: obje
...
@@ -13,7 +15,7 @@ def _append_tool_log(tool_log: list[dict], name: str, args: dict, response: obje
def
_bootstrap_context
(
sheet
:
dict
,
query_text
:
str
,
tool_log
:
list
[
dict
])
->
None
:
def
_bootstrap_context
(
sheet
:
dict
,
query_text
:
str
,
tool_log
:
list
[
dict
])
->
None
:
# Erstelle einen neuen Retrieval-Block oder aktualisiere den bestehenden
# Erstelle einen neuen Retrieval-Block oder aktualisiere den bestehenden
sources
=
retrieval_service
.
retrieve_context
(
query_text
=
query_text
)
sources
=
_
retrieve_context
(
query_text
=
query_text
)
retrievals
=
sheet
.
get
(
"retrieval_contexts"
,
[])
retrievals
=
sheet
.
get
(
"retrieval_contexts"
,
[])
if
retrievals
:
if
retrievals
:
...
@@ -36,6 +38,17 @@ def _extract_user_messages(messages: list[dict]) -> list[str]:
...
@@ -36,6 +38,17 @@ def _extract_user_messages(messages: list[dict]) -> list[str]:
user_contents
.
append
(
content
)
user_contents
.
append
(
content
)
return
user_contents
return
user_contents
def
_retrieve_context
(
query_text
:
str
,
pg_url
:
str
|
None
=
None
)
->
List
[
Source
]:
embedder
=
EmbeddingFactory
.
create
(
config
.
get_embedding_settings
())
url
=
pg_url
or
config
.
get_postgres_url
()
sources
=
vector_store
.
retrieve
(
pg_url
=
url
,
embedder
=
embedder
,
query
=
query_text
,
k
=
8
,
expand_links
=
True
,
)
return
sources
def
run_chat
(
messages
:
list
[
dict
],
draft
:
str
|
None
=
None
)
->
dict
:
def
run_chat
(
messages
:
list
[
dict
],
draft
:
str
|
None
=
None
)
->
dict
:
# Input-Fehlerbehandlung
# Input-Fehlerbehandlung
...
...
math-tutor/backend/app/deterministic_services/orchestrators/orchestrator_tutor.py
View file @
d8327473
from
app.deterministic_services
import
context_store
,
retrieval_service
,
tool_logging
from
typing
import
List
from
app.deterministic_services
import
referenz_decoder
from
app.deterministic_services
import
context_store
,
tool_logging
,
vector_store
,
Source
,
referenz_decoder
from
app.LLM_services
import
hint_LLM
,
decision_LLM
,
math_intent_LLM
,
solver_LLM
from
app.LLM_services
import
hint_LLM
,
decision_LLM
,
math_intent_LLM
,
solver_LLM
from
app.deterministic_services.embeddings
import
EmbeddingFactory
import
app.config
as
config
def
_is_new_chat
(
messages
:
list
[
dict
])
->
bool
:
def
_is_new_chat
(
messages
:
list
[
dict
])
->
bool
:
...
@@ -12,7 +16,7 @@ def _append_tool_log(tool_log: list[dict], name: str, args: dict, response: obje
...
@@ -12,7 +16,7 @@ def _append_tool_log(tool_log: list[dict], name: str, args: dict, response: obje
def
_bootstrap_context
(
sheet
:
dict
,
query_text
:
str
,
tool_log
:
list
[
dict
])
->
None
:
def
_bootstrap_context
(
sheet
:
dict
,
query_text
:
str
,
tool_log
:
list
[
dict
])
->
None
:
# Erstelle einen neuen Retrieval-Block oder aktualisiere den bestehenden
# Erstelle einen neuen Retrieval-Block oder aktualisiere den bestehenden
sources
=
retrieval_service
.
retrieve_context
(
query_text
=
query_text
)
sources
=
_
retrieve_context
(
query_text
=
query_text
)
retrievals
=
sheet
.
get
(
"retrieval_contexts"
,
[])
retrievals
=
sheet
.
get
(
"retrieval_contexts"
,
[])
if
retrievals
:
if
retrievals
:
...
@@ -40,6 +44,18 @@ def _extract_user_messages(messages: list[dict]) -> list[str]:
...
@@ -40,6 +44,18 @@ def _extract_user_messages(messages: list[dict]) -> list[str]:
user_contents
.
append
(
content
)
user_contents
.
append
(
content
)
return
user_contents
return
user_contents
def
_retrieve_context
(
query_text
:
str
,
pg_url
:
str
|
None
=
None
)
->
List
[
Source
]:
embedder
=
EmbeddingFactory
.
create
(
config
.
get_embedding_settings
())
url
=
pg_url
or
config
.
get_postgres_url
()
sources
=
vector_store
.
retrieve
(
pg_url
=
url
,
embedder
=
embedder
,
query
=
query_text
,
k
=
8
,
expand_links
=
True
,
)
return
sources
def
run_chat
(
messages
:
list
[
dict
],
draft
:
str
|
None
=
None
)
->
dict
:
def
run_chat
(
messages
:
list
[
dict
],
draft
:
str
|
None
=
None
)
->
dict
:
# Input-Fehlerbehandlung
# Input-Fehlerbehandlung
...
...
math-tutor/backend/app/deterministic_services/retrieval_service.py
deleted
100644 → 0
View file @
d58cbed7
# app/deterministic_services/retrieval_service.py
from
__future__
import
annotations
from
typing
import
List
from
app.deterministic_services
import
Source
from
app
import
config
from
app.deterministic_services.embeddings
import
EmbeddingFactory
from
app.deterministic_services
import
vector_store
def
retrieve_context
(
query_text
:
str
,
pg_url
:
str
|
None
=
None
)
->
List
[
Source
]:
embedder
=
EmbeddingFactory
.
create
(
config
.
get_embedding_settings
())
url
=
pg_url
or
config
.
get_postgres_url
()
sources
=
vector_store
.
retrieve
(
pg_url
=
url
,
embedder
=
embedder
,
query
=
query_text
,
k
=
8
,
expand_links
=
True
,
)
return
sources
\ No newline at end of file
math-tutor/backend/app/main.py
View file @
d8327473
...
@@ -2,7 +2,7 @@ from contextlib import asynccontextmanager
...
@@ -2,7 +2,7 @@ from contextlib import asynccontextmanager
from
fastapi
import
FastAPI
from
fastapi
import
FastAPI
from
fastapi.middleware.cors
import
CORSMiddleware
from
fastapi.middleware.cors
import
CORSMiddleware
from
app.api
import
canvas
,
chat
,
health
,
retrieval
,
context
from
app.api
import
canvas
,
chat
,
health
,
context
from
app.config
import
get_frontend_url
from
app.config
import
get_frontend_url
...
@@ -24,6 +24,5 @@ app.add_middleware(
...
@@ -24,6 +24,5 @@ app.add_middleware(
app
.
include_router
(
chat
.
router
)
app
.
include_router
(
chat
.
router
)
app
.
include_router
(
canvas
.
router
)
app
.
include_router
(
canvas
.
router
)
app
.
include_router
(
retrieval
.
router
)
app
.
include_router
(
context
.
router
)
app
.
include_router
(
context
.
router
)
app
.
include_router
(
health
.
router
)
app
.
include_router
(
health
.
router
)
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