Skip to content
Merged
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
12 changes: 12 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -96,3 +96,15 @@ coverage.xml
.hypothesis/
.pytest_cache/

# Logs e Arquivos Temporários
front/.npm-cache/
front/dist-web/
front/expo-server.log
front/expo-server.err.log
mobile/node_modules/
mobile/.expo/
mobile/.npm-cache/
mobile/dist-web/
front/.npm-cache/
front/dist-web/
*.log
2 changes: 1 addition & 1 deletion backend/.env.dev
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ DATABASE_URL=meubanco123
GEMINI_API_KEY=chave123

OLLAMA_HOST=https://ollama.com
OLLAMA_API_KEY=chave321
OLLAMA_API_KEY=
LLM_MODEL=gpt-oss:120b-cloud

EMBED_MODEL=qwen3-embedding:4b
Expand Down
15 changes: 11 additions & 4 deletions backend/services/llm_facade.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,13 @@
from services.rag_engine import RAGEngine
from services.workout_factory import WorkoutFactory


def _ollama_headers() -> dict[str, str]:
if not settings.OLLAMA_API_KEY:
return {}
return {"Authorization": f"Bearer {settings.OLLAMA_API_KEY}"}


_EXERCISE_SCHEMA = {
"type": "object",
"properties": {
Expand Down Expand Up @@ -57,7 +64,7 @@ def __init__(self, rag_engine: RAGEngine) -> None:
self._rag = rag_engine
self._client = Client(
host=settings.OLLAMA_HOST,
headers={"Authorization": f"Bearer {settings.OLLAMA_API_KEY}"},
headers=_ollama_headers(),
)

def generate_workout_plan(self, user_query: str, user: User) -> WorkoutPlan:
Expand Down Expand Up @@ -86,11 +93,11 @@ def _build_prompt(self, user_query: str, user: User, context: str | None) -> str
"Always answer in pt-BR, translate everything.\n\n"
"STRICT RULES FOR THE days ARRAY:\n"
"- Split the plan into one entry per training day. Each entry must have 'day' (integer), "
"'focus' (muscle group or session theme, e.g. 'Peito e Tríceps'), and 'exercises' (array).\n"
"'focus' (muscle group or session theme, e.g. 'Peito e Triceps'), and 'exercises' (array).\n"
"- The number of day entries must equal days_per_week.\n"
"- The 'reps' field must be compact: use only numbers or ranges (e.g. '12', '8-10'). "
"For time-based exercises use 'Xs' for seconds or 'Xmin' for minutes (e.g. '30s', '2min'). "
"Never write out words like 'repetições', 'segundos', 'minutos'.\n\n"
"Never write out words like 'repeticoes', 'segundos', 'minutos'.\n\n"
"Respond ONLY with a valid JSON object matching this schema (no markdown, no explanation):\n"
f"{schema_str}"
)
Expand All @@ -100,7 +107,7 @@ def _call_llm_json(self, prompt: str, retries: int = 2) -> dict:
chunks = []
for part in self._client.chat(model=settings.LLM_MODEL, messages=[{"role": "user", "content": prompt}], format="json", stream=True):
chunks.append(part.message.content)

content = "".join(chunks)
match = re.search(r"\{.*\}", content, re.DOTALL)
if not match:
Expand Down
30 changes: 22 additions & 8 deletions backend/services/rag_engine.py
Original file line number Diff line number Diff line change
@@ -1,30 +1,44 @@
# Serviço tlvz precise modificar ja que o banco ja ta estruturado
# Servico tlvz precise modificar ja que o banco ja ta estruturado
# pra poupar fazer busca vetorial/contextual toda vez
# e fazer um select normal
# "preciso de um treino para iniciante" -> busca no banco direto por iniciante

import ollama
from ollama import Client

from core.config import settings
from repositories.workout_repository import WorkoutRepository

EMBED_MODEL = settings.EMBED_MODEL
EMBEDDING_DIM = settings.EMBED_DIM


def _ollama_headers() -> dict[str, str]:
if not settings.OLLAMA_API_KEY:
return {}
return {"Authorization": f"Bearer {settings.OLLAMA_API_KEY}"}


class RAGEngine:
def __init__(self, workout_repository: WorkoutRepository) -> None:
self._repo = workout_repository
self._client = Client(
host=settings.OLLAMA_HOST,
headers=_ollama_headers(),
)

def _embed_query(self, query: str) -> list[float]:
response = ollama.embed(model=EMBED_MODEL, input=query, dimensions=EMBEDDING_DIM)
response = self._client.embed(model=EMBED_MODEL, input=query, dimensions=EMBEDDING_DIM)
return response["embeddings"][0]

def retrieve_context(self, query: str, n: int = 3) -> str | None:
vector = self._embed_query(query)
rows = self._repo.search_similar(vector, n=n)

try:
vector = self._embed_query(query)
rows = self._repo.search_similar(vector, n=n)
except Exception:
return None

if not rows:
return None

parts = [f"[{i}] {title}\n{chunk_text}" for i, (title, chunk_text) in enumerate(rows, start=1)]
return "\n\n---\n\n".join(parts)
return "\n\n---\n\n".join(parts)
3 changes: 2 additions & 1 deletion front/__tests__/detalheTreino.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ jest.mock("@/services/api", () => ({
jest.mock("expo-router", () => ({
useLocalSearchParams: () => mockUseLocalSearchParams(),
useRouter: () => ({ back: jest.fn(), push: jest.fn() }),
usePathname: () => '/workout/1',
}));

jest.mock("react-native-safe-area-context", () => ({
Expand Down Expand Up @@ -94,4 +95,4 @@ describe("Tela de detalhe do treino", () => {

expect(screen.getByTestId("loading")).toBeOnTheScreen();
});
});
});
Loading