Metadata-Version: 2.4
Name: finanfut-sdk
Version: 1.3.12
Summary: Official FinanFut Intelligence Python SDK
Author-email: FinanFut <support@finanfut.com>
License: MIT License
        
        Copyright (c) 2025 FinanFut
        
        Permission is hereby granted, free of charge, to any person obtaining a copy
        of this software and associated documentation files (the "Software"), to deal
        in the Software without restriction, including without limitation the rights
        to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
        copies of the Software, and to permit persons to whom the Software is
        furnished to do so, subject to the following conditions:
        
        The above copyright notice and this permission notice shall be included in all
        copies or substantial portions of the Software.
        
        THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
        IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
        FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
        AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
        LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
        OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
        SOFTWARE.
        
Project-URL: Homepage, https://finanfut.com
Project-URL: Repository, https://github.com/<EL-TEU-REPO>
Project-URL: Documentation, https://finanfut.com/docs
Requires-Python: >=3.12
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: requests>=2.31.0
Requires-Dist: pydantic>=2.6
Requires-Dist: pydantic-settings>=2
Dynamic: license-file

# FinanFut Intelligence Python SDK

SDK oficial de **FinanFut Intelligence** per integrar agents, intents, memòria i fluxos d'orquestració des de Python.

## Índex
- [Instal·lació](#instal·lació)
- [Configuració](#configuració)
- [Quickstart](#quickstart)
- [Capacitats del client](#capacitats-del-client)
- [Interact API (`/api/v1/public/requests`)](#interact-api-apiv1publicrequests)
- [Aplicacions i manifest SDK](#aplicacions-i-manifest-sdk)
- [Catàleg de models i model-settings](#catàleg-de-models-i-model-settings)
- [Documents i document QA](#documents-i-document-qa)
- [Contexts i context sessions](#contexts-i-context-sessions)
- [OCR mapping](#ocr-mapping)
- [Data import (CSV/PDF → JSON)](#data-import-csvpdf--json)
- [Billing, analytics i access tokens](#billing-analytics-i-access-tokens)
- [Gestió d'errors](#gestió-derrors)
- [CLI](#cli)
- [Llicència](#llicència)

---

## Instal·lació

```bash
pip install finanfut-sdk
```

Requisit: **Python >= 3.12**.

---

## Configuració

Pots inicialitzar el client amb paràmetres directes o bé mitjançant fitxer de configuració + variables d'entorn.

### Fitxer local (`~/.finanfut/config.json`)

```bash
mkdir -p ~/.finanfut
```

```json
{
  "api_key": "YOUR_API_KEY",
  "application_id": "YOUR_APPLICATION_ID",
  "api_url": "https://api.intelligence.finanfut.com"
}
```

### Variables d'entorn suportades

- `FINANFUT_API_KEY`
- `FINANFUT_APPLICATION_ID`
- `FINANFUT_API_URL`

`FINANFUT_API_URL` accepta host base o URL amb `/api` o `/api/v1`; l'SDK la normalitza per evitar segments duplicats.

---

## Quickstart

```python
from finanfut_sdk import FinanFutClient

client = FinanFutClient(
    api_key="YOUR_API_KEY",
    application_id="YOUR_APPLICATION_ID",
)

response = client.interact.query(
    query="Hola! Programa una reunió demà a les 10.",
    application_agent_id="YOUR_APPLICATION_AGENT_ID",
    intent_id="YOUR_INTENT_ID",
)

print(response.answer)
print(response.model, response.ai_model_id)
```

---

## Capacitats del client

`FinanFutClient` exposa aquests mòduls:

- `client.interact`
- `client.agents`
- `client.intents`
- `client.applications`
- `client.application_agents`
- `client.memory`
- `client.knowledge`
- `client.documents`
- `client.data_import`
- `client.ocr`
- `client.models`
- `client.contexts`
- `client.context_sessions`
- `client.billing`
- `client.analytics`
- `client.access_tokens`

També pots activar mode sandbox amb `FinanFutClient.for_sandbox(...)` o `dry_run=True`.

---

## Interact API (`/api/v1/public/requests`)

`client.interact.query(...)` requereix:

- `query`
- `intent_id`
- **una** referència d'agent: `application_agent_id` o `agent_slug` o `agent_id`

Paràmetres opcionals rellevants:

- `intent`: etiqueta textual de l'intent (p. ex. `knowledge.answer`) per mantenir traçabilitat humana al lifecycle del request.
- `parameters`: claus addicionals per a routing/execució (incloent compatibilitat amb `intent_name`).
- `parameters.knowledge_source`: controla on cerca el runtime per intents de coneixement. `auto` és el valor per defecte: usa el knowledge graph de l'aplicació quan no s'envia cap document/context, amb fallback a `knowledge_records`, i usa chunks quan s'envia `document_id` o `context_id`. També accepta `application`, `graph`, `application_graph`, `document`, `context` i `mixed`.
- `knowledge_source`, `knowledge_record_type`, `max_knowledge_records` i `knowledge_min_score`: dreceres tipades de `client.interact.query(...)` per no haver de construir manualment `parameters`.
- `mode`, `execution_mode`, `dry_run`, `metadata` i `extras` s'accepten per compatibilitat amb wrappers existents. Els controls no suportats com a arguments top-level pel backend es pleguen dins el payload públic; `dry_run` es propaga com a `parameters.dry_run`.

Exemple amb context i document:

```python
response = client.interact.query(
    query="Respon segons aquest context",
    intent_id="INTENT_UUID",
    intent="knowledge.answer",
    application_agent_id="APP_AGENT_UUID",
    context_id="CONTEXT_UUID",
    document_id="DOCUMENT_UUID",
    parameters={"lang": "ca"},
)

print(response.answer)
print(response.tokens)
```

Exemple amb knowledge d'aplicació per defecte:

```python
response = client.interact.query(
    query="Com es crea un torneig?",
    intent_id="INTENT_UUID",
    intent="knowledge.answer",
    application_agent_id="APP_AGENT_UUID",
)
```

Exemple combinant knowledge d'aplicació i chunks documentals:

```python
response = client.interact.query(
    query="Com es crea un torneig segons el contracte carregat?",
    intent_id="INTENT_UUID",
    intent="knowledge.answer",
    application_agent_id="APP_AGENT_UUID",
    document_id="DOCUMENT_UUID",
    parameters={"knowledge_source": "mixed"},
)
```

Streaming incremental via SDK:

```python
for chunk in client.interact.stream(
    query="Resposta curta: com es crea un torneig?",
    intent="knowledge.answer",
    application_agent_id="APP_AGENT_UUID",
    parameters={"response_length": "short"},
    timeout=60.0,
):
    if chunk.get("delta"):
        print(chunk["delta"], end="", flush=True)

    if chunk["event"] in {"request.completed", "response.completed"}:
        print("\nStream completat")

    if chunk.get("response"):
        print("\nResposta final:", chunk["response"].answer)
```

Patró de baix nivell, útil si el client vol reprendre amb `after_sequence`:

```python
request_id = client.interact.query_async(
    query="Escriu un resum executiu",
    intent="knowledge.answer",
    application_agent_id="APP_AGENT_UUID",
    stream=True,
)

last_sequence = 0
for event in client.requests.stream_events(request_id, after_sequence=last_sequence):
    data = event["data"]
    if data.get("sequence_no"):
        last_sequence = int(data["sequence_no"])

    payload = data.get("payload") or {}
    if payload.get("delta"):
        print(payload["delta"], end="", flush=True)
    if event["event"] in {"request.completed", "response.completed"}:
        break

final_response = client.requests.wait(request_id)
```

En consultes curtes sense `context_id`, `document_id`, `document_ids`,
`session_id` ni configuració de memòria explícita, `parameters={"response_length":
"short"}` permet al backend aplicar la política de baixa latència i saltar la
recuperació de memòria.

Quan el backend retorna una resposta estructurada dins `envelope.user_output.structured_json`, l'SDK la promou automàticament com a `response.answer` i també la deixa disponible a:

- `response.structured_answer`
- `response.envelope.user_output.structured_json`

Exemple per a `content.generate`:

```python
response = client.interact.query(
    query="Prepara una publicació de 100 paraules sobre la jornada",
    intent_id="INTENT_UUID",
    application_agent_id="APP_AGENT_UUID",
)

if isinstance(response.answer, dict):
    print(response.answer["post_text"])
    print(response.answer.get("image_url"))
    print(response.answer.get("freshness_status"))
```

---

### Recuperació de l’acceptació del briefing (1.3.12)

`client.requests.lookup(idempotency_key=..., session_id=...,
application_agent_id=..., intent_id=..., timeout=10)` fa un GET exacte a
`/api/v1/public/requests/lookup` sota `client.execution_scope(...)` i retorna
únicament `data.request_id`. L’aplicació S2S ha de reconstruir permisos actuals;
no utilitzis context aportat pel model. No envia feina, no cerca per prompt i
un 404 no prova absència ni autoritza reenviar. Desa l’ID abans de recollir el
resultat amb els mètodes existents. Vegeu
[contracte operatiu](../../docs/operacions/sports_briefing_acceptance_lookup.md).
La versió 1.3.12 està preparada localment; publicació i desplegament pendents.

## Aplicacions i manifest SDK

```python
apps = client.applications.list()
app = client.applications.get("APP_UUID")

manifest = client.applications.get_manifest("APP_UUID")
if manifest:
    print(manifest.manifest_version)

bootstrap = client.applications.bootstrap_manifest("APP_UUID")
if bootstrap:
    print(bootstrap.agents_by_id.keys())
```

Pots fer peticions condicionals amb ETag via `if_none_match`; si el backend respon `304`, el client retorna `None`.

---

## Catàleg de models i model-settings

```python
catalog = client.models.list(category="chat", only_active=True)

settings = client.models.list_application_settings(
    application_id=client.application_id,
    category="chat",
)

updated = client.models.update_application_setting(
    application_id=client.application_id,
    agent_ref="default",
    payload={
        "ai_model_id": catalog[0].id,
        "category": "chat",
        "temperature": 0.2,
    },
)

print(updated.ai_model_id)
```

---

## Knowledge incremental

`client.knowledge` permet enviar canvis de coneixement sense pujar documents complets cada vegada. El backend desa events pendents i `sync()` els consolida en records versionats per alimentar cerca, FAQ i agents documentals.

```python
event = client.knowledge.record(
    event_type="backend.endpoint.updated",
    record_key="sports.tournaments.create",
    record_type="backend.endpoint",
    title="Creació de tornejos",
    content="POST /tournaments crea un torneig i accepta registration_deadline.",
    metadata={"domain": "sports", "module": "tournaments"},
)

sync = client.knowledge.sync(async_=True)
results = client.knowledge.search(
    "Com creo un torneig?",
    filters={"domain": "sports"},
    min_score=0.1,
)
print(event.id, sync.updated_records, results.results[0].record.title)
```

Discovery i Knowledge Graph:

```python
discovery = client.knowledge.discover(
    frontend_routes=[
        {
            "path": "/config/tournaments",
            "component": "TournamentConfig",
            "api_paths": [{"method": "POST", "path": "/api/v1/tournaments"}],
        }
    ],
    sync_records=True,
)

graph = client.knowledge.graph(relation_type="USES_CONNECTOR")
print(discovery.nodes, len(graph.edges))
```

---

## Documents i document QA

`client.documents.upload(...)` admet `file_path`, `content` o `text`.

```python
from pathlib import Path

# Fitxer físic
pdf_doc = client.documents.upload(
    file_path=Path("knowledge.pdf"),
    document_type="context_general",
)

# Text lliure
schema_doc = client.documents.upload(
    text='{"name": "Ada", "role": "engineer"}',
    file_name="profile.json",
    mime_type="application/json",
    document_type="schema_contract",
)

print(pdf_doc.document_type, schema_doc.document_type_version)
```

Altres operacions habituals:

```python
items = client.documents.list(limit=20)
detail = client.documents.get(str(pdf_doc.id))
answer = client.documents.ask(str(pdf_doc.id), "Resumeix els punts clau")
```

Upload asíncron amb polling:

```python
accepted = client.documents.upload(
    text="# FAQ async",
    file_name="faq.md",
    mime_type="text/markdown",
    async_mode=True,
)

if hasattr(accepted, "job_id"):
    while True:
        job = client.documents.get_upload_job(str(accepted.job_id))
        if job.status == "completed":
            print("Document llest:", job.document.id if job.document else accepted.document_id)
            break
        if job.status == "failed":
            raise RuntimeError(job.error or "job_failed")
```

---

## Contexts i context sessions

### Contexts persistents

```python
ctx = client.contexts.create(
    name="Context operatiu",
    description="Documents base per al flux",
    documents=[str(pdf_doc.id)],
)

ctx = client.contexts.add_documents(ctx.id, [str(schema_doc.id)])
print(ctx.id, len(ctx.documents))
```

### Sessions de context

```python
session = client.context_sessions.create(
    name="Sessió de proves",
    context_id=ctx.id,
)

client.context_sessions.add_message(
    session.id,
    agent_name="sports_operations_agent",
    query="Quina és la següent acció recomanada?",
    answer="Revisar el calendari i validar la disponibilitat.",
)
```

---

## OCR mapping

`client.ocr.map(...)` crida `POST /api/v1/ocr/mappings` i transforma una imatge, un PDF amb text extractable, un fitxer de text o un document ja persistit en un JSON alineat amb el contracte que li passes.

Exemple amb una imatge local:

```python
from finanfut_sdk import FinanFutClient

client = FinanFutClient()

contract = {
    "type": "object",
    "additionalProperties": False,
    "required": ["competition", "home_team", "away_team", "score"],
    "properties": {
        "competition": {"type": ["string", "null"]},
        "home_team": {"type": ["string", "null"]},
        "away_team": {"type": ["string", "null"]},
        "score": {
            "type": "object",
            "additionalProperties": False,
            "required": ["home", "away"],
            "properties": {
                "home": {"type": ["integer", "null"]},
                "away": {"type": ["integer", "null"]},
            },
        },
    },
}

result = client.ocr.map(
    file_path="acta.jpg",
    profile_key="sports.match_report",
    output_contract=contract,
    instructions="Extreu només dades visibles a l'acta. No inventis valors.",
    confidence_threshold=0.78,
)

print(result.answer.mapped_json)
print(result.answer.validation.status)
print(result.answer.extraction.confidence)
```

Fonts acceptades:

- `file_path`: camí local; l'SDK llegeix el fitxer i l'envia en base64.
- `content`: bytes ja carregats en memòria.
- `content_base64`: contingut base64 o data URL ja preparat.
- `document_id`: reutilitza un document existent de la mateixa aplicació.

Opcions útils:

- `profile_key`: `generic.document_mapping` o `sports.match_report`.
- `include_raw_text_excerpt`: demana un extracte textual quan és operativament necessari.
- `persist_source`: desa la font com a document intern i retorna `document_id`.
- `timeout`: timeout HTTP per a documents grans o models més lents.

La resposta tipada és `OCRMappingResponse`. El resultat útil viu a `result.answer.mapped_json`; valida `result.answer.validation.status == "valid"` abans d'automatitzar accions. Si rep `"needs_review"`, revisa `missing_fields`, `errors`, `warnings` i `field_evidence`.

Els PDFs escanejats no s'han d'enviar com a PDF en v1. Cal convertir la pàgina a imatge i enviar-la amb `file_path`, `content` o `content_base64`.

---

## Data import (CSV/PDF → JSON)

```python
preview = client.data_import.upload_transient_csv(file_path="ingesta.csv")

result = client.data_import.transform_data(
    application_id=client.application_id,
    intent_id="UUID_INTENT_TRANSFORM_DATA",
    transient_csv=preview,
    contract_document_id="UUID_DATA_IMPORT_CONTRACT",
)

print(result.items[0] if result.items else result.response)
```

Per PDF:

```python
preview = client.data_import.upload_transient_pdf(file_path="factura.pdf")
result = client.data_import.transform_document(
    application_id=client.application_id,
    intent_id="UUID_INTENT_TRANSFORM_DOCUMENT",
    transient_document=preview,
    contract_document_id="UUID_DATA_IMPORT_CONTRACT",
)
```

---

## Billing, analytics i access tokens

```python
usage = client.billing.get_usage(period="month")
print(usage.tokens_used, usage.cost)

logs = client.analytics.logs()
requests_trace = client.analytics.requests()
print(len(logs), len(requests_trace))

tokens = client.access_tokens.list()
print(len(tokens.items))
```

---

## Gestió d'errors

L'SDK eleva excepcions tipades (`ValidationError`, `AuthenticationError`, `RateLimitError`, etc.) segons l'estat HTTP i el payload del backend.

Per errors de contracte (`UNPROCESSABLE_OUTPUT_CONTRACT`) retornats com `{"error": "<text>"}`, el missatge complet es propaga en una `ValidationError` perquè no es perdi el detall del contracte fallit.

---

## CLI

```bash
finanfut init
finanfut interact "Hola" --agent-id "APP_AGENT_UUID" --intent-id "INTENT_UUID"
finanfut usage --period month
finanfut agents list
```

Flags globals disponibles: `--api-key`, `--application-id`, `--api-url`, `--config`, `--dry-run`.

---

## Llicència

Aquest projecte es distribueix sota llicència MIT.


## Context de subjecte per a recursos vinculats (1.3.11)

El backend S2S construeix `server_scope` després de verificar l’usuari/perfil i els
permisos actuals. No l’obtinguis del navegador o del model. El receptor exigeix
API key amb contexts:interact (sessions també contexts:sessions), camps d’ExecutionScope,
issued_at/expires_at amb zona i vigència màxima de 600 s.

```python
with client.execution_scope(server_scope):
    status = client.requests.get(request_id)
    result = client.requests.wait(request_id)
```

El mateix bloc cobreix sessions i stream; consumeix els iteradors dins del bloc.
ContextVar evita mutacions compartides de headers i restaura el context en finally.
No és autenticació d’usuari ni signatura addicional; el servidor atesta el scope
amb la clau d’aplicació. No es renova automàticament la vigència en polling.
Sense scope no es poden llegir recursos vinculats; amb scope no s’adopten legacy.
1.3.11 és una preparació local pendent de publicació i rollout coordinat amb el
receptor. [Contracte operatiu](../../docs/operacions/execution_resource_authorization.md).

## Release

See [independent SDK release workflows](../../docs/operacions/sdk_release.md). Python tags use `python-sdk-v*`; TypeScript tags use `typescript-sdk-v*`. Generic `v*` tags no longer publish either package.
