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 logging
import random
import re
import time
from typing import Type, TypeVar
@@ -111,9 +112,10 @@ class LLMService:
elapsed,
)
# Valida con Pydantic
# Rimuovi eventuali code fences markdown e valida con Pydantic
clean_text = self._strip_code_fences(raw_text)
try:
result = response_schema.model_validate_json(raw_text)
result = response_schema.model_validate_json(clean_text)
# Pausa inter-request dopo chiamata riuscita
time.sleep(self._inter_request_delay)
return result
@@ -259,6 +261,20 @@ class LLMService:
# 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
def _parse_retry_after(error: anthropic.RateLimitError) -> float:
"""Estrae il valore retry-after dall'eccezione RateLimitError.

View File

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