Initial project structure
Scaffold all modules, route stubs, data models, and config. No logic implemented yet — all core methods raise NotImplementedError. Establishes the full directory layout matching the architecture in CLAUDE.md. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
0
fellowship/api/__init__.py
Normal file
0
fellowship/api/__init__.py
Normal file
83
fellowship/api/events.py
Normal file
83
fellowship/api/events.py
Normal file
@@ -0,0 +1,83 @@
|
||||
"""
|
||||
All WebSocket/SSE event types sent from Fellowship to connected members.
|
||||
Every event is a Pydantic model serialized to JSON.
|
||||
"""
|
||||
|
||||
from typing import Any, Literal
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class HistoryEvent(BaseModel):
|
||||
type: Literal["history"] = "history"
|
||||
messages: list[dict[str, Any]]
|
||||
|
||||
|
||||
class TurnStartEvent(BaseModel):
|
||||
type: Literal["turn_start"] = "turn_start"
|
||||
bot: str
|
||||
turn: int
|
||||
|
||||
|
||||
class BotMessageEvent(BaseModel):
|
||||
type: Literal["bot_message"] = "bot_message"
|
||||
bot: str
|
||||
content: str
|
||||
turn: int
|
||||
|
||||
|
||||
class TokenEvent(BaseModel):
|
||||
"""Only emitted when stream_tokens is enabled."""
|
||||
type: Literal["token"] = "token"
|
||||
bot: str
|
||||
token: str
|
||||
turn: int
|
||||
|
||||
|
||||
class TurnEndEvent(BaseModel):
|
||||
type: Literal["turn_end"] = "turn_end"
|
||||
bot: str
|
||||
turn: int
|
||||
tokens: int
|
||||
|
||||
|
||||
class TalkerMessageEvent(BaseModel):
|
||||
type: Literal["talker_message"] = "talker_message"
|
||||
talker_id: str
|
||||
talker_name: str
|
||||
content: str
|
||||
turn: int
|
||||
|
||||
|
||||
class MemberJoinedEvent(BaseModel):
|
||||
type: Literal["member_joined"] = "member_joined"
|
||||
role: Literal["talker", "observer"]
|
||||
|
||||
|
||||
class MemberLeftEvent(BaseModel):
|
||||
type: Literal["member_left"] = "member_left"
|
||||
role: Literal["talker", "observer"]
|
||||
|
||||
|
||||
class SessionPausedEvent(BaseModel):
|
||||
type: Literal["session_paused"] = "session_paused"
|
||||
|
||||
|
||||
class SessionResumedEvent(BaseModel):
|
||||
type: Literal["session_resumed"] = "session_resumed"
|
||||
|
||||
|
||||
class SessionEndEvent(BaseModel):
|
||||
type: Literal["session_end"] = "session_end"
|
||||
reason: Literal["max_turns", "max_time", "max_context", "orchestrator", "client_request"]
|
||||
|
||||
|
||||
class ErrorEvent(BaseModel):
|
||||
type: Literal["error"] = "error"
|
||||
message: str
|
||||
|
||||
|
||||
class DebugEvent(BaseModel):
|
||||
"""Only emitted when debug mode is active."""
|
||||
type: Literal["debug"] = "debug"
|
||||
category: str
|
||||
data: dict[str, Any]
|
||||
0
fellowship/api/models/__init__.py
Normal file
0
fellowship/api/models/__init__.py
Normal file
69
fellowship/api/models/session.py
Normal file
69
fellowship/api/models/session.py
Normal file
@@ -0,0 +1,69 @@
|
||||
"""
|
||||
Pydantic request and response models for the session API endpoints.
|
||||
"""
|
||||
|
||||
from typing import Optional
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from fellowship.core.session import (
|
||||
BotConfig,
|
||||
ParticipationMode,
|
||||
TurnOrder,
|
||||
SessionState,
|
||||
)
|
||||
|
||||
|
||||
class CreateSessionRequest(BaseModel):
|
||||
bots: list[BotConfig] = Field(description="List of bots in this session")
|
||||
global_system_prompt: Optional[str] = Field(
|
||||
default=None, description="System prompt injected for all bots"
|
||||
)
|
||||
goal: Optional[str] = Field(
|
||||
default=None,
|
||||
description="Natural language goal. Required for orchestrator end_session tool to be available.",
|
||||
)
|
||||
participation_mode: ParticipationMode = Field(
|
||||
default=ParticipationMode.AUTONOMOUS,
|
||||
description="How human talkers interact with the session",
|
||||
)
|
||||
turn_order: TurnOrder = Field(
|
||||
default=TurnOrder.ROUND_ROBIN,
|
||||
description="How the next bot speaker is selected",
|
||||
)
|
||||
max_talkers: int = Field(default=1, description="Maximum simultaneous talker connections")
|
||||
max_turns: Optional[int] = Field(default=None, description="End session after N bot turns")
|
||||
max_time: Optional[int] = Field(default=None, description="End session after N seconds")
|
||||
max_context_tokens: Optional[int] = Field(
|
||||
default=None, description="End or summarize when total context reaches N tokens"
|
||||
)
|
||||
rectify_history: bool = Field(default=True, description="Enable history rectification")
|
||||
summarize_context: bool = Field(
|
||||
default=False, description="Summarize old context instead of ending when limit is reached"
|
||||
)
|
||||
stream_tokens: bool = Field(default=False, description="Stream bot responses token-by-token")
|
||||
llm_base_url: Optional[str] = Field(
|
||||
default=None, description="Override LLM backend URL for this session"
|
||||
)
|
||||
llm_api_key: Optional[str] = Field(
|
||||
default=None, description="Override LLM API key for this session"
|
||||
)
|
||||
debug: Optional[bool] = Field(
|
||||
default=None, description="Override debug mode for this session"
|
||||
)
|
||||
|
||||
|
||||
class CreateSessionResponse(BaseModel):
|
||||
token: str = Field(description="Session token used for all subsequent interactions")
|
||||
state: SessionState
|
||||
bot_count: int
|
||||
|
||||
|
||||
class SessionStatusResponse(BaseModel):
|
||||
token: str
|
||||
state: SessionState
|
||||
bot_count: int
|
||||
turn_count: int
|
||||
talker_count: int
|
||||
observer_count: int
|
||||
participation_mode: ParticipationMode
|
||||
turn_order: TurnOrder
|
||||
0
fellowship/api/routes/__init__.py
Normal file
0
fellowship/api/routes/__init__.py
Normal file
94
fellowship/api/routes/sessions.py
Normal file
94
fellowship/api/routes/sessions.py
Normal file
@@ -0,0 +1,94 @@
|
||||
"""
|
||||
Session API routes. Route handlers are thin — they validate input and delegate to core/.
|
||||
All routes are mounted under /v1 in main.py.
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
from fastapi import APIRouter, HTTPException, WebSocket, WebSocketDisconnect
|
||||
from fastapi.responses import StreamingResponse
|
||||
|
||||
from fellowship.api.models.session import (
|
||||
CreateSessionRequest,
|
||||
CreateSessionResponse,
|
||||
SessionStatusResponse,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
router = APIRouter(tags=["sessions"])
|
||||
|
||||
|
||||
@router.post(
|
||||
"/session/create",
|
||||
response_model=CreateSessionResponse,
|
||||
summary="Initialize a new session",
|
||||
description="Create a new Fellowship session with the given bots and options. Returns a session token.",
|
||||
)
|
||||
async def create_session(body: CreateSessionRequest) -> CreateSessionResponse:
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
@router.get(
|
||||
"/session/{token}",
|
||||
response_model=SessionStatusResponse,
|
||||
summary="Get session status",
|
||||
description="Returns current state, turn count, and connected member counts for a session.",
|
||||
)
|
||||
async def get_session(token: str) -> SessionStatusResponse:
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
@router.delete(
|
||||
"/session/{token}",
|
||||
summary="End a session",
|
||||
description="Terminates the session loop and disconnects all members.",
|
||||
)
|
||||
async def end_session(token: str) -> dict[str, str]:
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
@router.post(
|
||||
"/session/{token}/pause",
|
||||
summary="Pause a session",
|
||||
description="Halts the session loop. Members remain connected and receive a session_paused event.",
|
||||
)
|
||||
async def pause_session(token: str) -> dict[str, str]:
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
@router.post(
|
||||
"/session/{token}/resume",
|
||||
summary="Resume a paused session",
|
||||
description="Restarts the session loop. Members receive a session_resumed event.",
|
||||
)
|
||||
async def resume_session(token: str) -> dict[str, str]:
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
@router.get(
|
||||
"/session/{token}/history",
|
||||
summary="Get full conversation history",
|
||||
description="Returns the complete ordered message log for the session.",
|
||||
)
|
||||
async def get_history(token: str) -> dict:
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
@router.websocket("/session/{token}/connect")
|
||||
async def websocket_connect(websocket: WebSocket, token: str, role: str = "observer") -> None:
|
||||
"""
|
||||
WebSocket connection for talkers (role=talker) and observers (role=observer).
|
||||
On connect: sends a history event with the full conversation, then streams live events.
|
||||
Talkers may send user_message and ping frames.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
@router.get(
|
||||
"/session/{token}/stream",
|
||||
summary="SSE observe-only stream",
|
||||
description="Server-Sent Events stream for observers. Sends history replay then live events.",
|
||||
)
|
||||
async def sse_stream(token: str) -> StreamingResponse:
|
||||
raise NotImplementedError
|
||||
Reference in New Issue
Block a user