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
6cf9eb8c
Commit
6cf9eb8c
authored
Jun 19, 2026
by
Kantz
Browse files
simplifications
parent
d9b6c146
Changes
17
Hide whitespace changes
Inline
Side-by-side
math-tutor/backend/app/api/chat.py
View file @
6cf9eb8c
...
@@ -9,10 +9,11 @@ import app.config as config
...
@@ -9,10 +9,11 @@ import app.config as config
from
app.deterministic_services
import
session_store
from
app.deterministic_services
import
session_store
from
app.deterministic_services
import
context_store
,
retrieval_store
,
task_catalog
from
app.deterministic_services
import
context_store
,
retrieval_store
,
task_catalog
from
app.deterministic_services
import
socratic_oranisator
from
app.deterministic_services
import
socratic_oranisator
from
app.deterministic_services.orchestrators.registry
import
(
from
app.deterministic_services.orchestrators
import
(
get_default_orchestrator
,
orchestrator_qa
,
is_valid_orchestrator
,
orchestrator_socratic
,
resolve_orchestrator
,
orchestrator_task
,
orchestrator_tutor
,
)
)
from
fastapi
import
APIRouter
,
HTTPException
,
Path
,
Query
from
fastapi
import
APIRouter
,
HTTPException
,
Path
,
Query
from
pydantic
import
BaseModel
,
Field
from
pydantic
import
BaseModel
,
Field
...
@@ -20,6 +21,13 @@ from pydantic import BaseModel, Field
...
@@ -20,6 +21,13 @@ from pydantic import BaseModel, Field
router
=
APIRouter
()
router
=
APIRouter
()
logger
=
logging
.
getLogger
(
__name__
)
logger
=
logging
.
getLogger
(
__name__
)
DEFAULT_ORCHESTRATOR
=
"qa"
ORCHESTRATORS
=
{
"qa"
:
orchestrator_qa
,
"tutor"
:
orchestrator_tutor
,
"task"
:
orchestrator_task
,
"socratic"
:
orchestrator_socratic
,
}
class
ChatMessage
(
BaseModel
):
class
ChatMessage
(
BaseModel
):
...
@@ -87,12 +95,11 @@ def chat(request: ChatRequest) -> ChatResponse:
...
@@ -87,12 +95,11 @@ def chat(request: ChatRequest) -> ChatResponse:
raise
HTTPException
(
status_code
=
400
,
detail
=
"messages required"
)
raise
HTTPException
(
status_code
=
400
,
detail
=
"messages required"
)
requested_orchestrator
=
str
(
request
.
orchestrator
or
""
).
strip
().
lower
()
requested_orchestrator
=
str
(
request
.
orchestrator
or
""
).
strip
().
lower
()
if
requested_orchestrator
and
not
is_valid_orchestrator
(
requested_orchestrator
)
:
if
requested_orchestrator
and
requested_orchestrator
not
in
ORCHESTRATORS
:
raise
HTTPException
(
status_code
=
422
,
detail
=
"unsupported orchestrator"
)
raise
HTTPException
(
status_code
=
422
,
detail
=
"unsupported orchestrator"
)
orchestrator_name
,
orchestrator_impl
=
resolve_orchestrator
(
orchestrator_name
=
requested_orchestrator
or
DEFAULT_ORCHESTRATOR
requested_orchestrator
or
None
orchestrator_impl
=
ORCHESTRATORS
[
orchestrator_name
]
)
try
:
try
:
payload_messages
=
[{
"role"
:
m
.
role
,
"content"
:
m
.
text
}
for
m
in
request
.
messages
]
payload_messages
=
[{
"role"
:
m
.
role
,
"content"
:
m
.
text
}
for
m
in
request
.
messages
]
...
@@ -203,7 +210,7 @@ def get_archive(chat_id: str = Path(..., min_length=1)) -> ChatArchiveDetail:
...
@@ -203,7 +210,7 @@ def get_archive(chat_id: str = Path(..., min_length=1)) -> ChatArchiveDetail:
history
=
[
ChatMessage
(
role
=
item
[
"role"
],
text
=
item
[
"text"
])
for
item
in
record
[
"history"
]],
history
=
[
ChatMessage
(
role
=
item
[
"role"
],
text
=
item
[
"text"
])
for
item
in
record
[
"history"
]],
selected_task
=
selected_task
,
selected_task
=
selected_task
,
selected_topic
=
selected_topic
,
selected_topic
=
selected_topic
,
orchestrator
=
record
.
get
(
"orchestrator"
)
or
get_default_orchestrator
()
,
orchestrator
=
record
.
get
(
"orchestrator"
)
or
DEFAULT_ORCHESTRATOR
,
)
)
...
@@ -213,9 +220,9 @@ def archive_chat(request: ChatRequest) -> ChatArchiveResponse:
...
@@ -213,9 +220,9 @@ def archive_chat(request: ChatRequest) -> ChatArchiveResponse:
return
ChatArchiveResponse
(
status
=
"skipped"
,
chat_id
=
"unknown"
)
return
ChatArchiveResponse
(
status
=
"skipped"
,
chat_id
=
"unknown"
)
requested_orchestrator
=
str
(
request
.
orchestrator
or
""
).
strip
().
lower
()
requested_orchestrator
=
str
(
request
.
orchestrator
or
""
).
strip
().
lower
()
if
requested_orchestrator
and
not
is_valid_orchestrator
(
requested_orchestrator
)
:
if
requested_orchestrator
and
requested_orchestrator
not
in
ORCHESTRATORS
:
raise
HTTPException
(
status_code
=
422
,
detail
=
"unsupported orchestrator"
)
raise
HTTPException
(
status_code
=
422
,
detail
=
"unsupported orchestrator"
)
archive_orchestrator
=
requested_orchestrator
or
get_default_orchestrator
()
archive_orchestrator
=
requested_orchestrator
or
DEFAULT_ORCHESTRATOR
try
:
try
:
chat_id
=
session_store
.
archive_chat
(
chat_id
=
session_store
.
archive_chat
(
...
...
math-tutor/backend/app/deterministic_services/__init__.py
View file @
6cf9eb8c
# Package marker for services
# Package marker for deterministic services.
from
app.deterministic_services.retrieval_store
import
Source
,
SourceID
__all__
=
[
"Source"
,
"SourceID"
]
\ No newline at end of file
math-tutor/backend/app/deterministic_services/context_stores/__init__.py
View file @
6cf9eb8c
from
app.deterministic_services.context_stores
import
context_store_base
# Package marker for context store implementations.
from
app.deterministic_services.context_stores
import
context_store_task
from
app.deterministic_services.context_stores
import
context_store_open
__all__
=
[
"context_store_base"
,
"context_store_open"
,
"context_store_task"
]
math-tutor/backend/app/deterministic_services/context_stores/context_store_base.py
View file @
6cf9eb8c
...
@@ -11,7 +11,7 @@ from zoneinfo import ZoneInfo
...
@@ -11,7 +11,7 @@ from zoneinfo import ZoneInfo
from
threading
import
Lock
from
threading
import
Lock
from
typing
import
Any
from
typing
import
Any
from
app.deterministic_services
import
Source
from
app.deterministic_services
.retrieval_store
import
Source
from
app.deterministic_services.tool_logging
import
(
from
app.deterministic_services.tool_logging
import
(
format_filename_timestamp
,
format_filename_timestamp
,
format_log_timestamp
,
format_log_timestamp
,
...
@@ -36,12 +36,12 @@ def _resolve_latest_path(chat_id: str) -> str | None:
...
@@ -36,12 +36,12 @@ def _resolve_latest_path(chat_id: str) -> str | None:
# timekeeping
# timekeeping
# ---
# ---
def
_mez_now
()
->
str
:
def
berlin_now_iso
()
->
str
:
return
datetime
.
now
(
ZoneInfo
(
"Europe/Berlin"
)).
strftime
(
"%Y-%m-%dT%H:%M:%SZ"
)
return
datetime
.
now
(
ZoneInfo
(
"Europe/Berlin"
)).
strftime
(
"%Y-%m-%dT%H:%M:%SZ"
)
def
_touch
(
sheet
:
dict
[
str
,
Any
])
->
None
:
def
_touch
(
sheet
:
dict
[
str
,
Any
])
->
None
:
sheet
[
"updated_at"
]
=
_mez_now
()
sheet
[
"updated_at"
]
=
berlin_now_iso
()
def
touch_sheet
(
sheet
:
dict
[
str
,
Any
])
->
None
:
def
touch_sheet
(
sheet
:
dict
[
str
,
Any
])
->
None
:
...
@@ -66,7 +66,7 @@ def get_chat_id(messages: list[dict], draft: str | None = None) -> str:
...
@@ -66,7 +66,7 @@ def get_chat_id(messages: list[dict], draft: str | None = None) -> str:
def
init_sheet_base
(
chat_id
:
str
,
messages
:
list
[
dict
])
->
dict
[
str
,
Any
]:
def
init_sheet_base
(
chat_id
:
str
,
messages
:
list
[
dict
])
->
dict
[
str
,
Any
]:
timestamp
=
_mez_now
()
timestamp
=
berlin_now_iso
()
return
{
return
{
"chat_id"
:
chat_id
,
"chat_id"
:
chat_id
,
"created_at"
:
timestamp
,
"created_at"
:
timestamp
,
...
@@ -203,7 +203,7 @@ def get_decisions(sheet: dict[str, Any]) -> list[dict[str, Any]]:
...
@@ -203,7 +203,7 @@ def get_decisions(sheet: dict[str, Any]) -> list[dict[str, Any]]:
def
add_decision
(
sheet
:
dict
[
str
,
Any
],
decision
:
dict
[
str
,
Any
])
->
None
:
def
add_decision
(
sheet
:
dict
[
str
,
Any
],
decision
:
dict
[
str
,
Any
])
->
None
:
entry
=
{
"timestamp"
:
_mez_now
(),
**
decision
}
entry
=
{
"timestamp"
:
berlin_now_iso
(),
**
decision
}
sheet
.
setdefault
(
"decisions"
,
[]).
append
(
entry
)
sheet
.
setdefault
(
"decisions"
,
[]).
append
(
entry
)
_touch
(
sheet
)
_touch
(
sheet
)
...
...
math-tutor/backend/app/deterministic_services/embedding_provider.py
View file @
6cf9eb8c
...
@@ -7,7 +7,7 @@ from threading import Lock
...
@@ -7,7 +7,7 @@ from threading import Lock
from
typing
import
Any
,
Dict
,
Tuple
from
typing
import
Any
,
Dict
,
Tuple
import
app.config
as
config
import
app.config
as
config
from
app.deterministic_services.embeddings
import
BaseEmbeddings
,
EmbeddingFactory
from
app.deterministic_services.embeddings
import
BaseEmbeddings
,
create_embedder
_EMBEDDER_LOCK
=
Lock
()
_EMBEDDER_LOCK
=
Lock
()
...
@@ -35,7 +35,7 @@ def get_embedder() -> tuple[BaseEmbeddings, bool]:
...
@@ -35,7 +35,7 @@ def get_embedder() -> tuple[BaseEmbeddings, bool]:
if
_CACHED_EMBEDDER
is
not
None
and
_CACHED_KEY
==
key
:
if
_CACHED_EMBEDDER
is
not
None
and
_CACHED_KEY
==
key
:
return
_CACHED_EMBEDDER
,
True
return
_CACHED_EMBEDDER
,
True
_CACHED_EMBEDDER
=
EmbeddingFactory
.
create
(
settings
)
_CACHED_EMBEDDER
=
create_embedder
(
settings
)
_CACHED_KEY
=
key
_CACHED_KEY
=
key
return
_CACHED_EMBEDDER
,
False
return
_CACHED_EMBEDDER
,
False
...
...
math-tutor/backend/app/deterministic_services/embeddings.py
View file @
6cf9eb8c
# Embeddings-Implementation with Factory-Pattern
from
__future__
import
annotations
from
__future__
import
annotations
import
math
import
math
from
typing
import
List
,
Optional
,
Union
from
enum
import
Enum
from
enum
import
Enum
from
typing
import
List
,
Optional
,
Union
from
openai
import
OpenAI
from
openai
import
OpenAI
from
pydantic
import
BaseModel
,
Field
from
pydantic
import
BaseModel
,
Field
from
sentence_transformers
import
SentenceTransformer
from
sentence_transformers
import
SentenceTransformer
# -----------------------------
# configuration model
# -----------------------------
class
EmbeddingType
(
str
,
Enum
):
class
EmbeddingType
(
str
,
Enum
):
OPENAI_LIKE
=
"openai-like"
OPENAI_LIKE
=
"openai-like"
SENTENCE_TRANSFORMER
=
"sentence-transformer"
SENTENCE_TRANSFORMER
=
"sentence-transformer"
class
OpenAILikeConfig
(
BaseModel
):
class
OpenAILikeConfig
(
BaseModel
):
"""Konfiguration für OpenAI-ähnliche APIs."""
base_url
:
str
=
Field
(...,
description
=
"Base URL of the embedding API."
)
base_url
:
str
=
Field
(...,
api_key
:
str
=
Field
(...,
description
=
"API key for the embedding API."
)
description
=
"Base URL der API (z. B. http://localhost:11434/v1)"
)
model
:
str
=
Field
(...,
description
=
"Embedding model name."
)
api_key
:
str
=
Field
(...,
target_dim
:
int
=
Field
(
1024
,
description
=
"Target embedding dimension."
)
description
=
"API-Key (z. B. 'ollama' für Ollama)"
)
model
:
str
=
Field
(...,
description
=
"Modellname (z. B. 'nomic-embed-text')"
)
target_dim
:
int
=
Field
(
1024
,
description
=
"Ziel-Dimension der Embeddings"
)
class
SentenceTransformerConfig
(
BaseModel
):
class
SentenceTransformerConfig
(
BaseModel
):
"""Konfiguration für lokale SentenceTransformer-Modelle."""
model
:
str
=
Field
(...,
description
=
"SentenceTransformer model name."
)
model
:
str
=
Field
(...,
target_dim
:
int
=
Field
(
384
,
description
=
"Target embedding dimension."
)
description
=
"Name des SentenceTransformer-Modells (z. B. 'all-MiniLM-L6-v2')"
)
target_dim
:
int
=
Field
(
384
,
description
=
"Ziel-Dimension der Embeddings"
)
class
EmbeddingConfig
(
BaseModel
):
class
EmbeddingConfig
(
BaseModel
):
"""Gemeinsame Konfiguration für die Factory."""
embedding_type
:
EmbeddingType
=
Field
(...,
description
=
"Embedding backend type."
)
embedding_type
:
EmbeddingType
=
Field
(...,
description
=
"Typ der Embeddings"
)
config
:
Union
[
OpenAILikeConfig
,
SentenceTransformerConfig
]
=
Field
(
config
:
Union
[
OpenAILikeConfig
,
SentenceTransformerConfig
]
=
Field
(
...,
description
=
"Spezifische Konfiguration"
)
...,
description
=
"Backend-specific embedding config."
)
# -----------------------------
# base class for embeddings
# -----------------------------
class
BaseEmbeddings
:
class
BaseEmbeddings
:
"""
Basisklasse für Embedding-Generierung mit gemeinsamen Methoden.
"""
def
__init__
(
self
,
target_dim
:
int
=
384
)
->
None
:
def
__init__
(
self
,
target_dim
:
int
=
384
)
->
None
:
self
.
target_dim
=
target_dim
self
.
target_dim
=
target_dim
def
_normalize
(
self
,
vec
:
List
[
float
])
->
List
[
float
]:
def
_normalize
(
self
,
vec
:
List
[
float
])
->
List
[
float
]:
"""Normalisiert einen Vektor auf L2-Norm."""
norm
=
math
.
sqrt
(
sum
(
x
*
x
for
x
in
vec
))
norm
=
math
.
sqrt
(
sum
(
x
*
x
for
x
in
vec
))
if
norm
==
0.0
:
if
norm
==
0.0
:
return
vec
return
vec
return
[
x
/
norm
for
x
in
vec
]
return
[
x
/
norm
for
x
in
vec
]
def
_truncate
(
self
,
vec
:
List
[
float
])
->
List
[
float
]:
def
_truncate
(
self
,
vec
:
List
[
float
])
->
List
[
float
]:
"""Trunziert oder füllt den Vektor auf die Ziel-Dimension."""
if
len
(
vec
)
<
self
.
target_dim
:
if
len
(
vec
)
<
self
.
target_dim
:
raise
ValueError
(
raise
ValueError
(
f
"Embedding dimension
{
len
(
vec
)
}
< target
{
self
.
target_dim
}
"
)
f
"Embedding dimension
{
len
(
vec
)
}
< target
{
self
.
target_dim
}
"
)
if
len
(
vec
)
>
self
.
target_dim
:
if
len
(
vec
)
>
self
.
target_dim
:
vec
=
vec
[:
self
.
target_dim
]
vec
=
vec
[:
self
.
target_dim
]
return
self
.
_normalize
(
vec
)
return
self
.
_normalize
(
vec
)
def
embed_documents
(
self
,
texts
:
List
[
str
])
->
List
[
List
[
float
]]:
def
embed_documents
(
self
,
texts
:
List
[
str
])
->
List
[
List
[
float
]]:
"""Generiert Embeddings für eine Liste von Texten."""
return
self
.
_embed
(
texts
)
return
self
.
_embed
(
texts
)
def
embed_query
(
self
,
text
:
str
)
->
List
[
float
]:
def
embed_query
(
self
,
text
:
str
)
->
List
[
float
]:
"""Generiert ein Embedding für einen einzelnen Text."""
return
self
.
_embed
(
text
)[
0
]
return
self
.
_embed
(
text
)[
0
]
def
_embed
(
self
,
inputs
:
List
[
str
]
|
str
)
->
List
[
List
[
float
]]:
def
_embed
(
self
,
inputs
:
List
[
str
]
|
str
)
->
List
[
List
[
float
]]:
"""Abstrakte Methode – muss in Unterklassen implementiert werden."""
raise
NotImplementedError
raise
NotImplementedError
(
"Subclass must implement _embed method."
)
# -----------------------------
# subclass
# -----------------------------
class
OpenAILikeEmbeddings
(
BaseEmbeddings
):
class
OpenAILikeEmbeddings
(
BaseEmbeddings
):
"""
Embeddings-Wrapper für OpenAI-ähnliche APIs (z. B. OpenAI, Ollama, TogetherAI).
"""
def
__init__
(
self
,
config
:
OpenAILikeConfig
)
->
None
:
def
__init__
(
self
,
config
:
OpenAILikeConfig
)
->
None
:
super
().
__init__
(
target_dim
=
config
.
target_dim
)
super
().
__init__
(
target_dim
=
config
.
target_dim
)
self
.
base_url
=
config
.
base_url
.
rstrip
(
"/"
)
self
.
base_url
=
config
.
base_url
.
rstrip
(
"/"
)
...
@@ -103,11 +68,7 @@ class OpenAILikeEmbeddings(BaseEmbeddings):
...
@@ -103,11 +68,7 @@ class OpenAILikeEmbeddings(BaseEmbeddings):
self
.
model
=
config
.
model
self
.
model
=
config
.
model
def
_embed
(
self
,
inputs
:
List
[
str
]
|
str
)
->
List
[
List
[
float
]]:
def
_embed
(
self
,
inputs
:
List
[
str
]
|
str
)
->
List
[
List
[
float
]]:
"""Ruft die externe Embedding-API auf."""
client
=
OpenAI
(
api_key
=
self
.
api_key
,
base_url
=
self
.
base_url
)
client
=
OpenAI
(
api_key
=
self
.
api_key
,
base_url
=
self
.
base_url
,
)
response
=
client
.
embeddings
.
create
(
response
=
client
.
embeddings
.
create
(
input
=
inputs
,
input
=
inputs
,
model
=
self
.
model
,
model
=
self
.
model
,
...
@@ -118,16 +79,12 @@ class OpenAILikeEmbeddings(BaseEmbeddings):
...
@@ -118,16 +79,12 @@ class OpenAILikeEmbeddings(BaseEmbeddings):
if
not
isinstance
(
data
,
list
):
if
not
isinstance
(
data
,
list
):
raise
ValueError
(
"Embedding response missing 'data' list."
)
raise
ValueError
(
"Embedding response missing 'data' list."
)
# Sortiere nach Index, falls nötig
data_sorted
=
sorted
(
data
,
key
=
_embedding_item_index
)
embeddings
:
List
[
List
[
float
]]
=
[]
embeddings
:
List
[
List
[
float
]]
=
[]
for
item
in
data_
sorted
:
for
item
in
sorted
(
data
,
key
=
_embedding_item_index
)
:
emb
=
_embedding_item_vector
(
item
)
emb
=
_embedding_item_vector
(
item
)
if
not
isinstance
(
emb
,
list
):
if
not
isinstance
(
emb
,
list
):
raise
ValueError
(
"Embedding item missing 'embedding' list."
)
raise
ValueError
(
"Embedding item missing 'embedding' list."
)
embeddings
.
append
(
self
.
_truncate
([
float
(
x
)
for
x
in
emb
]))
embeddings
.
append
(
self
.
_truncate
([
float
(
x
)
for
x
in
emb
]))
return
embeddings
return
embeddings
...
@@ -144,10 +101,6 @@ def _embedding_item_vector(item: object) -> object:
...
@@ -144,10 +101,6 @@ def _embedding_item_vector(item: object) -> object:
class
SentenceTransformerEmbeddings
(
BaseEmbeddings
):
class
SentenceTransformerEmbeddings
(
BaseEmbeddings
):
"""
Embeddings-Wrapper für lokale SentenceTransformer Modelle.
"""
def
__init__
(
self
,
config
:
SentenceTransformerConfig
)
->
None
:
def
__init__
(
self
,
config
:
SentenceTransformerConfig
)
->
None
:
super
().
__init__
(
target_dim
=
config
.
target_dim
)
super
().
__init__
(
target_dim
=
config
.
target_dim
)
self
.
model_name
=
config
.
model
self
.
model_name
=
config
.
model
...
@@ -155,61 +108,31 @@ class SentenceTransformerEmbeddings(BaseEmbeddings):
...
@@ -155,61 +108,31 @@ class SentenceTransformerEmbeddings(BaseEmbeddings):
@
property
@
property
def
model
(
self
)
->
SentenceTransformer
:
def
model
(
self
)
->
SentenceTransformer
:
"""Liefert das SentenceTransformer-Modell (lazy load)."""
if
self
.
_model
is
None
:
if
self
.
_model
is
None
:
self
.
_model
=
SentenceTransformer
(
self
.
_model
=
SentenceTransformer
(
self
.
model_name
,
trust_remote_code
=
True
)
self
.
model_name
,
trust_remote_code
=
True
,
)
self
.
_model
.
max_seq_length
=
512
self
.
_model
.
max_seq_length
=
512
return
self
.
_model
return
self
.
_model
def
embed_documents
(
self
,
texts
:
List
[
str
])
->
List
[
List
[
float
]]:
def
embed_documents
(
self
,
texts
:
List
[
str
])
->
List
[
List
[
float
]]:
"""Generiert Embeddings für eine Liste von Texten."""
vectors
=
self
.
model
.
encode
(
passage_embeddings
=
self
.
model
.
encode
(
sentences
=
texts
,
sentences
=
texts
,
task
=
"retrieval"
,
task
=
"retrieval"
,
prompt_name
=
"document"
,
prompt_name
=
"document"
,
)
)
return
[
self
.
_truncate
([
float
(
x
)
for
x
in
emb
])
for
emb
in
passage_embedding
s
]
return
[
self
.
_truncate
([
float
(
x
)
for
x
in
emb
])
for
emb
in
vector
s
]
def
embed_query
(
self
,
text
:
str
)
->
List
[
float
]:
def
embed_query
(
self
,
text
:
str
)
->
List
[
float
]:
"""Generiert ein Embedding für einen einzelnen Text."""
vectors
=
self
.
model
.
encode
(
query_embeddings
=
self
.
model
.
encode
(
sentences
=
[
text
],
sentences
=
[
text
],
task
=
"retrieval"
,
task
=
"retrieval"
,
prompt_name
=
"query"
,
prompt_name
=
"query"
,
)
)
return
self
.
_truncate
([
float
(
x
)
for
x
in
query_embeddings
[
0
]])
return
self
.
_truncate
([
float
(
x
)
for
x
in
vectors
[
0
]])
# -----------------------------
def
create_embedder
(
config
:
EmbeddingConfig
)
->
BaseEmbeddings
:
# Factory: creats embeddings based on config
if
config
.
embedding_type
==
EmbeddingType
.
OPENAI_LIKE
:
# -----------------------------
return
OpenAILikeEmbeddings
(
config
=
config
)
if
config
.
embedding_type
==
EmbeddingType
.
SENTENCE_TRANSFORMER
:
class
EmbeddingFactory
:
return
SentenceTransformerEmbeddings
(
config
=
config
)
"""
raise
ValueError
(
f
"Unsupported embedding type:
{
config
.
embedding_type
}
"
)
Factory-Klasse zur dynamischen Erzeugung von Embeddings-Instanzen.
"""
@
staticmethod
def
create
(
config
:
EmbeddingConfig
)
->
BaseEmbeddings
:
"""
Erzeugt eine Embeddings-Instanz basierend auf der Konfiguration.
Args:
config (EmbeddingConfig): Die Konfiguration mit Typ und Details.
Returns:
BaseEmbeddings: Instanz der passenden Embeddings-Klasse.
Raises:
ValueError: Wenn der Typ nicht unterstützt wird.
"""
if
config
.
embedding_type
==
EmbeddingType
.
OPENAI_LIKE
:
return
OpenAILikeEmbeddings
(
config
=
config
)
elif
config
.
embedding_type
==
EmbeddingType
.
SENTENCE_TRANSFORMER
:
return
SentenceTransformerEmbeddings
(
config
=
config
)
else
:
raise
ValueError
(
f
"Unsupported embedding type:
{
config
.
embedding_type
}
"
)
math-tutor/backend/app/deterministic_services/orchestrators/orchestrator_base.py
View file @
6cf9eb8c
...
@@ -2,13 +2,10 @@ from __future__ import annotations
...
@@ -2,13 +2,10 @@ from __future__ import annotations
import
time
import
time
from
dataclasses
import
dataclass
from
dataclasses
import
dataclass
from
datetime
import
datetime
from
zoneinfo
import
ZoneInfo
from
typing
import
Any
,
Callable
,
List
,
TypeVar
from
typing
import
Any
,
Callable
,
List
,
TypeVar
import
app.config
as
config
import
app.config
as
config
from
app.deterministic_services
import
(
from
app.deterministic_services
import
(
Source
,
context_store
,
context_store
,
embedding_provider
,
embedding_provider
,
referenz_decoder
,
referenz_decoder
,
...
@@ -16,6 +13,8 @@ from app.deterministic_services import (
...
@@ -16,6 +13,8 @@ from app.deterministic_services import (
tool_log_context
,
tool_log_context
,
tool_logging
,
tool_logging
,
)
)
from
app.deterministic_services.context_stores.context_store_base
import
berlin_now_iso
from
app.deterministic_services.retrieval_store
import
Source
T
=
TypeVar
(
"T"
)
T
=
TypeVar
(
"T"
)
...
@@ -40,16 +39,12 @@ def is_new_chat(messages: list[dict]) -> bool:
...
@@ -40,16 +39,12 @@ def is_new_chat(messages: list[dict]) -> bool:
# Timekeeping
# Timekeeping
# ---
# ---
def
_utc_now_iso
()
->
str
:
return
datetime
.
now
(
ZoneInfo
(
"Europe/Berlin"
)).
strftime
(
"%Y-%m-%dT%H:%M:%SZ"
)
def
_start_timing
()
->
tuple
[
str
,
float
]:
def
_start_timing
()
->
tuple
[
str
,
float
]:
return
_utc
_now_iso
(),
time
.
perf_counter
()
return
berlin
_now_iso
(),
time
.
perf_counter
()
def
_finish_timing
(
started_perf
:
float
)
->
tuple
[
str
,
float
]:
def
_finish_timing
(
started_perf
:
float
)
->
tuple
[
str
,
float
]:
finished_at
=
_utc
_now_iso
()
finished_at
=
berlin
_now_iso
()
duration_ms
=
round
((
time
.
perf_counter
()
-
started_perf
)
*
1000
,
2
)
duration_ms
=
round
((
time
.
perf_counter
()
-
started_perf
)
*
1000
,
2
)
return
finished_at
,
duration_ms
return
finished_at
,
duration_ms
...
...
math-tutor/backend/app/deterministic_services/orchestrators/registry.py
deleted
100644 → 0
View file @
d9b6c146
from
__future__
import
annotations
from
typing
import
Any
from
app.deterministic_services.orchestrators
import
(
orchestrator_qa
,
orchestrator_socratic
,
orchestrator_task
,
orchestrator_tutor
,
)
AVAILABLE_ORCHESTRATORS
:
tuple
[
str
,
...]
=
(
"qa"
,
"tutor"
,
"task"
,
"socratic"
)
DEFAULT_ORCHESTRATOR
=
"qa"
_ORCHESTRATOR_MODULES
:
dict
[
str
,
Any
]
=
{
"qa"
:
orchestrator_qa
,
"tutor"
:
orchestrator_tutor
,
"task"
:
orchestrator_task
,
"socratic"
:
orchestrator_socratic
,
}
def
get_default_orchestrator
()
->
str
:
return
DEFAULT_ORCHESTRATOR
def
is_valid_orchestrator
(
value
:
str
)
->
bool
:
return
value
in
_ORCHESTRATOR_MODULES
def
resolve_orchestrator
(
value
:
str
|
None
=
None
)
->
tuple
[
str
,
Any
]:
if
value
:
normalized
=
value
.
strip
().
lower
()
if
normalized
in
_ORCHESTRATOR_MODULES
:
return
normalized
,
_ORCHESTRATOR_MODULES
[
normalized
]
default_name
=
get_default_orchestrator
()
return
default_name
,
_ORCHESTRATOR_MODULES
[
default_name
]
math-tutor/backend/app/deterministic_services/retrieval_store.py
View file @
6cf9eb8c
...
@@ -13,14 +13,6 @@ from pydantic import BaseModel
...
@@ -13,14 +13,6 @@ from pydantic import BaseModel
from
app.deterministic_services.vector_store
import
EmbeddingLike
,
embed_query
from
app.deterministic_services.vector_store
import
EmbeddingLike
,
embed_query
def
_load_subsection_store
():
from
app.deterministic_services
import
vector_store_subsection
return
vector_store_subsection
class
SourceID
(
BaseModel
):
class
SourceID
(
BaseModel
):
chapter_title
:
Optional
[
str
]
=
None
chapter_title
:
Optional
[
str
]
=
None
section_title
:
Optional
[
str
]
=
None
section_title
:
Optional
[
str
]
=
None
...
@@ -29,16 +21,6 @@ class SourceID(BaseModel):
...
@@ -29,16 +21,6 @@ class SourceID(BaseModel):
title
:
str
title
:
str
doc_type
:
str
doc_type
:
str
def
to_dict
(
self
)
->
Dict
[
str
,
Any
]:
return
{
"chapter_title"
:
self
.
chapter_title
,
"section_title"
:
self
.
section_title
,
"subsection_title"
:
self
.
subsection_title
,
"subsubsection_title"
:
self
.
subsubsection_title
,
"title"
:
self
.
title
,
"doc_type"
:
self
.
doc_type
,
}
def
to_string
(
self
)
->
str
:
def
to_string
(
self
)
->
str
:
string_rep
=
self
.
title
string_rep
=
self
.
title
if
self
.
subsubsection_title
:
if
self
.
subsubsection_title
:
...
@@ -59,15 +41,6 @@ class Source(BaseModel):
...
@@ -59,15 +41,6 @@ class Source(BaseModel):
score
:
float
score
:
float
markdown
:
str
markdown
:
str
def
to_dict
(
self
)
->
Dict
[
str
,
Any
]:
return
{
"source_id"
:
self
.
source_id
.
to_dict
(),
"retrieved_as"
:
self
.
retrieved_as
,
"source_type"
:
self
.
source_type
,
"score"
:
self
.
score
,
"markdown"
:
self
.
markdown
,
}
def
to_string
(
self
)
->
str
:
def
to_string
(
self
)
->
str
:
return
(
return
(
f
"Source(source_id=
{
self
.
source_id
.
to_string
()
}
,
\n
"
f
"Source(source_id=
{
self
.
source_id
.
to_string
()
}
,
\n
"
...
@@ -86,16 +59,6 @@ class Retrieved:
...
@@ -86,16 +59,6 @@ class Retrieved:
metadata
:
Dict
[
str
,
Any
]
metadata
:
Dict
[
str
,
Any
]
markdown
:
str
markdown
:
str
def
to_dict
(
self
)
->
Dict
[
str
,
Any
]:
return
{
"uid"
:
self
.
uid
,
"doc_type"
:
self
.
doc_type
,
"score"
:
self
.
score
,
"metadata"
:
self
.
metadata
,
"markdown"
:
self
.
markdown
,
}
def
_row_to_retrieved
(
row
:
Dict
[
str
,
Any
],
source_type
:
Optional
[
str
]
=
None
)
->
Retrieved
:
def
_row_to_retrieved
(
row
:
Dict
[
str
,
Any
],
source_type
:
Optional
[
str
]
=
None
)
->
Retrieved
:
meta
=
{
meta
=
{
"uid"
:
row
[
"uid"
],
"uid"
:
row
[
"uid"
],
...
...
math-tutor/backend/app/deterministic_services/session_store.py
View file @
6cf9eb8c
...
@@ -5,22 +5,17 @@ from __future__ import annotations
...
@@ -5,22 +5,17 @@ from __future__ import annotations
import
json
import
json
import
os
import
os
from
collections
import
deque
from
collections
import
deque
from
datetime
import
datetime
from
threading
import
Lock
from
threading
import
Lock
from
typing
import
Any
from
typing
import
Any
from
zoneinfo
import
ZoneInfo
from
app.deterministic_services
import
context_store
from
app.deterministic_services
import
context_store
from
app.deterministic_services.context_stores.context_store_base
import
berlin_now_iso
_LOCK
=
Lock
()
_LOCK
=
Lock
()
_LOG_DIR
=
os
.
path
.
join
(
"logs"
,
"chat_sessions"
)
_LOG_DIR
=
os
.
path
.
join
(
"logs"
,
"chat_sessions"
)
_LOG_PATH
=
os
.
path
.
join
(
_LOG_DIR
,
"archive.jsonl"
)
_LOG_PATH
=
os
.
path
.
join
(
_LOG_DIR
,
"archive.jsonl"
)
def
_mez_now
()
->
str
:
return
datetime
.
now
(
ZoneInfo
(
"Europe/Berlin"
)).
strftime
(
"%Y-%m-%dT%H:%M:%SZ"
)
def
_extract_selected_task
(
sheet
:
dict
[
str
,
Any
])
->
dict
[
str
,
str
]
|
None
:
def
_extract_selected_task
(
sheet
:
dict
[
str
,
Any
])
->
dict
[
str
,
str
]
|
None
:
file_id
=
str
(
sheet
.
get
(
"task_file_id"
,
""
)).
strip
()
file_id
=
str
(
sheet
.
get
(
"task_file_id"
,
""
)).
strip
()
task_id
=
str
(
sheet
.
get
(
"task_id"
,
""
)).
strip
()
task_id
=
str
(
sheet
.
get
(
"task_id"
,
""
)).
strip
()
...
@@ -51,7 +46,7 @@ def archive_chat(
...
@@ -51,7 +46,7 @@ def archive_chat(
record
=
{
record
=
{
"chat_id"
:
chat_id
,
"chat_id"
:
chat_id
,
"saved_at"
:
_mez_now
(),
"saved_at"
:
berlin_now_iso
(),
"orchestrator"
:
str
(
orchestrator
or
""
).
strip
().
lower
()
or
None
,
"orchestrator"
:
str
(
orchestrator
or
""
).
strip
().
lower
()
or
None
,
"history"
:
sheet
.
get
(
"history"
,
[]),
"history"
:
sheet
.
get
(
"history"
,
[]),
"context_sheet"
:
context_store
.
format_sheet
(
sheet
),
"context_sheet"
:
context_store
.
format_sheet
(
sheet
),
...
...
math-tutor/backend/requirements.txt
View file @
6cf9eb8c
...
@@ -2,13 +2,11 @@ fastapi
...
@@ -2,13 +2,11 @@ fastapi
uvicorn
uvicorn
python-dotenv
python-dotenv
mpxpy
mpxpy
pillow
httpx
httpx
ollama
ollama
openai
openai
mistralai
mistralai
mcp
mcp
sympy
psycopg[binary]
psycopg[binary]
pgvector
pgvector
pyyaml
pyyaml
...
...
math-tutor/backend/scripts/retrieval_cli.py
View file @
6cf9eb8c
...
@@ -3,7 +3,7 @@
...
@@ -3,7 +3,7 @@
from
__future__
import
annotations
from
__future__
import
annotations
from
app.deterministic_services
import
retrieval_store
,
vector_store
from
app.deterministic_services
import
retrieval_store
,
vector_store
from
app.deterministic_services.embeddings
import
BaseEmbeddings
,
EmbeddingFactory
from
app.deterministic_services.embeddings
import
BaseEmbeddings
,
create_embedder
import
argparse
import
argparse
import
os
import
os
...
@@ -21,7 +21,7 @@ if str(ROOT_DIR) not in sys.path:
...
@@ -21,7 +21,7 @@ if str(ROOT_DIR) not in sys.path:
def
build_embedder
()
->
BaseEmbeddings
:
def
build_embedder
()
->
BaseEmbeddings
:
return
EmbeddingFactory
.
create
(
config
.
get_embedding_settings
())
return
create_embedder
(
config
.
get_embedding_settings
())
def
cli_init_db
(
args
:
argparse
.
Namespace
)
->
None
:
def
cli_init_db
(
args
:
argparse
.
Namespace
)
->
None
:
...
...
math-tutor/backend/test/retrieval_store_test.py
View file @
6cf9eb8c
...
@@ -5,7 +5,7 @@ os.environ.setdefault("EMBEDDING_PROVIDER", "sentence-transformer")
...
@@ -5,7 +5,7 @@ os.environ.setdefault("EMBEDDING_PROVIDER", "sentence-transformer")
os
.
environ
.
setdefault
(
"EMBEDDING_TYPE"
,
"sentence-transformer"
)
os
.
environ
.
setdefault
(
"EMBEDDING_TYPE"
,
"sentence-transformer"
)
from
app
import
config
from
app
import
config
from
app.deterministic_services.embeddings
import
EmbeddingFactory
from
app.deterministic_services.embeddings
import
create_embedder
from
app.deterministic_services
import
retrieval_store
from
app.deterministic_services
import
retrieval_store
...
@@ -70,7 +70,7 @@ def main() -> None:
...
@@ -70,7 +70,7 @@ def main() -> None:
args
=
parser
.
parse_args
()
args
=
parser
.
parse_args
()
pg_url
=
args
.
pg
or
config
.
get_postgres_url
()
pg_url
=
args
.
pg
or
config
.
get_postgres_url
()
embedder
=
EmbeddingFactory
.
create
(
config
.
get_embedding_settings
())
embedder
=
create_embedder
(
config
.
get_embedding_settings
())
subsection_refs
=
_parse_subsections
(
subsection_refs
=
_parse_subsections
(
args
.
subsections
,
args
.
chapter_index
,
args
.
section_index
args
.
subsections
,
args
.
chapter_index
,
args
.
section_index
...
...
math-tutor/backend/test/vector_store_pipeline_unit_test.py
View file @
6cf9eb8c
...
@@ -7,8 +7,9 @@ from unittest.mock import patch
...
@@ -7,8 +7,9 @@ from unittest.mock import patch
os
.
environ
.
setdefault
(
"EMBEDDING_PROVIDER"
,
"sentence-transformer"
)
os
.
environ
.
setdefault
(
"EMBEDDING_PROVIDER"
,
"sentence-transformer"
)
os
.
environ
.
setdefault
(
"EMBEDDING_TYPE"
,
"sentence-transformer"
)
os
.
environ
.
setdefault
(
"EMBEDDING_TYPE"
,
"sentence-transformer"
)
from
app.deterministic_services
import
Source
as
PackageSource
,
SourceID
as
PackageSourceID
from
app.deterministic_services.retrieval_store
import
(
from
app.deterministic_services.retrieval_store
import
(
Source
as
PackageSource
,
SourceID
as
PackageSourceID
,
Retrieved
,
Retrieved
,
Source
,
Source
,
SourceID
,
SourceID
,
...
...
math-tutor/frontend/src/state/tutorSession.tsx
View file @
6cf9eb8c
...
@@ -12,7 +12,7 @@ import {
...
@@ -12,7 +12,7 @@ import {
import
{
t
}
from
"
../i18n
"
;
import
{
t
}
from
"
../i18n
"
;
import
{
import
{
DEFAULT_ORCHESTRATOR
,
DEFAULT_ORCHESTRATOR
,
FALLBACK_
ORCHESTRATORS
,
ORCHESTRATORS
,
isTaskCoupledOrchestrator
,
isTaskCoupledOrchestrator
,
type
OrchestratorName
,
type
OrchestratorName
,
}
from
"
../utils/orchestrator
"
;
}
from
"
../utils/orchestrator
"
;
...
@@ -102,7 +102,7 @@ export function TutorSessionProvider({ children }: PropsWithChildren) {
...
@@ -102,7 +102,7 @@ export function TutorSessionProvider({ children }: PropsWithChildren) {
const
[
selectedOrchestrator
,
setSelectedOrchestratorState
]
=
const
[
selectedOrchestrator
,
setSelectedOrchestratorState
]
=
useState
<
OrchestratorName
>
(
DEFAULT_ORCHESTRATOR
);
useState
<
OrchestratorName
>
(
DEFAULT_ORCHESTRATOR
);
const
[
availableOrchestrators
,
setAvailableOrchestrators
]
=
useState
<
OrchestratorName
[]
>
(
const
[
availableOrchestrators
,
setAvailableOrchestrators
]
=
useState
<
OrchestratorName
[]
>
(
()
=>
[...
FALLBACK_
ORCHESTRATORS
]
()
=>
[...
ORCHESTRATORS
]
);
);
const
[
isTasksInitialized
,
setIsTasksInitialized
]
=
useState
(
false
);
const
[
isTasksInitialized
,
setIsTasksInitialized
]
=
useState
(
false
);
const
[
taskFiles
,
setTaskFiles
]
=
useState
<
TaskFile
[]
>
([]);
const
[
taskFiles
,
setTaskFiles
]
=
useState
<
TaskFile
[]
>
([]);
...
@@ -313,7 +313,7 @@ export function TutorSessionProvider({ children }: PropsWithChildren) {
...
@@ -313,7 +313,7 @@ export function TutorSessionProvider({ children }: PropsWithChildren) {
const
initTasks
=
useCallback
(
async
()
=>
{
const
initTasks
=
useCallback
(
async
()
=>
{
try
{
try
{
const
available
=
[...
FALLBACK_
ORCHESTRATORS
];
const
available
=
[...
ORCHESTRATORS
];
const
nextOrchestrator
=
available
.
includes
(
selectedOrchestrator
)
const
nextOrchestrator
=
available
.
includes
(
selectedOrchestrator
)
?
selectedOrchestrator
?
selectedOrchestrator
:
DEFAULT_ORCHESTRATOR
;
:
DEFAULT_ORCHESTRATOR
;
...
@@ -322,7 +322,7 @@ export function TutorSessionProvider({ children }: PropsWithChildren) {
...
@@ -322,7 +322,7 @@ export function TutorSessionProvider({ children }: PropsWithChildren) {
setSelectedOrchestratorState
(
nextOrchestrator
);
setSelectedOrchestratorState
(
nextOrchestrator
);
await
loadSelectionData
(
nextOrchestrator
);
await
loadSelectionData
(
nextOrchestrator
);
}
catch
(
error
)
{
}
catch
(
error
)
{
setAvailableOrchestrators
([...
FALLBACK_
ORCHESTRATORS
]);
setAvailableOrchestrators
([...
ORCHESTRATORS
]);
setSelectedOrchestratorState
(
DEFAULT_ORCHESTRATOR
);
setSelectedOrchestratorState
(
DEFAULT_ORCHESTRATOR
);
await
loadSelectionData
(
DEFAULT_ORCHESTRATOR
);
await
loadSelectionData
(
DEFAULT_ORCHESTRATOR
);
void
error
;
void
error
;
...
...
math-tutor/frontend/src/utils/orchestrator.ts
View file @
6cf9eb8c
...
@@ -4,8 +4,6 @@ export type OrchestratorName = (typeof ORCHESTRATORS)[number];
...
@@ -4,8 +4,6 @@ export type OrchestratorName = (typeof ORCHESTRATORS)[number];
export
const
DEFAULT_ORCHESTRATOR
:
OrchestratorName
=
"
qa
"
;
export
const
DEFAULT_ORCHESTRATOR
:
OrchestratorName
=
"
qa
"
;
export
const
FALLBACK_ORCHESTRATORS
:
OrchestratorName
[]
=
[...
ORCHESTRATORS
];
export
const
normalizeOrchestrator
=
(
export
const
normalizeOrchestrator
=
(
value
:
string
|
null
|
undefined
value
:
string
|
null
|
undefined
):
OrchestratorName
|
null
=>
{
):
OrchestratorName
|
null
=>
{
...
...
math-tutor/frontend/src/utils/orchestratorRoutes.ts
deleted
100644 → 0
View file @
d9b6c146
export
{
getSelectionRouteForOrchestrator
,
isSocraticOrchestrator
,
isTaskSelectionOrchestrator
,
type
OrchestratorName
,
}
from
"
./orchestrator
"
;
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