landfeedAPI · MCP · 2.0.0EnglishAgent Markdown

Rozmowa i strumień SSE

Wyślij POST /v1/ask z kluczem konta. Accept: text/event-stream wybiera ask-v2. Dla JSON ustaw version: ask-v2, aby dostać zdarzenia w tablicy events i pole continuation. Zwykły JSON bez pól rozmowy zachowuje format odpowiedzi ask-v1. Opis operacji zawiera pełny schemat.

Na pl-prod planowaniem wywołań zajmuje się codex-app-server, z jawnie wybranym modelem i osobnym logowaniem ChatGPT zarządzanym przez CLI dla usługi. Fakty pochodzą z odpowiedzi API. Brak konfiguracji lub logowania oznacza odmowę bez opłaty. Opcjonalny dostawca Anthropic wymaga klucza API; tokeny subskrypcji OAuth są odrzucane.

Minimalny klient strumienia

Python używa biblioteki standardowej. Zapisz kod jako ask-stream.py, ustaw LANDFEED_API_KEY i uruchom python3 ask-stream.py. Nie ponawia wywołania automatycznie. Błąd przed przyjęciem strumienia przychodzi jako odpowiedź JSON z kodem HTTP.

import json
import os
import urllib.request

request = urllib.request.Request(
    "https://mcp.agentdzialek.pl/v1/ask",
    method="POST",
    headers={"X-Api-Key": os.environ["LANDFEED_API_KEY"],
             "Content-Type": "application/json", "Accept": "text/event-stream"},
    data=json.dumps({"question": "Czy działka leży w obszarze zagrożenia powodziowego?",
                     "context": {"parcel_id": "146501_1.0101.12/3"},
                     "max_credits": 5, "version": "ask-v2"}).encode("utf-8"),
)
artifacts, citations, text, continuation = [], [], "", None
name, data_lines, done, failed = None, [], False, False
with urllib.request.urlopen(request, timeout=30) as response:
    if not response.headers.get("Content-Type", "").startswith("text/event-stream"):
        raise RuntimeError("Expected SSE; inspect the JSON admission error")
    for raw_line in response:
        line = raw_line.decode("utf-8").rstrip("\r\n")
        if line.startswith(":"):
            continue
        if line.startswith("event:"):
            name = line[6:].lstrip(" ")
        elif line.startswith("data:"):
            data_lines.append(line[5:].lstrip(" "))
        elif not line and data_lines:
            data = json.loads("\n".join(data_lines))
            if name == "artifact":
                artifacts.append(data)  # Save the whole signed object, including data.
            elif name == "answer.delta":
                text += data["text"]
            elif name == "answer.citations":
                citations = data
            elif name == "error":
                failed = True
                print(json.dumps({"error": data}, ensure_ascii=False))
            elif name in ("confirm.required", "client_tool.call", "budget.exceeded"):
                print(json.dumps({"event": name, "data": data}, ensure_ascii=False))
            elif name == "done":
                done, continuation = True, data["continuation"]
                # With a preceding error, a zero total does not prove successful settlement.
                if not failed:
                    print(json.dumps({"credits_total": data["credits_total"]}))
            name, data_lines = None, []
if not done:
    raise RuntimeError("Stream interrupted; reconcile billing before replay")
print(json.dumps({"answer": text, "citations": citations, "artifacts": artifacts,
                  "continuation": continuation, "failed": failed}, ensure_ascii=False))

Ramka ma nazwę w event: i JSON w data:; pusty wiersz kończy ramkę. Przyjęty i nieprzerwany strumień kończy jedno done, również po error. Zerwane połączenie może nie dostarczyć końcowej ramki; nie dowodzi to anulowania pracy. Nie buforuj ani nie zapisuj w publicznej pamięci podręcznej uwierzytelnionego strumienia.

Zdarzenia i rozliczenie

eventZnaczenie
answer.citationsCytowania z datą i źródłem.
answer.deltaFragment odpowiedzi po rozliczeniu.
artifactCały podpisany wynik API.
budget.exceededNiewykonany plan przekracza budżet.
client_tool.callŻądanie czynności interfejsu.
confirm.requiredPauza przed działaniem wymagającym zgody.
doneKoniec i kontynuacja; koszt po rozliczeniu.
errorBłąd; sprawdź stan rozliczenia.
step.finishedZakończony krok i rozliczony koszt.
step.startedPlan i koszt przed wykonaniem.

Pełne pola i ograniczenia: referencja SSE. step.started podaje plan żądania i szacowany koszt przed wykonaniem. step.finished podaje rozliczony koszt po decyzji o całej turze. artifact zawiera dokładną odpowiedź publicznego API, a nie tekst napisany przez model. Treść odpowiedzi i nowe artefakty są udostępniane po rozliczeniu. Przy błędzie rozliczenia są wstrzymane.

Domyślny budżet to 5 kredytów, maksymalny 20. Przed pracą serwer rezerwuje cały budżet. Narzędzia korzystają z publicznych cen, a zakończona odpowiedź dodaje 1 kredyt. Tura ask-v2 zatrzymana przez budżet jest bezpłatna w całości, również za wcześniejsze odczyty; nowe artefakty i fakty nie są wtedy udostępniane. Narzędzia interfejsu i samo oczekiwanie na potwierdzenie nie dodają opłaty za odpowiedź. done.credits_total jest miarodajne dopiero po udanym rozliczeniu; po error nie traktuj zera jako potwierdzonego zwrotu.

Kontynuacja bez zapisywania rozmowy na serwerze

Klient przechowuje messages, całe podpisane obiekty z artifact i najnowszą continuation. Przy kolejnym pytaniu odeślij je jako messages, results i continuation. Podpis obejmuje dane i konto; nie modyfikuj obiektu, nie przesyłaj go między kontami. Continuation wygasa po godzinie. Każde wznowienie wymaga niepustego question, na przykład „Kontynuuj.”. Serwer nie odszukuje wyniku po samym identyfikatorze artefaktu. Do planera trafia ostatnich 20 wiadomości; starszy zamiar jest podsumowywany w bieżącym żądaniu.

confirm.required zatrzymuje pracę przed utworzeniem lub usunięciem obserwacji, rejestracją webhooka, eksportem i krokiem za więcej niż 3 kredyty. Dopiero zgoda użytkownika pozwala odesłać confirmation z właściwym step_id i approved: true wraz z podpisaną kontynuacją. Eksport pozostaje niedostępny w obecnym publicznym interfejsie. Podpis nie czyni potwierdzenia jednorazowym: ponowna rejestracja webhooka rotuje sekret i zeruje kursor, dlatego nie odtwarzaj jej automatycznie.

Przy client_tool.call wykonaj zadeklarowaną czynność interfejsu i odeślij client_results z call_id, result oraz kontynuacją. Stan interfejsu jest niezaufany i nie stanowi dowodu o działce. Limity kontekstu i rozmiarów opisano w limitach. Pytania, odpowiedzi i zawartość artefaktów nie są zapisywane jako rozmowa przez serwer; klient odpowiada za własną pamięć i retencję.

Limit czasu żądania Ask wynosi domyślnie 120 sekund. Operator może ustawić dodatnią, skończoną wartość LANDFEED_ASK_REQUEST_SECONDS. Oczekiwanie na dostawcę mieści się w pozostałym czasie. Otwarty strumień wysyła co 10 sekund komentarz : keepalive, również podczas oczekiwania na model. Komentarz nie jest zdarzeniem ani płatnym krokiem. Przekroczenie czasu kończy się error i done; niewykonany krok nie jest płatny. Zakończona praca zachowuje dotychczasowe rozliczenie.