Skip to content

Commit 18b357f

Browse files
committed
Merge branch 'post-endpoints' into release-candidate
2 parents 1c17f17 + 2e0d053 commit 18b357f

20 files changed

Lines changed: 247 additions & 90 deletions

README.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -88,17 +88,17 @@ Generated artifacts that persist across runs:
8888
| `GET` | `/api/chatbot/help` | Help payload stub | Placeholder |
8989
| `GET` | `/api/chatbot/availablechatbots` | Returns model names from `litellm_config.yaml` | Requires auth |
9090
| `GET` | `/api/chatbot/newthread` | Generates a fresh `thread_id` | Requires auth |
91-
| `GET` | `/api/chatbot/getthread?thread_id=...` | Fetches thread contents omitting prompts + redundant StreamEnd variants | Requires auth |
92-
| `GET` | `/api/chatbot/getuserthreads` | Returns latest 10 threads for authenticated user | Falls back to query `user_id` only if `ALLOW_FALLBACK_OLD_AUTH` |
93-
| `GET` | `/api/chatbot/streamresponse` | Starts an SSE stream of `StreamVariant` JSON payloads | Query params: `thread_id`, `input` (required), `chatbot` |
94-
| `GET/POST` | `/api/chatbot/stop` | Initiates stopping of an active conversation | Requires auth |
91+
| `POST` | `/api/chatbot/getthread` | Fetches thread contents omitting prompts + redundant StreamEnd variants | Requires auth |
92+
| `POST` | `/api/chatbot/getuserthreads` | Returns recent threads for authenticated user | JSON body: `num_threads`, `page` |
93+
| `POST` | `/api/chatbot/streamresponse` | Starts an SSE stream of `StreamVariant` JSON payloads | Query params: `thread_id`, `input` (required), `chatbot` |
94+
| `POST` | `/api/chatbot/stop` | Initiates stopping of an active conversation | JSON body: `thread_id`; requires auth |
9595

9696
### Streaming contract
9797
- Response type: `application/x-ndjson`
9898
- Each `data:` line is a JSON object with `variant` discriminators (`Assistant`, `Code`, `CodeOutput`, `CodeError`, `Image`, `ServerHint`, `StreamEnd`, etc.).
9999
- Code tool calls stream incremental chunks while LiteLLM emits `tool_calls`. When the MCP tool resolves, results are converted back into JSON events and appended to Mongo/disk storage.
100100
- The first chunk is a `ServerHint` carrying the `thread_id`; conversation variants are stored in-memory during streaming and flushed to MongoDB at the end, ensuring replay safety.
101-
- Clients can call `/api/chatbot/stop?thread_id=...` to move a conversation into `STOPPING`; the streaming loop exits and cancels in-flight MCP requests (code, rag, web-search) via the shared `ActiveRequest` registry.
101+
- Clients can call `POST /api/chatbot/stop` with `{"thread_id": "..."}` to move a conversation into `STOPPING`; the streaming loop exits and cancels in-flight MCP requests (code, rag, web-search) via the shared `ActiveRequest` registry.
102102

103103
## Persistence, Prompts, and Assets
104104
- **MongoDB (`mongodb_storage.py`)**: canonical record for threads. Each document stores `user_id`, `thread_id`, ISO timestamp, topic (summarized via LiteLLM), and serialized `StreamVariant` list.

src/climateclaw/api/chatbot/deletethread.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from __future__ import annotations
22

33
from fastapi import APIRouter, Depends, HTTPException
4+
from pydantic import BaseModel
45

56
from climateclaw.core.logging_setup import configure_logging
67
from climateclaw.services.service_factory import (
@@ -14,9 +15,13 @@
1415
router = APIRouter()
1516

1617

17-
@router.get("/deletethread", dependencies=[AuthRequired])
18+
class DeleteThreadRequest(BaseModel):
19+
thread_id: str
20+
21+
22+
@router.post("/deletethread", dependencies=[AuthRequired])
1823
async def delete_thread(
19-
thread_id: str,
24+
request: DeleteThreadRequest,
2025
auth: Authenticator = Depends(auth_dependency),
2126
storage: ThreadStorage = Depends(get_thread_storage),
2227
):
@@ -47,6 +52,9 @@ async def delete_thread(
4752
HTTPException (500):
4853
- If deletion fails due to an internal storage error.
4954
"""
55+
56+
thread_id = request.thread_id
57+
5058
logger = configure_logging(__name__, thread_id=thread_id, user_id=auth.username)
5159

5260
if not thread_id:

src/climateclaw/api/chatbot/editthread.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from fastapi import APIRouter, Depends, HTTPException
2+
from pydantic import BaseModel
23

34
from climateclaw.core.logging_setup import configure_logging
45
from climateclaw.services.service_factory import (
@@ -22,10 +23,14 @@
2223
router = APIRouter()
2324

2425

25-
@router.get("/editthread", dependencies=[AuthRequired])
26+
class EditThreadRequest(BaseModel):
27+
source_thread_id: str
28+
user_index: int
29+
30+
31+
@router.post("/editthread", dependencies=[AuthRequired])
2632
async def edit_thread(
27-
source_thread_id: str,
28-
user_index: int,
33+
request: EditThreadRequest,
2934
auth: Authenticator = Depends(auth_dependency),
3035
storage: ThreadStorage = Depends(get_thread_storage),
3136
):
@@ -80,6 +85,9 @@ async def edit_thread(
8085
`source_thread_id`. If deep branching is introduced, root tracking
8186
logic may require refinement.
8287
"""
88+
89+
source_thread_id = request.source_thread_id
90+
user_index = request.user_index
8391
user_name = auth.username
8492

8593
if not source_thread_id:

src/climateclaw/api/chatbot/getthread.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from __future__ import annotations
22

3-
from fastapi import APIRouter, Depends, HTTPException, Query
3+
from fastapi import APIRouter, Depends, HTTPException
4+
from pydantic import BaseModel
45

56
from climateclaw.core.logging_setup import configure_logging
67
from climateclaw.services.service_factory import (
@@ -22,6 +23,10 @@
2223
router = APIRouter()
2324

2425

26+
class GetThreadRequest(BaseModel):
27+
thread_id: str | None = None
28+
29+
2530
def _post_process(variants: list[StreamVariant]) -> list[SVDict]:
2631
"""Remove Prompt variants before returning, drop any StreamEnd except the final one, and drop 'unexpected manner' ones anywhere."""
2732
items = [item for item in variants if not is_prompt(item)]
@@ -35,9 +40,9 @@ def _post_process(variants: list[StreamVariant]) -> list[SVDict]:
3540
return cleaned
3641

3742

38-
@router.get("/getthread", dependencies=[AuthRequired])
43+
@router.post("/getthread", dependencies=[AuthRequired])
3944
async def get_thread(
40-
thread_id: str | None = Query(None),
45+
request: GetThreadRequest,
4146
auth: Authenticator = Depends(auth_dependency),
4247
storage: ThreadStorage = Depends(get_thread_storage),
4348
):
@@ -73,6 +78,8 @@ async def get_thread(
7378
- If an error occurs while reading or processing the thread.
7479
"""
7580

81+
thread_id = request.thread_id
82+
7683
if not thread_id:
7784
raise HTTPException(
7885
status_code=422,

src/climateclaw/api/chatbot/getuserthreads.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from __future__ import annotations
22

33
from fastapi import APIRouter, Depends, HTTPException
4+
from pydantic import BaseModel
45

56
from climateclaw.core.logging_setup import configure_logging
67
from climateclaw.services.service_factory import (
@@ -14,10 +15,14 @@
1415
router = APIRouter()
1516

1617

17-
@router.get("/getuserthreads", dependencies=[AuthRequired])
18+
class GetUserThreadsRequest(BaseModel):
19+
num_threads: int = 20
20+
page: int = 0
21+
22+
23+
@router.post("/getuserthreads", dependencies=[AuthRequired])
1824
async def get_user_threads(
19-
num_threads: int = 20,
20-
page: int = 0,
25+
request: GetUserThreadsRequest,
2126
auth: Authenticator = Depends(auth_dependency),
2227
storage: ThreadStorage = Depends(get_thread_storage),
2328
):
@@ -58,6 +63,9 @@ async def get_user_threads(
5863
HTTPException (500):
5964
- If fetching the user's thread history fails.
6065
"""
66+
num_threads = request.num_threads
67+
page = request.page
68+
6169
logger = configure_logging(__name__, user_id=auth.username)
6270

6371
if not auth.username:

src/climateclaw/api/chatbot/searchthreads.py

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from __future__ import annotations
22

33
from fastapi import APIRouter, Depends, HTTPException
4+
from pydantic import BaseModel
45

56
from climateclaw.core.logging_setup import configure_logging
67
from climateclaw.services.service_factory import (
@@ -14,11 +15,15 @@
1415
router = APIRouter()
1516

1617

17-
@router.get("/searchthreads", dependencies=[AuthRequired])
18+
class SearchThreadRequest(BaseModel):
19+
query: str
20+
page: int = 0
21+
num_threads: int = 20
22+
23+
24+
@router.post("/searchthreads", dependencies=[AuthRequired])
1825
async def search_threads(
19-
query: str,
20-
page: int = 0,
21-
num_threads: int = 20,
26+
request: SearchThreadRequest,
2227
auth: Authenticator = Depends(auth_dependency),
2328
storage: ThreadStorage = Depends(get_thread_storage),
2429
):
@@ -61,6 +66,11 @@ async def search_threads(
6166
HTTPException (500):
6267
- If querying threads fails due to an internal error.
6368
"""
69+
70+
query = request.query
71+
page = request.page
72+
num_threads = request.num_threads
73+
6474
logger = configure_logging(__name__, user_id=auth.username)
6575

6676
if not auth.username:

src/climateclaw/api/chatbot/setthreadtopic.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from __future__ import annotations
22

33
from fastapi import APIRouter, Depends, HTTPException
4+
from pydantic import BaseModel
45

56
from climateclaw.core.logging_setup import configure_logging
67
from climateclaw.services.service_factory import (
@@ -14,10 +15,14 @@
1415
router = APIRouter()
1516

1617

17-
@router.get("/setthreadtopic", dependencies=[AuthRequired])
18+
class SetThreadTopicRequest(BaseModel):
19+
thread_id: str
20+
topic: str
21+
22+
23+
@router.post("/setthreadtopic", dependencies=[AuthRequired])
1824
async def set_thread_topic(
19-
thread_id: str,
20-
topic: str,
25+
request: SetThreadTopicRequest,
2126
auth: Authenticator = Depends(auth_dependency),
2227
storage: ThreadStorage = Depends(get_thread_storage),
2328
):
@@ -51,6 +56,10 @@ async def set_thread_topic(
5156
HTTPException (500):
5257
- If updating the thread topic fails due to an internal error.
5358
"""
59+
60+
thread_id = request.thread_id
61+
topic = request.topic
62+
5463
if not thread_id:
5564
raise HTTPException(
5665
status_code=422,

src/climateclaw/api/chatbot/stop.py

Lines changed: 21 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
from fastapi import APIRouter, HTTPException, Query
1+
from fastapi import APIRouter, HTTPException
2+
from pydantic import BaseModel
23
from starlette.status import HTTP_422_UNPROCESSABLE_CONTENT
34

45
from climateclaw.core.logging_setup import configure_logging
@@ -8,11 +9,13 @@
89
router = APIRouter()
910

1011

11-
@router.get("/stop", dependencies=[AuthRequired])
12-
async def stop_get(
13-
thread_id: str | None = Query(
14-
default=None, description="Thread to stop (optional)"
15-
),
12+
class StopRequest(BaseModel):
13+
thread_id: str | None = None
14+
15+
16+
@router.post("/stop", dependencies=[AuthRequired])
17+
async def stop(
18+
request: StopRequest,
1619
):
1720
"""
1821
Stop Active Conversation Streaming.
@@ -24,7 +27,7 @@ async def stop_get(
2427
Parameters:
2528
thread_id (str | None):
2629
The unique identifier of the thread whose streaming process
27-
should be stopped. Must be provided as a query parameter.
30+
should be stopped. Must be provided in the request body.
2831
2932
Returns:
3033
dict:
@@ -39,11 +42,12 @@ async def stop_get(
3942
HTTPException (500):
4043
- Failure to request stop.
4144
"""
45+
thread_id = request.thread_id
4246

4347
if not thread_id:
4448
raise HTTPException(
4549
status_code=HTTP_422_UNPROCESSABLE_CONTENT,
46-
detail="Thread ID is missing. Please provide a thread_id in the query parameters.",
50+
detail="Thread ID is missing. Please provide a thread_id in the request body.",
4751
)
4852

4953
logger = configure_logging(__name__, thread_id=thread_id)
@@ -53,14 +57,15 @@ async def stop_get(
5357
if ok:
5458
logger.debug("Initiated stop request", extra={"thread_id": thread_id})
5559
return {"detail": "Conversation stopped."}
56-
else:
57-
logger.exception(
58-
f"Thread not found in the registry. Nothing to stop: {thread_id}"
59-
)
60-
raise HTTPException(
61-
status_code=404,
62-
detail=f"Conversation with given thread-id not found in the registry: {thread_id}",
63-
)
60+
logger.warning(
61+
f"Thread not found in the registry. Nothing to stop: {thread_id}"
62+
)
63+
raise HTTPException(
64+
status_code=404,
65+
detail=f"Conversation with given thread-id not found in the registry: {thread_id}",
66+
)
67+
except HTTPException:
68+
raise
6469
except Exception as e:
6570
logger.exception(f"Failed to stop the thread {thread_id}: {e}")
6671
raise HTTPException(

src/climateclaw/api/chatbot/streamresponse.py

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44
import time
55
from collections.abc import Generator
66

7-
from fastapi import APIRouter, Depends, HTTPException, Query
7+
from fastapi import APIRouter, Depends, HTTPException
8+
from pydantic import BaseModel
89
from starlette.responses import StreamingResponse
910

1011
from climateclaw.core.available_chatbots import (
@@ -49,6 +50,13 @@
4950
CHECK_INTERVAL = 3 # seconds, the interval to wait before check STOP request
5051

5152

53+
class StreamResponseRequest(BaseModel):
54+
thread_id: str | None = None
55+
input: str | None = None
56+
chatbot: str | None = None
57+
store_thread: bool = True
58+
59+
5260
def _sse_data(obj: SVDict) -> Generator[bytes]:
5361
if obj.get("variant") == IMAGE:
5462
image_b64 = obj.get("content")
@@ -64,12 +72,9 @@ def _sse_data(obj: SVDict) -> Generator[bytes]:
6472
yield f"{payload}\n".encode()
6573

6674

67-
@router.get("/streamresponse", dependencies=[AuthRequired])
75+
@router.post("/streamresponse", dependencies=[AuthRequired])
6876
async def streamresponse(
69-
thread_id: str | None = Query(None),
70-
input: str | None = Query(None),
71-
chatbot: str | None = Query(None),
72-
store_thread: bool = True,
77+
request: StreamResponseRequest,
7378
auth: Authenticator = Depends(auth_dependency),
7479
storage: ThreadStorage = Depends(get_thread_storage),
7580
):
@@ -124,6 +129,12 @@ async def streamresponse(
124129
- If stream preparation fails or an internal server error occurs
125130
before streaming begins.
126131
"""
132+
133+
thread_id = request.thread_id
134+
input = request.input
135+
chatbot = request.chatbot
136+
store_thread = request.store_thread
137+
127138
logger = configure_logging(__name__)
128139

129140
if not thread_id:

0 commit comments

Comments
 (0)