Documentació

API de teoria del canvi

Genera l'estructura de teoria del canvi d'un projecte en català a partir d'una descripció textual: problema, activitats, outputs, outcomes i impacte.

Resum

  • URL base: https://serveisikora.lovable.app
  • Endpoint: POST /api/v1/theory-of-change
  • Format: JSON. Idioma de la resposta: català.
  • Autenticació: Bearer token creat des del panell d'administració.
  • Origen: només dominis donats d'alta al panell.
  • Models per defecte: els configurats al servei (Mistral). Normalment només cal enviar text.

Autenticació i CORS

Totes les peticions han d'incloure aquestes capçaleres:

Authorization: Bearer <el teu token>
Content-Type: application/json

Els tokens es creen i es revoquen des del panell d'administració del servei i es desen xifrats a la base de dades. El valor complet només es mostra un cop, en el moment de crear-lo: si es perd, cal generar-ne un de nou. Revocar un token té efecte immediat i no cal tocar cap configuració del servidor.

Els dominis des dels quals es pot cridar l'API també es gestionen des del panell (alta, activació i baixa). El domini ha de coincidir exactament (esquema, domini i port si escau) i sense barra final. Per a producció es recomana cridar des del backend i no exposar mai el token al navegador.

Petició

POST /api/v1/theory-of-change
Content-Type: application/json
Authorization: Bearer <THEORY_API_TOKEN>

{
  "text": "Descripció llarga del projecte...",
  "topOutputs": 3,
  "topOutcomes": 3
}
CampTipusObligatoriDescripció
textstringDescripció del projecte. Mínim 20 caràcters.
providerstringNoProveïdor del model de llenguatge. Si no s'envia, s'usa el del servidor.
modelstringNoModel de llenguatge. Si no s'envia, s'usa el del servidor.
embeddingProviderstringNoProveïdor d'embeddings.
embeddingModelstringNoModel d'embeddings.
topOutputsnumberNoNombre d'outputs a retornar. Entre 1 i 10. Per defecte 3.
topOutcomesnumberNoNombre d'outcomes a retornar. Entre 1 i 10. Per defecte 3.

Resposta 200

{
  "problema": "Definició del problema que aborda el projecte...",
  "activitats": [
    { "id": "act_001", "nom": "Tallers setmanals", "justificacio": "..." }
  ],
  "outputs": [
    {
      "id": "out_001",
      "nom": "Participants que completen activitats formatives",
      "justificacio": "...",
      "similitud": 0.82,
      "indicadors": [
        {
          "id": "ind_001",
          "nom": "Nombre de participants que finalitzen la formació",
          "preguntes": [
            {
              "id": "pre_001",
              "tipus_pregunta": "quantitativa",
              "font_informacio": "registre del projecte",
              "pregunta_recomanada": "Quantes persones han completat la formació?",
              "opcions_resposta": null,
              "observacions": null
            }
          ]
        }
      ]
    }
  ],
  "outcomes": [
    {
      "id": "outcome_001",
      "nom": "Millora de l'autonomia de les persones participants",
      "justificacio": "...",
      "similitud": 0.79,
      "indicadors": []
    }
  ],
  "impacte": "Impacte esperat a llarg termini...",
  "meta": {
    "provider": "mistral",
    "model": "mistral-small-latest",
    "embeddingProvider": "mistral",
    "embeddingModel": "mistral-embed"
  }
}
CampDescripció
problemaDefinició del problema que aborda el projecte.
activitatsActivitats detectades o vinculades al catàleg.
outputsOutputs seleccionats per similitud semàntica amb el catàleg.
outcomesOutcomes seleccionats per similitud semàntica amb el catàleg.
outputs[].similitudPuntuació de similitud (0–1). Com més alta, més afinitat.
indicadorsIndicadors associats a cada output o outcome.
preguntesPreguntes recomanades per a cada indicador.
impacteImpacte esperat a llarg termini.
metaProveïdor i models utilitzats per generar la resposta.

Els outputs i outcomes es trien automàticament per similitud semàntica amb el catàleg d'indicadors; el model de llenguatge només redacta el problema, les activitats, l'impacte i les justificacions.

Models disponibles

Per saber quins proveïdors i models accepta el servei (llenguatge i embeddings) hi ha un endpoint de només lectura amb el mateix control d'accés que la generació: token del panell i domini autoritzat.

GET /api/v1/models
Authorization: Bearer <el teu token>
{
  "defaults": {
    "provider": "mistral",
    "model": "mistral-small-latest",
    "embeddingProvider": "mistral",
    "embeddingModel": "mistral-embed"
  },
  "providers": [
    {
      "id": "mistral",
      "label": "Mistral (EU)",
      "openSource": true,
      "configured": true,
      "chatModels": [{ "id": "mistral-small-latest", "label": "Mistral Small (obert)" }],
      "embeddingModels": [{ "id": "mistral-embed", "label": "mistral-embed" }]
    }
  ]
}

defaults indica què s'utilitza si la petició no envia provider, model, embeddingProvider o embeddingModel. configured indica si aquell proveïdor té credencials actives al servei. Els codis d'error són els mateixos de la taula següent.

curl "https://serveisikora.lovable.app/api/v1/models" \
  -H "Authorization: Bearer $THEORY_API_TOKEN"

Errors

{
  "error": {
    "code": "invalid_request",
    "message": "Petició invàlida.",
    "request_id": "0f4c0b5b-4f9a-4f9e-8df7-8bb806afc8e7",
    "details": [
      { "field": "text", "message": "El text del projecte és massa curt (mínim 20 caràcters)." }
    ]
  }
}
HTTPCodiCausa habitual
400invalid_requestJSON invàlid, text massa curt o paràmetres fora de rang.
401unauthorizedFalta el Bearer token o no coincideix.
403forbidden_originEl domini d'origen no està a la llista autoritzada.
413invalid_requestEl text supera el màxim de caràcters permès.
415invalid_requestFalta la capçalera Content-Type: application/json.
429rate_limitedMassa peticions dins la finestra configurada.
500internal_errorError intern. Fes servir el request_id per buscar els logs.
502ai_gateway_errorEl proveïdor d'IA no ha retornat una resposta vàlida.
503internal_errorEndpoint no configurat al servidor.

Integració amb Laravel

Recomanació per a l'equip de desenvolupament: cridar l'API sempre des del backend de Laravel, mai des del navegador de l'usuari final. El token només ha de viure al servidor.

1. Configuració (.env)

THEORY_API_BASE_URL=https://serveisikora.lovable.app
THEORY_API_TOKEN=token-llarg-secret

A config/services.php:

'theory_of_change' => [
    'base_url' => env('THEORY_API_BASE_URL', 'https://serveisikora.lovable.app'),
    'token'    => env('THEORY_API_TOKEN'),
    'timeout'  => env('THEORY_API_TIMEOUT', 90), // la generació pot trigar desenes de segons
],

2. Client amb el HTTP client de Laravel

use Illuminate\Support\Facades\Http;
use Illuminate\Http\Client\RequestException;

function generateTheoryOfChange(string $text): array
{
    $response = Http::withToken(config('services.theory_of_change.token'))
        ->baseUrl(config('services.theory_of_change.base_url'))
        ->acceptJson()
        ->timeout(config('services.theory_of_change.timeout'))
        ->retry(2, 1000, fn ($exception) => $exception instanceof \Illuminate\Http\Client\ConnectionException)
        ->post('/api/v1/theory-of-change', [
            'text' => $text,
            // topOutputs / topOutcomes opcionals (1-10, per defecte 3)
        ]);

    if ($response->failed()) {
        $error = $response->json('error') ?? [];
        throw new \RuntimeException(
            sprintf(
                'Theory API %s: %s (request_id: %s)',
                $error['code'] ?? $response->status(),
                $error['message'] ?? 'Error desconegut',
                $error['request_id'] ?? '-',
            ),
            $response->status(),
        );
    }

    return $response->json();
}

3. Servei dedicat (alternativa neta)

namespace App\Services;

use Illuminate\Support\Facades\Http;

class TheoryOfChangeService
{
    public function generate(string $text): array
    {
        return Http::withToken(config('services.theory_of_change.token'))
            ->baseUrl(config('services.theory_of_change.base_url'))
            ->acceptJson()
            ->timeout(90)
            ->throw()
            ->post('/api/v1/theory-of-change', ['text' => $text])
            ->json();
    }
}

4. Notes per a producció

  • Posa la crida dins d'un Job en cua (Laravel Queues): la generació pot trigar desenes de segons i no ha de bloquejar la petició web.
  • Augmenta el timeout (60–120 s) i gestiona els codis 429 (rate limit) amb reintents i espera exponencial.
  • Guarda el request_id dels errors als logs: és la referència per depurar incidències.
  • El token va al fitxer .env del servidor; mai al codi ni al frontend.
  • No cal donar d'alta cap domini a CORS si la crida és servidor a servidor; la llista d'orígens només aplica a crides des del navegador.

Exemples

cURL

curl -X POST "https://serveisikora.lovable.app/api/v1/theory-of-change" \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $THEORY_API_TOKEN" \
  -d '{"text":"Projecte d\'acompanyament a joves en risc d\'exclusió social amb tallers i orientació laboral."}'

JavaScript (backend)

const response = await fetch("https://serveisikora.lovable.app/api/v1/theory-of-change", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    Authorization: `Bearer ${process.env.THEORY_API_TOKEN}`,
  },
  body: JSON.stringify({ text: projectDescription }),
});

const result = await response.json();

if (!response.ok) {
  throw new Error(`${result.error?.code}: ${result.error?.message}`);
}

La generació pot trigar desenes de segons segons el proveïdor d'IA i la mida del text. L'endpoint antic /api/public/theory-of-change queda limitat al mateix domini de l'app i no s'ha de fer servir per a integracions noves.