sefia-fastapi 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- sefia_fastapi/__init__.py +31 -0
- sefia_fastapi/_events.py +102 -0
- sefia_fastapi/_input.py +201 -0
- sefia_fastapi/_kv.py +16 -0
- sefia_fastapi/exceptions.py +39 -0
- sefia_fastapi/py.typed +0 -0
- sefia_fastapi-0.1.0.dist-info/METADATA +85 -0
- sefia_fastapi-0.1.0.dist-info/RECORD +10 -0
- sefia_fastapi-0.1.0.dist-info/WHEEL +4 -0
- sefia_fastapi-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""FastAPI (HTTP) building blocks for Sefia applications.
|
|
2
|
+
|
|
3
|
+
This package holds the HTTP-side pieces that depend only on ``sefia`` and
|
|
4
|
+
FastAPI: the input core (an :class:`InputChannel` persisted over a
|
|
5
|
+
:class:`KeyValueStore`), per-session SSE streams (:class:`SessionEvents`),
|
|
6
|
+
and the exceptions an application maps to HTTP responses. The runtime
|
|
7
|
+
wiring — session management, persistence, and the pausing tool — is provided
|
|
8
|
+
by an integration layer such as ``sefios.fastapi``.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from ._events import SessionEvents, SSEEvent
|
|
12
|
+
from ._input import InputChannel, InputRequest
|
|
13
|
+
from ._kv import KeyValueStore
|
|
14
|
+
from .exceptions import (
|
|
15
|
+
AmbiguousInputError,
|
|
16
|
+
InputRequired,
|
|
17
|
+
UnknownInputError,
|
|
18
|
+
UnknownSessionError,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
__all__ = [
|
|
22
|
+
"InputChannel",
|
|
23
|
+
"InputRequest",
|
|
24
|
+
"KeyValueStore",
|
|
25
|
+
"SessionEvents",
|
|
26
|
+
"SSEEvent",
|
|
27
|
+
"InputRequired",
|
|
28
|
+
"UnknownSessionError",
|
|
29
|
+
"UnknownInputError",
|
|
30
|
+
"AmbiguousInputError",
|
|
31
|
+
]
|
sefia_fastapi/_events.py
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"""Per-session server-sent events: publish, token relay, and the SSE response."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import json
|
|
7
|
+
from collections.abc import AsyncIterator
|
|
8
|
+
from contextlib import asynccontextmanager
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from fastapi.encoders import jsonable_encoder
|
|
13
|
+
from fastapi.responses import StreamingResponse
|
|
14
|
+
from sefia.event_system import EventHandler
|
|
15
|
+
from sefia.llm.events import LLMTokenReceived
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class SSEEvent:
|
|
19
|
+
"""The wire names of the server-sent events an application publishes.
|
|
20
|
+
|
|
21
|
+
Single source of truth: the facade and browser clients import these rather
|
|
22
|
+
than repeating literals.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
TOKEN = "token"
|
|
26
|
+
INPUT_REQUIRED = "input_required"
|
|
27
|
+
OUTPUT = "output"
|
|
28
|
+
COMPLETED = "completed"
|
|
29
|
+
EXECUTION_FAILED = "execution_failed"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass(frozen=True)
|
|
33
|
+
class _SessionEvent:
|
|
34
|
+
name: str
|
|
35
|
+
data: Any
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class SessionEvents:
|
|
39
|
+
"""Per-session event streams for an HTTP application.
|
|
40
|
+
|
|
41
|
+
One object owns the whole surface: :meth:`publish` fans an event out to a
|
|
42
|
+
session's subscribers, :meth:`token_handler` returns a sefia event handler
|
|
43
|
+
that relays LLM tokens into the stream, and :meth:`response` serves the
|
|
44
|
+
stream as a ``text/event-stream`` response. Publishing to a session nobody
|
|
45
|
+
is subscribed to is a no-op.
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
def __init__(self):
|
|
49
|
+
self._subscribers: dict[str, set[asyncio.Queue[_SessionEvent]]] = {}
|
|
50
|
+
|
|
51
|
+
async def publish(self, session_id: str, name: str, data: Any) -> None:
|
|
52
|
+
subscribers = list(self._subscribers.get(session_id, ()))
|
|
53
|
+
if not subscribers:
|
|
54
|
+
return
|
|
55
|
+
event = _SessionEvent(name=name, data=data)
|
|
56
|
+
for queue in subscribers:
|
|
57
|
+
await queue.put(event)
|
|
58
|
+
|
|
59
|
+
def token_handler(self, session_id: str) -> EventHandler[LLMTokenReceived]:
|
|
60
|
+
"""A sefia event handler relaying LLM tokens into this session's stream."""
|
|
61
|
+
return _TokenRelay(self, session_id)
|
|
62
|
+
|
|
63
|
+
def response(self, session_id: str) -> StreamingResponse:
|
|
64
|
+
"""A ``text/event-stream`` response relaying this session's events."""
|
|
65
|
+
return StreamingResponse(
|
|
66
|
+
self._event_stream(session_id),
|
|
67
|
+
media_type="text/event-stream",
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
@asynccontextmanager
|
|
71
|
+
async def _subscribe(
|
|
72
|
+
self, session_id: str
|
|
73
|
+
) -> AsyncIterator[asyncio.Queue[_SessionEvent]]:
|
|
74
|
+
queue: asyncio.Queue[_SessionEvent] = asyncio.Queue()
|
|
75
|
+
subscribers = self._subscribers.setdefault(session_id, set())
|
|
76
|
+
subscribers.add(queue)
|
|
77
|
+
try:
|
|
78
|
+
yield queue
|
|
79
|
+
finally:
|
|
80
|
+
subscribers.discard(queue)
|
|
81
|
+
if not subscribers:
|
|
82
|
+
self._subscribers.pop(session_id, None)
|
|
83
|
+
|
|
84
|
+
async def _event_stream(self, session_id: str) -> AsyncIterator[str]:
|
|
85
|
+
async with self._subscribe(session_id) as queue:
|
|
86
|
+
while True:
|
|
87
|
+
event = await queue.get()
|
|
88
|
+
yield _format_sse_event(event.name, event.data)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class _TokenRelay(EventHandler[LLMTokenReceived]):
|
|
92
|
+
def __init__(self, events: SessionEvents, session_id: str):
|
|
93
|
+
self._events = events
|
|
94
|
+
self._session_id = session_id
|
|
95
|
+
|
|
96
|
+
async def handle(self, event: LLMTokenReceived) -> None:
|
|
97
|
+
await self._events.publish(self._session_id, SSEEvent.TOKEN, event.token)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _format_sse_event(event: str, data: Any) -> str:
|
|
101
|
+
payload = json.dumps(jsonable_encoder(data), ensure_ascii=False)
|
|
102
|
+
return f"event: {event}\ndata: {payload}\n\n"
|
sefia_fastapi/_input.py
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
"""The HTTP-side input core.
|
|
2
|
+
|
|
3
|
+
Pending prompts, provided inputs, and queued inputs are persisted through a
|
|
4
|
+
:class:`KeyValueStore`, so a paused request can be resumed by a later one. The
|
|
5
|
+
channel only sees primitives; how the runtime provides persistence (and which
|
|
6
|
+
tool raises the pause) is wired up by the integration layer.
|
|
7
|
+
|
|
8
|
+
Deliberately independent from the CLI counterpart in ``sefia_typer``: the two
|
|
9
|
+
surfaces share semantics today but are free to diverge.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from contextlib import contextmanager
|
|
13
|
+
from contextvars import ContextVar
|
|
14
|
+
from dataclasses import dataclass
|
|
15
|
+
|
|
16
|
+
from ._kv import KeyValueStore
|
|
17
|
+
from .exceptions import AmbiguousInputError, UnknownInputError
|
|
18
|
+
|
|
19
|
+
_DEFAULT_NAMESPACE = "input_channel"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass(frozen=True)
|
|
23
|
+
class InputRequest:
|
|
24
|
+
"""A pending request for external input."""
|
|
25
|
+
|
|
26
|
+
interaction_id: str
|
|
27
|
+
prompt: str
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class InputChannel:
|
|
31
|
+
"""The input pipe between an HTTP application and a paused agent.
|
|
32
|
+
|
|
33
|
+
One object owns the whole lifecycle. The tool-facing side records prompts
|
|
34
|
+
and picks up provided input (:meth:`record_request` / :meth:`provide_input`
|
|
35
|
+
/ :meth:`complete_request`); the application-facing side routes arriving
|
|
36
|
+
input to pending requests (:meth:`receive_input`); :meth:`use_store` binds
|
|
37
|
+
the persistence both sides share.
|
|
38
|
+
|
|
39
|
+
Reads observe writes made earlier in the same session because the bound
|
|
40
|
+
:class:`KeyValueStore` is expected to provide read-your-writes consistency.
|
|
41
|
+
The active binding is held in a :class:`~contextvars.ContextVar` rather
|
|
42
|
+
than a plain attribute so that a single shared channel stays correct when
|
|
43
|
+
several sessions run concurrently (e.g. one asyncio task per HTTP
|
|
44
|
+
request): each task binds and reads its own store.
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
def __init__(self, *, namespace: str = _DEFAULT_NAMESPACE):
|
|
48
|
+
namespace = namespace.strip("/")
|
|
49
|
+
if not namespace:
|
|
50
|
+
raise ValueError("Input channel namespace must not be empty.")
|
|
51
|
+
self._namespace = namespace
|
|
52
|
+
self._active_store: ContextVar[KeyValueStore | None] = ContextVar(
|
|
53
|
+
"input_active_store", default=None
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
@contextmanager
|
|
57
|
+
def use_store(self, store: KeyValueStore):
|
|
58
|
+
"""Bind the persistence backing this channel for the enclosed block."""
|
|
59
|
+
token = self._active_store.set(store)
|
|
60
|
+
try:
|
|
61
|
+
yield
|
|
62
|
+
finally:
|
|
63
|
+
self._active_store.reset(token)
|
|
64
|
+
|
|
65
|
+
async def pending(self) -> list[InputRequest]:
|
|
66
|
+
"""The requests still waiting for input, ordered by interaction id."""
|
|
67
|
+
pending = await self._pending_map()
|
|
68
|
+
return [
|
|
69
|
+
InputRequest(interaction_id=entry["id"], prompt=entry["prompt"])
|
|
70
|
+
for _, entry in sorted(pending.items())
|
|
71
|
+
]
|
|
72
|
+
|
|
73
|
+
async def receive_input(
|
|
74
|
+
self,
|
|
75
|
+
input_value: str | list[str] | None,
|
|
76
|
+
*,
|
|
77
|
+
reply_to: str | None = None,
|
|
78
|
+
) -> None:
|
|
79
|
+
"""Route request input to a pending prompt, or queue it for the next.
|
|
80
|
+
|
|
81
|
+
``None`` and blank input are ignored. With ``reply_to`` the input
|
|
82
|
+
resolves that specific request; otherwise a single pending request is
|
|
83
|
+
resolved directly, multiple pending requests raise
|
|
84
|
+
:class:`AmbiguousInputError`, and no pending request queues the input
|
|
85
|
+
for the next prompt.
|
|
86
|
+
"""
|
|
87
|
+
if input_value is None:
|
|
88
|
+
return
|
|
89
|
+
input_text = _to_input_text(input_value)
|
|
90
|
+
if not input_text:
|
|
91
|
+
return
|
|
92
|
+
|
|
93
|
+
pending = await self._pending_map()
|
|
94
|
+
|
|
95
|
+
if reply_to is not None:
|
|
96
|
+
if reply_to not in pending:
|
|
97
|
+
raise UnknownInputError(reply_to)
|
|
98
|
+
await self._store_input(reply_to, input_text)
|
|
99
|
+
return
|
|
100
|
+
|
|
101
|
+
if len(pending) == 1:
|
|
102
|
+
await self._store_input(next(iter(pending)), input_text)
|
|
103
|
+
return
|
|
104
|
+
|
|
105
|
+
if len(pending) > 1:
|
|
106
|
+
raise AmbiguousInputError(sorted(pending))
|
|
107
|
+
|
|
108
|
+
await self._queue_input(input_text)
|
|
109
|
+
|
|
110
|
+
async def provide_input(self, interaction_id: str) -> str | None:
|
|
111
|
+
"""Return the stored input, or claim a queued one if unambiguous."""
|
|
112
|
+
provided = await self._stored_input(interaction_id)
|
|
113
|
+
if provided is not None:
|
|
114
|
+
return provided
|
|
115
|
+
|
|
116
|
+
pending = await self._pending_map()
|
|
117
|
+
if any(other_id != interaction_id for other_id in pending):
|
|
118
|
+
return None
|
|
119
|
+
|
|
120
|
+
return await self._pop_queued_input()
|
|
121
|
+
|
|
122
|
+
async def record_request(self, interaction_id: str, prompt: str) -> None:
|
|
123
|
+
pending = await self._pending_map()
|
|
124
|
+
pending[interaction_id] = {"id": interaction_id, "prompt": prompt}
|
|
125
|
+
await self._save_pending(pending)
|
|
126
|
+
|
|
127
|
+
async def complete_request(self, interaction_id: str) -> None:
|
|
128
|
+
pending = await self._pending_map()
|
|
129
|
+
pending.pop(interaction_id, None)
|
|
130
|
+
await self._save_pending(pending)
|
|
131
|
+
|
|
132
|
+
async def _pending_map(self) -> dict[str, dict]:
|
|
133
|
+
store = self._store()
|
|
134
|
+
pending = await store.get(self._pending_key, dict) or {}
|
|
135
|
+
if not pending:
|
|
136
|
+
return {}
|
|
137
|
+
|
|
138
|
+
unresolved = {}
|
|
139
|
+
for interaction_id, request in pending.items():
|
|
140
|
+
provided = await self._stored_input(interaction_id)
|
|
141
|
+
if provided is None:
|
|
142
|
+
unresolved[interaction_id] = request
|
|
143
|
+
|
|
144
|
+
await self._save_pending(unresolved)
|
|
145
|
+
return dict(unresolved)
|
|
146
|
+
|
|
147
|
+
async def _save_pending(self, pending: dict[str, dict]) -> None:
|
|
148
|
+
store = self._store()
|
|
149
|
+
if pending:
|
|
150
|
+
await store.set(self._pending_key, pending, dict)
|
|
151
|
+
return
|
|
152
|
+
|
|
153
|
+
await store.delete(self._pending_key)
|
|
154
|
+
|
|
155
|
+
async def _stored_input(self, interaction_id: str) -> str | None:
|
|
156
|
+
return await self._store().get(self._input_key(interaction_id), str)
|
|
157
|
+
|
|
158
|
+
async def _store_input(self, interaction_id: str, input_text: str) -> None:
|
|
159
|
+
await self._store().set(self._input_key(interaction_id), input_text, str)
|
|
160
|
+
|
|
161
|
+
async def _queue_input(self, input_text: str) -> None:
|
|
162
|
+
store = self._store()
|
|
163
|
+
queue = await store.get(self._queued_key, list) or []
|
|
164
|
+
queue.append(input_text)
|
|
165
|
+
await store.set(self._queued_key, queue, list)
|
|
166
|
+
|
|
167
|
+
async def _pop_queued_input(self) -> str | None:
|
|
168
|
+
store = self._store()
|
|
169
|
+
queue = await store.get(self._queued_key, list)
|
|
170
|
+
if not queue:
|
|
171
|
+
return None
|
|
172
|
+
|
|
173
|
+
next_input = queue.pop(0)
|
|
174
|
+
if queue:
|
|
175
|
+
await store.set(self._queued_key, queue, list)
|
|
176
|
+
else:
|
|
177
|
+
await store.delete(self._queued_key)
|
|
178
|
+
return next_input
|
|
179
|
+
|
|
180
|
+
def _store(self) -> KeyValueStore:
|
|
181
|
+
store = self._active_store.get()
|
|
182
|
+
if store is None:
|
|
183
|
+
raise RuntimeError("Input channel is not bound to a store.")
|
|
184
|
+
return store
|
|
185
|
+
|
|
186
|
+
@property
|
|
187
|
+
def _pending_key(self) -> str:
|
|
188
|
+
return f"{self._namespace}/pending"
|
|
189
|
+
|
|
190
|
+
@property
|
|
191
|
+
def _queued_key(self) -> str:
|
|
192
|
+
return f"{self._namespace}/queued"
|
|
193
|
+
|
|
194
|
+
def _input_key(self, interaction_id: str) -> str:
|
|
195
|
+
return f"{self._namespace}/input/{interaction_id}"
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def _to_input_text(input_value: str | list[str]) -> str:
|
|
199
|
+
if isinstance(input_value, str):
|
|
200
|
+
return input_value.strip()
|
|
201
|
+
return " ".join(input_value).strip()
|
sefia_fastapi/_kv.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from typing import Any, Protocol
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class KeyValueStore(Protocol):
|
|
5
|
+
"""Async key-value persistence required by the HTTP input state.
|
|
6
|
+
|
|
7
|
+
Structurally matches ``sefios.SessionStorage``, so a bound session storage
|
|
8
|
+
can be passed in directly; any other implementation with the same shape
|
|
9
|
+
works too.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
async def get(self, key: str, type_hint: type) -> Any | None: ...
|
|
13
|
+
|
|
14
|
+
async def set(self, key: str, value: Any, type_hint: type) -> None: ...
|
|
15
|
+
|
|
16
|
+
async def delete(self, key: str) -> None: ...
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class UnknownSessionError(Exception):
|
|
5
|
+
"""Raised when a requested HTTP session is not known."""
|
|
6
|
+
|
|
7
|
+
def __init__(self, session_id: str):
|
|
8
|
+
super().__init__(f"Unknown session: {session_id}")
|
|
9
|
+
self.session_id = session_id
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class UnknownInputError(Exception):
|
|
13
|
+
"""Raised when an input targets an unknown pending input."""
|
|
14
|
+
|
|
15
|
+
def __init__(self, interaction_id: str):
|
|
16
|
+
super().__init__(f"Unknown pending input: {interaction_id}")
|
|
17
|
+
self.interaction_id = interaction_id
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class AmbiguousInputError(Exception):
|
|
21
|
+
"""Raised when multiple pending inputs need an explicit reply target."""
|
|
22
|
+
|
|
23
|
+
def __init__(self, interaction_ids: list[str]):
|
|
24
|
+
super().__init__(
|
|
25
|
+
"Multiple pending inputs exist. Specify one with reply_to: "
|
|
26
|
+
+ ", ".join(interaction_ids)
|
|
27
|
+
)
|
|
28
|
+
self.interaction_ids = interaction_ids
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass(frozen=True)
|
|
32
|
+
class InputRequired(Exception):
|
|
33
|
+
"""Raised when a session pauses to wait for external input."""
|
|
34
|
+
|
|
35
|
+
interaction_id: str
|
|
36
|
+
prompt: str
|
|
37
|
+
|
|
38
|
+
def __str__(self) -> str:
|
|
39
|
+
return f"Input required: {self.prompt}"
|
sefia_fastapi/py.typed
ADDED
|
File without changes
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: sefia-fastapi
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: FastAPI (HTTP) building blocks for Sefia applications.
|
|
5
|
+
Project-URL: Homepage, https://github.com/nueruyu/sefia
|
|
6
|
+
Project-URL: Repository, https://github.com/nueruyu/sefia
|
|
7
|
+
Project-URL: Issues, https://github.com/nueruyu/sefia/issues
|
|
8
|
+
Project-URL: Documentation, https://github.com/nueruyu/sefia/tree/main/docs
|
|
9
|
+
Author: nueruyu
|
|
10
|
+
License: MIT License
|
|
11
|
+
|
|
12
|
+
Copyright (c) 2026 nueruyu
|
|
13
|
+
|
|
14
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
15
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
16
|
+
in the Software without restriction, including without limitation the rights
|
|
17
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
18
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
19
|
+
furnished to do so, subject to the following conditions:
|
|
20
|
+
|
|
21
|
+
The above copyright notice and this permission notice shall be included in all
|
|
22
|
+
copies or substantial portions of the Software.
|
|
23
|
+
|
|
24
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
25
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
26
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
27
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
28
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
29
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
30
|
+
SOFTWARE.
|
|
31
|
+
License-File: LICENSE
|
|
32
|
+
Keywords: fastapi,human-in-the-loop,sefia,sse
|
|
33
|
+
Classifier: Development Status :: 3 - Alpha
|
|
34
|
+
Classifier: Framework :: AsyncIO
|
|
35
|
+
Classifier: Framework :: FastAPI
|
|
36
|
+
Classifier: Intended Audience :: Developers
|
|
37
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
38
|
+
Classifier: Operating System :: OS Independent
|
|
39
|
+
Classifier: Programming Language :: Python :: 3
|
|
40
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
41
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
42
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
43
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
44
|
+
Classifier: Typing :: Typed
|
|
45
|
+
Requires-Python: >=3.11
|
|
46
|
+
Requires-Dist: fastapi>=0.110
|
|
47
|
+
Requires-Dist: sefia>=0.1.0
|
|
48
|
+
Description-Content-Type: text/markdown
|
|
49
|
+
|
|
50
|
+
# sefia-fastapi
|
|
51
|
+
|
|
52
|
+
FastAPI (HTTP) building blocks for [Sefia](https://pypi.org/project/sefia/)
|
|
53
|
+
applications.
|
|
54
|
+
|
|
55
|
+
This package holds the HTTP-side pieces that depend only on `sefia` and
|
|
56
|
+
FastAPI: the input core (an `InputChannel` persisted over a `KeyValueStore`),
|
|
57
|
+
per-session SSE streams (`SessionEvents`), and the exceptions an application
|
|
58
|
+
maps to HTTP responses. The runtime wiring — session management, persistence,
|
|
59
|
+
and the pausing tool — is provided by an integration layer such as
|
|
60
|
+
`sefios.fastapi` from [`sefios`](https://pypi.org/project/sefios/).
|
|
61
|
+
|
|
62
|
+
## Install
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
pip install sefia-fastapi
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Most applications install it through the stack instead:
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
pip install 'sefios[fastapi]'
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Documentation
|
|
75
|
+
|
|
76
|
+
See the [repository](https://github.com/nueruyu/sefia) for the full README,
|
|
77
|
+
tutorial, and architecture docs.
|
|
78
|
+
|
|
79
|
+
## Status
|
|
80
|
+
|
|
81
|
+
Early development. APIs may change before v1.0.
|
|
82
|
+
|
|
83
|
+
## License
|
|
84
|
+
|
|
85
|
+
MIT
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
sefia_fastapi/__init__.py,sha256=eelVE8u2eRvZKgppxnACfsQauyLlNlakTaYBsOzRMpI,939
|
|
2
|
+
sefia_fastapi/_events.py,sha256=uJ5jIryO_fDUFtx6hIWiDqLWbaaeLy_IvmD25dg1b5w,3485
|
|
3
|
+
sefia_fastapi/_input.py,sha256=ZZKZjQM6QNilifZ0hWZUhSnyu5t0USIfGVbJVdcwBFo,7174
|
|
4
|
+
sefia_fastapi/_kv.py,sha256=Zprg6Ag9eo-YS_NnbJh2hODsKYxGDt2zH7ZoeXWMO6A,513
|
|
5
|
+
sefia_fastapi/exceptions.py,sha256=MiCWCIzwgAxw5K8NudDjfHFa7lAXP85fEDPXcWkdcwY,1148
|
|
6
|
+
sefia_fastapi/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
7
|
+
sefia_fastapi-0.1.0.dist-info/METADATA,sha256=1l1gxoK46lbiyEoLG320jNeY15nNAUpXhF_mJjbs-bw,3272
|
|
8
|
+
sefia_fastapi-0.1.0.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
|
|
9
|
+
sefia_fastapi-0.1.0.dist-info/licenses/LICENSE,sha256=bNtLcBZk03HGyB7qD0gK37uO4E0sre_G01X1vCFBPG8,1064
|
|
10
|
+
sefia_fastapi-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 nueruyu
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|