fastrag.query.chat#
Chat persistence: sessions, chats, and messages.
The ChatStore contract and its PostgreSQL
implementation. Storage only — no FastAPI, no HTTP types, no SSE; a serving
layer built on top of a ChatStore owns all of that. See
fastrag.query.chat.base for what a serving layer built on this
package gets: session create/read/touch, chat create/list/read/delete, and
message append/list/update-last-assistant.
- class fastrag.query.chat.Chat(id, session_id, created_at, expires_at)[source]#
Bases:
objectOne conversation: an ordered, linear list of messages.
- created_at#
Derived from
id.- Type:
- expires_at#
When this chat (and, by cascade, its messages) stops being usable. Independent of its session’s
expires_at— a chat can go idle while its session stays active.- Type:
- class fastrag.query.chat.ChatMessage(id, chat_id, created_at, role, content=None, sources=None, finish_reason=None, error_code=None, error_message=None)[source]#
Bases:
objectOne persisted turn, from
user,assistant, orsystem.A different thing from
Message, the model-facing turn aChatModelconsumes — seeto_llm_history()for the conversion.- Parameters:
id (str)
chat_id (str)
created_at (datetime)
role (Literal['user', 'assistant', 'system'])
content (str | None)
finish_reason (Literal['stop', 'length', 'error', 'interrupted'] | None)
error_code (Literal['MODEL_ERROR', 'TIMEOUT', 'RATE_LIMIT', 'CONTEXT_TOO_LONG', 'NETWORK_ERROR', 'SERVICE_UNAVAILABLE', 'INVALID_REQUEST', 'INVALID_RESPONSE', 'INTERNAL_ERROR'] | None)
error_message (str | None)
- created_at#
Derived from
id.- Type:
- role#
Who sent the turn.
- Type:
Literal[‘user’, ‘assistant’, ‘system’]
- content#
The turn’s text.
Noneonly on anassistantturn whosefinish_reasonis"error"and nothing was generated before it failed.- Type:
str | None
- sources#
The documents the answer was grounded in, serialized as plain JSON objects (a store never interprets their shape).
Nonefor auser/systemturn.
- finish_reason#
Why an
assistantturn stopped. Required forassistantturns, absent otherwise.- Type:
Literal[‘stop’, ‘length’, ‘error’, ‘interrupted’] | None
- error_code#
Set exactly when
finish_reason == "error".- Type:
Literal[‘MODEL_ERROR’, ‘TIMEOUT’, ‘RATE_LIMIT’, ‘CONTEXT_TOO_LONG’, ‘NETWORK_ERROR’, ‘SERVICE_UNAVAILABLE’, ‘INVALID_REQUEST’, ‘INVALID_RESPONSE’, ‘INTERNAL_ERROR’] | None
- class fastrag.query.chat.ChatStore[source]#
Bases:
AbstractAsyncContextManager[ChatStore],ABCPluggable abstract persistence for sessions, chats, and messages.
Storage only: no FastAPI, no HTTP types, no SSE, no
QueryEngine— a serving layer built on top of aChatStoreowns all of that.- abstractmethod async create_session(*, ttl_days, ip_address=None, country=None)[source]#
Mint and persist a new session.
- abstractmethod async touch_session(session_id, *, ttl_days, min_bump_days)[source]#
Bump an active session’s expiry, throttled.
The expiry is only written when the new value is at least
min_bump_dayslater than the stored one, so an active conversation stays alive without a write on every request.- Parameters:
- Returns:
The session as it now stands (bumped or not), or
Noneifsession_idis unknown or already expired.- Return type:
Session | None
- abstractmethod async create_chat(*, session_id, ttl_days)[source]#
Start a new, empty chat owned by
session_id.
- abstractmethod async get_chat(chat_id, *, session_id)[source]#
Fetch a chat, scoped to its owning session.
- abstractmethod async list_chats(session_id, *, before=None, limit=50)[source]#
List
session_id’s chats, newest-first.
- abstractmethod async delete_chat(chat_id, *, session_id)[source]#
Delete a chat and every message in it.
- abstractmethod async append_message(chat_id, *, role, content=None, sources=None, finish_reason=None, error_code=None, error_message=None)[source]#
Append a new turn to a chat.
- Parameters:
chat_id (str) – The chat this turn belongs to.
role (Literal['user', 'assistant', 'system']) – Who sent the turn.
content (str | None) – The turn’s text. May be
Noneonly for anassistantturn whosefinish_reasonis"error".sources (list[dict[str, Any]] | None) – The documents the answer was grounded in, as plain JSON objects. Only meaningful for an
assistantturn.finish_reason (Literal['stop', 'length', 'error', 'interrupted'] | None) – Required for an
assistantturn, must be absent otherwise.error_code (Literal['MODEL_ERROR', 'TIMEOUT', 'RATE_LIMIT', 'CONTEXT_TOO_LONG', 'NETWORK_ERROR', 'SERVICE_UNAVAILABLE', 'INVALID_REQUEST', 'INVALID_RESPONSE', 'INTERNAL_ERROR'] | None) – Set exactly when
finish_reason == "error".error_message (str | None) – Set exactly when
finish_reason == "error".
- Return type:
- abstractmethod async list_messages(chat_id, *, before=None, limit=50)[source]#
List a chat’s messages, newest-first.
- Parameters:
- Return type:
- abstractmethod async update_last_assistant_message(chat_id, *, content, sources=None, finish_reason, error_code=None, error_message=None)[source]#
Overwrite a chat’s most recent
assistantturn in place.The storage primitive behind both the
/retryflow (regenerate the last answer) and the/editflow (regenerate after editing an earlier turn): both end by replacing the chat’s latestassistantmessage with a new outcome, in place, rather than deleting and re-appending.- Parameters:
chat_id (str) – The chat whose latest
assistantmessage is updated.content (str | None) – The turn’s new text. May be
Noneonly whenfinish_reason == "error".sources (list[dict[str, Any]] | None) – The documents the new answer was grounded in.
finish_reason (Literal['stop', 'length', 'error', 'interrupted']) – Why generation stopped this time.
error_code (Literal['MODEL_ERROR', 'TIMEOUT', 'RATE_LIMIT', 'CONTEXT_TOO_LONG', 'NETWORK_ERROR', 'SERVICE_UNAVAILABLE', 'INVALID_REQUEST', 'INVALID_RESPONSE', 'INTERNAL_ERROR'] | None) – Set exactly when
finish_reason == "error".error_message (str | None) – Set exactly when
finish_reason == "error".
- Returns:
The updated message, or
Noneifchat_idhas noassistantmessage yet.- Return type:
ChatMessage | None
- class fastrag.query.chat.Page(items, has_more)[source]#
Bases:
Generic[T]One page of a ULID-cursor listing.
- class fastrag.query.chat.PostgresChatStore(*, database_url, schema='fastrag_chat', pool_size=5)[source]#
Bases:
ChatStorePersist sessions, chats, and messages on a PostgreSQL server.
async with PostgresChatStore(database_url=DATABASE_URL) as chat_store: session = await chat_store.create_session(ttl_days=30)
- Parameters:
database_url (str) –
postgresql://user:password@host:port/database. Accepted as a plainpostgresql://URL; the asyncpg driver is selected internally.schema (str) – The Postgres schema this store’s tables live in. Created if absent. Defaults to a dedicated schema so the store never collides with application tables.
pool_size (int) – Connections held by this store’s engine.
- property engine: AsyncEngine#
The store’s underlying async engine.
The serving layer (
fastrag.serve) uses this engine — not one it owns — for two Postgres-specific things that need a raw engine rather than aChatStoremethod: the per-chat advisory lock that serializes generations, and the/healthzconnection check. Requires the store to have been entered (__aenter__), else it raisesfastrag.exceptions.ChatStoreError. Not part of theChatStorecontract — a non-Postgres store may not offer one (in which case those two serving features are unavailable).
- async create_session(*, ttl_days, ip_address=None, country=None)[source]#
See
create_session().
- async get_session(session_id)[source]#
See
get_session().
- async touch_session(session_id, *, ttl_days, min_bump_days)[source]#
See
touch_session().
- async create_chat(*, session_id, ttl_days)[source]#
See
create_chat().
- async get_chat(chat_id, *, session_id)[source]#
See
get_chat().
- async list_chats(session_id, *, before=None, limit=50)[source]#
See
list_chats().
- async delete_chat(chat_id, *, session_id)[source]#
See
delete_chat().
- async append_message(chat_id, *, role, content=None, sources=None, finish_reason=None, error_code=None, error_message=None)[source]#
See
append_message().- Parameters:
chat_id (str)
role (Literal['user', 'assistant', 'system'])
content (str | None)
finish_reason (Literal['stop', 'length', 'error', 'interrupted'] | None)
error_code (Literal['MODEL_ERROR', 'TIMEOUT', 'RATE_LIMIT', 'CONTEXT_TOO_LONG', 'NETWORK_ERROR', 'SERVICE_UNAVAILABLE', 'INVALID_REQUEST', 'INVALID_RESPONSE', 'INTERNAL_ERROR'] | None)
error_message (str | None)
- Return type:
- async list_messages(chat_id, *, before=None, limit=50)[source]#
See
list_messages().- Parameters:
- Return type:
- async update_last_assistant_message(chat_id, *, content, sources=None, finish_reason, error_code=None, error_message=None)[source]#
See
update_last_assistant_message().- Parameters:
chat_id (str)
content (str | None)
finish_reason (Literal['stop', 'length', 'error', 'interrupted'])
error_code (Literal['MODEL_ERROR', 'TIMEOUT', 'RATE_LIMIT', 'CONTEXT_TOO_LONG', 'NETWORK_ERROR', 'SERVICE_UNAVAILABLE', 'INVALID_REQUEST', 'INVALID_RESPONSE', 'INTERNAL_ERROR'] | None)
error_message (str | None)
- Return type:
ChatMessage | None
- class fastrag.query.chat.Session(id, created_at, expires_at, ip_address=None, country=None)[source]#
Bases:
objectOne anonymous browser identity, owning its chats.
- Parameters:
- id#
ULID, minted on creation. The bearer credential itself — a caller who holds this id can act as this session — so it is never logged.
- Type:
- created_at#
Derived from
id, soORDER BY idandORDER BY created_atalways agree.- Type:
- expires_at#
When this session (and, by cascade, its chats and their messages) stops being usable.
- Type:
- fastrag.query.chat.to_llm_history(messages)[source]#
Turn a page of persisted messages into model-facing chat history.
Reorders
messagesinto chronological (oldest-first) order — the order everyChatModelexpects — and drops any turn withcontent is None(anassistantturn that errored before generating anything), which a chat model has nothing to replay. Alist_messagespage is newest-first, so this is normally the conversion a caller applies right before building a prompt.- Parameters:
messages (Sequence[ChatMessage])
- Return type:
Modules
The |
|
PostgreSQL-backed |
|
The tables behind |