Compare commits

4 Commits
v1 ... main

Author SHA1 Message Date
Michele
5870b5eede fix: strip markdown code fences from LLM JSON responses
Claude wraps JSON in ```json ... ``` fences even when instructed to
return raw JSON. This caused all TopicResult validations to fail with
"Invalid JSON at line 1 column 1". Strip fences before parsing.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 15:10:39 +01:00
Michele
5c06b1a342 fix: add trailing slashes to settings and prompts API calls
Without trailing slash, FastAPI's SPAStaticFiles catch-all intercepts
/api/settings and /api/prompts before the API router, returning HTML
instead of JSON (405 error in UI).

Affected endpoints:
- GET /api/settings → /api/settings/
- PUT /api/settings → /api/settings/
- GET /api/prompts → /api/prompts/

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 12:54:46 +01:00
Michele
36a7e0281d chore: mark project as deployed on VPS
URL: https://lab.mlhub.it/postgenerator/
Container: lab-postgenerator-app (1024M RAM, 1.0 CPU)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 12:07:50 +01:00
Michele
d188ce5c63 chore: add project config and deploy preparation
- .vps-lab-config.json for VPS lab context
- .gitattributes
- README.md
- .planning/agent-history.json

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 12:03:52 +01:00
6 changed files with 112 additions and 5 deletions

24
.gitattributes vendored Normal file
View File

@@ -0,0 +1,24 @@
# Auto detect text files and perform LF normalization
* text=auto
# Force LF for scripts (importante per VPS Linux)
*.sh text eol=lf
*.bash text eol=lf
deploy.sh text eol=lf
# Force LF for config files
*.yml text eol=lf
*.yaml text eol=lf
*.json text eol=lf
*.md text eol=lf
# Binary files
*.png binary
*.jpg binary
*.jpeg binary
*.gif binary
*.ico binary
*.woff binary
*.woff2 binary
*.ttf binary
*.eot binary

View File

@@ -0,0 +1 @@
{"version":"1.0","max_entries":50,"entries":[]}

28
.vps-lab-config.json Normal file
View File

@@ -0,0 +1,28 @@
{
"type": "vps-lab",
"project_name": "postgenerator",
"slug": "postgenerator",
"created_at": "2026-03-07T12:17:48Z",
"gitea": {
"repo_url": "https://git.mlhub.it/Michele/postgenerator",
"clone_url": "https://git.mlhub.it/Michele/postgenerator.git"
},
"vps": {
"deployed": true,
"url": "https://lab.mlhub.it/postgenerator/",
"last_deploy": "2026-03-09T11:06:00Z",
"container": "lab-postgenerator-app",
"path": "/opt/lab-postgenerator/"
},
"supabase": {
"enabled": false,
"project_ref": null
},
"resources": {
"limits": {
"ram_mb": 1024,
"cpu_percent": 100
},
"deployed_at": "2026-03-09T11:06:00Z"
}
}

38
README.md Normal file
View File

@@ -0,0 +1,38 @@
# postgenerator
Lab project hosted at https://lab.mlhub.it/postgenerator/
## Development
This project uses [gsd (get-shit-done)](https://github.com/glittercowboy/get-shit-done) for spec-driven development.
### Getting Started
1. Run `/gsd:new-project` to initialize project specs
2. Follow gsd workflow: discuss > plan > execute > verify
3. Run `vps-lab-deploy` when ready to deploy
### Project Structure
```
postgenerator/
├── .planning/ # gsd planning docs (created by /gsd:new-project)
│ ├── PROJECT.md # Project vision
│ ├── REQUIREMENTS.md # Scoped requirements
│ ├── ROADMAP.md # Phase breakdown
│ └── STATE.md # Project memory
├── src/ # Source code (created during development)
└── README.md # This file
```
## Repository
- **Gitea**: https://git.mlhub.it/Michele/postgenerator
- **Clone**: `git clone https://git.mlhub.it/Michele/postgenerator.git`
## Deployment
When ready to deploy, use `vps-lab-deploy` skill.
- **URL**: https://lab.mlhub.it/postgenerator/
- **VPS**: /opt/lab-postgenerator/

View File

@@ -12,6 +12,7 @@ from __future__ import annotations
import json import json
import logging import logging
import random import random
import re
import time import time
from typing import Type, TypeVar from typing import Type, TypeVar
@@ -111,9 +112,10 @@ class LLMService:
elapsed, elapsed,
) )
# Valida con Pydantic # Rimuovi eventuali code fences markdown e valida con Pydantic
clean_text = self._strip_code_fences(raw_text)
try: try:
result = response_schema.model_validate_json(raw_text) result = response_schema.model_validate_json(clean_text)
# Pausa inter-request dopo chiamata riuscita # Pausa inter-request dopo chiamata riuscita
time.sleep(self._inter_request_delay) time.sleep(self._inter_request_delay)
return result return result
@@ -259,6 +261,20 @@ class LLMService:
# Metodi privati # Metodi privati
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@staticmethod
def _strip_code_fences(text: str) -> str:
"""Rimuove i code fences markdown dalla risposta LLM.
Claude a volte wrappa il JSON in ```json ... ``` anche quando
gli si chiede di rispondere solo con JSON.
"""
stripped = text.strip()
# Rimuove ```json ... ``` o ``` ... ```
match = re.match(r"^```(?:json)?\s*\n?(.*?)\n?\s*```$", stripped, re.DOTALL)
if match:
return match.group(1).strip()
return stripped
@staticmethod @staticmethod
def _parse_retry_after(error: anthropic.RateLimitError) -> float: def _parse_retry_after(error: anthropic.RateLimitError) -> float:
"""Estrae il valore retry-after dall'eccezione RateLimitError. """Estrae il valore retry-after dall'eccezione RateLimitError.

View File

@@ -37,7 +37,7 @@ import type {
export function useSettings() { export function useSettings() {
return useQuery<Settings>({ return useQuery<Settings>({
queryKey: ['settings'], queryKey: ['settings'],
queryFn: () => apiGet<Settings>('/settings'), queryFn: () => apiGet<Settings>('/settings/'),
staleTime: 60_000, staleTime: 60_000,
}) })
} }
@@ -56,7 +56,7 @@ export function useSettingsStatus() {
export function useUpdateSettings() { export function useUpdateSettings() {
const queryClient = useQueryClient() const queryClient = useQueryClient()
return useMutation<Settings, Error, Partial<Settings>>({ return useMutation<Settings, Error, Partial<Settings>>({
mutationFn: (settings) => apiPut<Settings>('/settings', settings), mutationFn: (settings) => apiPut<Settings>('/settings/', settings),
onSuccess: () => { onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['settings'] }) queryClient.invalidateQueries({ queryKey: ['settings'] })
}, },
@@ -199,7 +199,7 @@ export function useDownloadEditedCsv() {
export function usePromptList() { export function usePromptList() {
return useQuery<PromptListResponse>({ return useQuery<PromptListResponse>({
queryKey: ['prompts'], queryKey: ['prompts'],
queryFn: () => apiGet<PromptListResponse>('/prompts'), queryFn: () => apiGet<PromptListResponse>('/prompts/'),
staleTime: 30_000, staleTime: 30_000,
}) })
} }