Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,13 @@

All notable changes to this project will be documented in this file.

## [0.1.1] - 2025-12-02

### Fixed

- `TranscriptItem.timestamp` now accepts integer Unix timestamps (milliseconds) from LayerCode API
- Fixed loguru logging to use f-string interpolation instead of invalid `%s` syntax

## [0.1.0] - 2025-12-02

### Added
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "layercode-create-app"
version = "0.1.0"
version = "0.1.1"
description = "Fast scaffolding for LayerCode-ready FastAPI backends"
readme = "README.md"
requires-python = ">=3.12"
Expand Down
2 changes: 1 addition & 1 deletion src/layercode_create_app/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ async def run_server() -> None:
try:
await launcher.start()
except RuntimeError as exc:
logger.error("Tunnel error: %s", exc)
logger.error(f"Tunnel error: {exc}")
server.should_exit = True
raise
await server_task
Expand Down
2 changes: 1 addition & 1 deletion src/layercode_create_app/sdk/events.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ class TranscriptItem(BaseModel):

role: str
text: str
timestamp: str | None = None
timestamp: int | str | None = None # Unix ms (int) or ISO string

model_config = ConfigDict(extra="allow")

Expand Down
35 changes: 12 additions & 23 deletions src/layercode_create_app/server/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ def create_app(settings: AppSettings, agent: BaseLayercodeAgent) -> FastAPI:
async def lifespan(app: FastAPI) -> AsyncIterator[None]:
client = httpx.AsyncClient(timeout=httpx.Timeout(15.0, connect=5.0))
app.state.http_client = client
logger.info("FastAPI application starting on %s:%s", settings.host, settings.port)
logger.info(f"FastAPI application starting on {settings.host}:{settings.port}")
yield
await client.aclose()
logger.info("FastAPI application shutdown")
Expand Down Expand Up @@ -94,13 +94,13 @@ async def authorize_endpoint(request: Request) -> Response:
)
response.raise_for_status()
except httpx.HTTPStatusError as exc:
logger.error("LayerCode API error %s", exc.response.status_code)
logger.error(f"LayerCode API error {exc.response.status_code}")
raise HTTPException(
status_code=exc.response.status_code,
detail=exc.response.text or exc.response.reason_phrase,
) from exc
except httpx.RequestError as exc:
logger.error("Failed to reach LayerCode API: %s", exc)
logger.error(f"Failed to reach LayerCode API: {exc}")
raise HTTPException(status.HTTP_502_BAD_GATEWAY, "LayerCode API unreachable") from exc

logger.info("Session authorized for agent")
Expand All @@ -125,7 +125,7 @@ async def agent_webhook(request: Request) -> Response:
try:
verify_signature(body_str, signature, secret)
except InvalidSignatureError as exc:
logger.warning("Invalid signature: %s", exc)
logger.warning(f"Invalid signature: {exc}")
raise HTTPException(status.HTTP_401_UNAUTHORIZED, str(exc)) from exc

try:
Expand All @@ -136,14 +136,10 @@ async def agent_webhook(request: Request) -> Response:
try:
payload = parse_webhook_payload(payload_dict)
except Exception as exc:
logger.error("Payload validation failed: %s", exc)
logger.error(f"Payload validation failed: {exc}")
raise HTTPException(status.HTTP_400_BAD_REQUEST, "Invalid payload schema") from exc

logger.info(
"Webhook received: type=%s conversation=%s",
payload.type,
payload.conversation_id,
)
logger.info(f"Webhook received: type={payload.type} conversation={payload.conversation_id}")

await conversation_store.acquire_lock(payload.conversation_id)
try:
Expand Down Expand Up @@ -208,11 +204,8 @@ async def _handle_data(
payload: DataPayload,
agent: BaseLayercodeAgent,
) -> Response:
logger.info(
"Data event received: session=%s keys=%s",
payload.session_id,
list(payload.data.keys()) if payload.data else [],
)
data_keys = list(payload.data.keys()) if payload.data else []
logger.info(f"Data event received: session={payload.session_id} keys={data_keys}")
response_data = await agent.handle_data(payload)
return JSONResponse(response_data or {"status": "ok"})

Expand All @@ -222,10 +215,8 @@ async def _handle_session_update(
agent: BaseLayercodeAgent,
) -> Response:
logger.info(
"Session update: session=%s recording_status=%s duration=%s",
payload.session_id,
payload.recording_status,
payload.recording_duration,
f"Session update: session={payload.session_id} "
f"recording_status={payload.recording_status} duration={payload.recording_duration}"
)
await agent.handle_session_update(payload)
return JSONResponse({"status": "ok"})
Expand All @@ -237,10 +228,8 @@ async def _handle_session_end(
) -> Response:
transcript_count = len(payload.transcript) if payload.transcript else 0
logger.info(
"Session ended: session=%s duration=%sms transcript_items=%d",
payload.session_id,
payload.duration,
transcript_count,
f"Session ended: session={payload.session_id} "
f"duration={payload.duration}ms transcript_items={transcript_count}"
)
await agent.handle_session_end(payload)
return JSONResponse({"status": "ok"})
18 changes: 18 additions & 0 deletions tests/test_events.py
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,24 @@ def test_session_end_transcript_with_extra_fields() -> None:
assert item.text == "Hello"


def test_session_end_transcript_integer_timestamps() -> None:
"""TranscriptItem accepts integer timestamps (Unix ms) from LayerCode API."""
payload = SessionEndPayload.model_validate(
{
"type": "session.end",
"session_id": "sess_123",
"conversation_id": "conv_456",
"transcript": [
{"role": "assistant", "text": "Hello!", "timestamp": 1764715092729},
{"role": "user", "text": "Hi", "timestamp": 1764715102963},
],
}
)
assert payload.transcript is not None
assert payload.transcript[0].timestamp == 1764715092729
assert payload.transcript[1].timestamp == 1764715102963


# =============================================================================
# Union Parsing Tests (parse_webhook_payload)
# =============================================================================
Expand Down
2 changes: 1 addition & 1 deletion uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading