Skip to content
eternego / docs

Perception

This page covers the endpoints that give a persona something to perceive from the dashboard: text you type, voice you record, files you attach, and chat history you import from another AI. These are how the web channel speaks into her — the counterpart to the WebSocket, which streams her replies back out.

Every route here requires a running persona — the agent is resolved through manager.find(persona_id), and if she isn't being served you get 409 ("Persona is not active."). The three delivery routes (read, hear, attach) additionally need her web channel connected: they hand the input to that channel, so if it isn't open you get 400 ("Web channel not connected"). feed does not touch the web channel — it only needs her running. Bring her up first with POST .../start. All take her id as a path param.

How a perception lands: read, hear, and attach don't process anything inline — they hand the message or file to her web channel, which fires it as a signal her agent picks up on its next beat. They return immediately with {"status": "received"}; her actual reply arrives later over the WebSocket. feed is the exception — it processes synchronously and returns when done.


Read

Send the persona a text message — exactly what typing in the chat box does.

POST /api/persona/{persona_id}/read

Path param Type Description
persona_id string Her UUID.

Request body

JSON, validated by the ReadRequest model:

Field Type Required Description
message string yes The text to send her.

Response

200 — accepted for delivery; her reply comes later over the WebSocket:

{ "status": "received" }

Errors

Status Meaning
409 She isn't running ("Persona is not active.").
400 Her web channel isn't connected ("Web channel not connected").
422 message missing or not a string (Pydantic validation).

Example

curl -s -X POST http://localhost:5000/api/persona/6c17c83c-3158-450d-8e43-0e7efea717c1/read \
  -H 'Content-Type: application/json' \
  -d '{"message": "What did the morning market scan turn up?"}'

Hear

Send the persona an audio clip as if she heard you speak just now — the live-mic path. Same backend delivery as an attached audio file; the separate endpoint preserves the difference between "she heard me speak" and "she received an audio file."

POST /api/persona/{persona_id}/hear

Path param Type Description
persona_id string Her UUID.

Request body

multipart/form-data:

Part Type Required Description
audio file yes The audio clip. The filename's extension is preserved; if it has none, .webm is assumed (the browser mic recorder's format).
caption string (form field) no Optional text to accompany the clip. Defaults to empty.

/hear does not validate the extension — any audio the mic produces is accepted. (For arbitrary uploaded files, use /attach, which gates on extension.) Whether she can transcribe it depends on her Ear organ being configured.

Response

200:

{ "status": "received" }

Errors

Status Meaning
409 She isn't running ("Persona is not active.").
400 Her web channel isn't connected ("Web channel not connected").
422 audio part missing (Pydantic/Form validation).

Example

curl -s -X POST http://localhost:5000/api/persona/6c17c83c-3158-450d-8e43-0e7efea717c1/hear \
  -F '[email protected]' \
  -F 'caption=quick question for you'

Attach

The single ingress for any attached file. The route reads the file's extension and routes it to the perception that claims it — image → her sight, audio → her hearing, document → her researcher. New file types land here too.

POST /api/persona/{persona_id}/attach

Path param Type Description
persona_id string Her UUID.

Request body

multipart/form-data:

Part Type Required Description
file file yes The file to attach. Routed by its extension (see below).
caption string (form field) no Optional text to accompany the file. Defaults to empty.

Accepted extensions

Routing is by file extension (lower-cased), from a single source of truth (config/perception.py). An extension outside all three sets is rejected with 415.

Perception Organ that handles it Extensions
Image Eye .png .jpg .jpeg .gif .webp
Audio Ear .wav .mp3 .m4a .ogg .oga .webm .flac .aac .opus
Document Researcher .pdf .csv .json .txt .md

The extension only decides which perception receives the file. Whether she can actually act on it depends on the matching organ being configured — an image with no Eye, audio with no Ear, or a document with no Researcher still gets routed, but she has nothing to perceive it with.

Response

200:

{ "status": "received" }

Errors

Status Meaning
409 She isn't running ("Persona is not active.").
400 Her web channel isn't connected ("Web channel not connected").
415 The file's extension matches no perception. detail names the rejected extension (or unknown if the filename had none) and lists every accepted one, alphabetically sorted: "No perception capability for <ext>. Accepted: <sorted list>."
422 file part missing (Pydantic/Form validation).

Example

curl -s -X POST http://localhost:5000/api/persona/9ecfd82b-ec47-4644-8941-02ecfb7e6205/attach \
  -F '[email protected]' \
  -F 'caption=what do you make of this?'

Feed

Import the person's chat history from another AI so the persona can learn from it. Unlike the others on this page, feed runs synchronously: each imported conversation is consolidated in a sandbox, the distilled context is handed to her live memory, and the call returns only when that's done. This can take a while for large histories.

POST /api/persona/{persona_id}/feed

Path param Type Description
persona_id string Her UUID.

Request body

multipart/form-data:

Part Type Required Description
history file yes The exported chat history file. Decoded as UTF-8 text and parsed according to source.
source string (form field) yes Which AI the export came from — tells the parser how to read it.

Response

200 — the persona, wrapped in a single persona key:

{
  "persona": {
    "id": "6c17c83c-3158-450d-8e43-0e7efea717c1",
    "name": "Adam",
    "thinking": { "name": "gpt-5.4", "provider": "openai", "api_key": "sk-XXXX", "url": "..." },
    "version": "v1",
    "base_model": "",
    "birthday": "2026-04-17",
    "status": "active",
    "idle_timeout": 3600,
    "imagination": null,
    "mouth": null,
    "eye": null,
    "ear": null,
    "teacher": null,
    "researcher": null,
    "channels": []
  }
}

The inner object is the raw Persona dataclass, serialized field-for-field in declaration order: id, name, thinking, version, base_model, birthday, status, idle_timeout, then each organ (imagination, mouth, eye, ear, teacher, researchernull when unset) and channels (null if she has none, since the dataclass default is None, not []). This is not the reduced view from GET /api/personas: the raw record keeps version and idle_timeout, exposes each organ's real api_key and each channel's full credentials/verified_at, and has no running flag.

Unmasked — contains real credentials

The inner persona is the raw Persona dataclass, serialized field-for-field with no masking — HTTP responses that echo a persona return real values. Each cloud organ carries its real api_key, and each channel its real credentials.token. Don't log or forward this body.

Errors

Status Meaning
409 She isn't running ("Persona is not active.").
400 The export couldn't be parsed ("Could not parse the external data. Please check the file format."), or the conversations couldn't be analyzed because her model was unreachable ("Could not analyze the conversations. Please make sure the model is running.").
422 history or source missing (Pydantic/Form validation).

Example

curl -s -X POST http://localhost:5000/api/persona/6c17c83c-3158-450d-8e43-0e7efea717c1/feed \
  -F '[email protected]' \
  -F 'source=chatgpt'

  • WebSockets — how her replies to these inputs stream back to the dashboard.
  • Knowledge — read what she's learned, including from a feed.
  • Lifecycle — bring her up so these routes work (they need a running agent).
  • The panel — Chat — the screen these routes sit behind.
  • Vocabularyorgan (Eye, Ear, Researcher), channel, person.