interloper-api 0.103.0__tar.gz → 0.105.0__tar.gz

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.
Files changed (32) hide show
  1. {interloper_api-0.103.0 → interloper_api-0.105.0}/PKG-INFO +1 -1
  2. {interloper_api-0.103.0 → interloper_api-0.105.0}/pyproject.toml +1 -1
  3. {interloper_api-0.103.0 → interloper_api-0.105.0}/pyproject.toml.orig +1 -1
  4. {interloper_api-0.103.0 → interloper_api-0.105.0}/src/interloper_api/routes/agent.py +86 -9
  5. {interloper_api-0.103.0 → interloper_api-0.105.0}/README.md +0 -0
  6. {interloper_api-0.103.0 → interloper_api-0.105.0}/src/interloper_api/__init__.py +0 -0
  7. {interloper_api-0.103.0 → interloper_api-0.105.0}/src/interloper_api/app.py +0 -0
  8. {interloper_api-0.103.0 → interloper_api-0.105.0}/src/interloper_api/dependencies/__init__.py +0 -0
  9. {interloper_api-0.103.0 → interloper_api-0.105.0}/src/interloper_api/dependencies/auth.py +0 -0
  10. {interloper_api-0.103.0 → interloper_api-0.105.0}/src/interloper_api/dependencies/rbac.py +0 -0
  11. {interloper_api-0.103.0 → interloper_api-0.105.0}/src/interloper_api/dependencies/state.py +0 -0
  12. {interloper_api-0.103.0 → interloper_api-0.105.0}/src/interloper_api/notifications/__init__.py +0 -0
  13. {interloper_api-0.103.0 → interloper_api-0.105.0}/src/interloper_api/notifications/base.py +0 -0
  14. {interloper_api-0.103.0 → interloper_api-0.105.0}/src/interloper_api/notifications/invitations.py +0 -0
  15. {interloper_api-0.103.0 → interloper_api-0.105.0}/src/interloper_api/notifications/super_admins.py +0 -0
  16. {interloper_api-0.103.0 → interloper_api-0.105.0}/src/interloper_api/routes/__init__.py +0 -0
  17. {interloper_api-0.103.0 → interloper_api-0.105.0}/src/interloper_api/routes/admin.py +0 -0
  18. {interloper_api-0.103.0 → interloper_api-0.105.0}/src/interloper_api/routes/auth.py +0 -0
  19. {interloper_api-0.103.0 → interloper_api-0.105.0}/src/interloper_api/routes/backfills.py +0 -0
  20. {interloper_api-0.103.0 → interloper_api-0.105.0}/src/interloper_api/routes/catalog.py +0 -0
  21. {interloper_api-0.103.0 → interloper_api-0.105.0}/src/interloper_api/routes/components.py +0 -0
  22. {interloper_api-0.103.0 → interloper_api-0.105.0}/src/interloper_api/routes/executions.py +0 -0
  23. {interloper_api-0.103.0 → interloper_api-0.105.0}/src/interloper_api/routes/health.py +0 -0
  24. {interloper_api-0.103.0 → interloper_api-0.105.0}/src/interloper_api/routes/oauth.py +0 -0
  25. {interloper_api-0.103.0 → interloper_api-0.105.0}/src/interloper_api/routes/organisations.py +0 -0
  26. {interloper_api-0.103.0 → interloper_api-0.105.0}/src/interloper_api/routes/overview.py +0 -0
  27. {interloper_api-0.103.0 → interloper_api-0.105.0}/src/interloper_api/routes/relations.py +0 -0
  28. {interloper_api-0.103.0 → interloper_api-0.105.0}/src/interloper_api/routes/runs.py +0 -0
  29. {interloper_api-0.103.0 → interloper_api-0.105.0}/src/interloper_api/routes/tokens.py +0 -0
  30. {interloper_api-0.103.0 → interloper_api-0.105.0}/src/interloper_api/routes/websocket.py +0 -0
  31. {interloper_api-0.103.0 → interloper_api-0.105.0}/src/interloper_api/utils/__init__.py +0 -0
  32. {interloper_api-0.103.0 → interloper_api-0.105.0}/src/interloper_api/utils/time.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: interloper-api
3
- Version: 0.103.0
3
+ Version: 0.105.0
4
4
  Summary: Interloper FastAPI routes
5
5
  Author: Guillaume Onfroy
6
6
  Author-email: Guillaume Onfroy <guillaume@digitlcloud.com>
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "interloper-api"
3
- version = "0.103.0"
3
+ version = "0.105.0"
4
4
  description = "Interloper FastAPI routes"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
@@ -3,7 +3,7 @@
3
3
  # ###############
4
4
  [project]
5
5
  name = "interloper-api"
6
- version = "0.103.0"
6
+ version = "0.105.0"
7
7
  description = "Interloper FastAPI routes"
8
8
  readme = "README.md"
9
9
  authors = [{ name = "Guillaume Onfroy", email = "guillaume@digitlcloud.com" }]
@@ -3,9 +3,9 @@
3
3
  A conversation is a member's own: its history lives in the store and the
4
4
  server, not the client, is its source of truth. Each turn is one
5
5
  :meth:`~pydantic_ai.ui.vercel_ai.VercelAIAdapter.dispatch_request`: the
6
- client's request carries the new message (and any tool approvals or answers
7
- the app collected), the stored history is passed as ``message_history``, and
8
- the whole history is saved back when the run completes.
6
+ stored history is passed as ``message_history``, the client's request adds
7
+ the new prompt (or the tool approvals and answers the app collected), and the
8
+ whole history is saved back when the run completes.
9
9
 
10
10
  Available when ``interloper-agent`` is installed.
11
11
  """
@@ -13,7 +13,8 @@ Available when ``interloper-agent`` is installed.
13
13
  from __future__ import annotations
14
14
 
15
15
  import logging
16
- from collections.abc import AsyncIterator
16
+ from collections.abc import AsyncIterator, Sequence
17
+ from dataclasses import dataclass
17
18
  from datetime import datetime
18
19
  from typing import Annotated, Any
19
20
  from uuid import UUID
@@ -24,8 +25,17 @@ from interloper_db import Page, PageQuery
24
25
  from interloper_db.models import Conversation
25
26
  from interloper_toolkit import ToolkitContext
26
27
  from pydantic import BaseModel
28
+ from pydantic_ai import DeferredToolResults
27
29
  from pydantic_ai.agent import AgentRunResult
28
- from pydantic_ai.messages import ModelMessage, ModelMessagesTypeAdapter, ModelRequest, UserPromptPart
30
+ from pydantic_ai.messages import (
31
+ ModelMessage,
32
+ ModelMessagesTypeAdapter,
33
+ ModelRequest,
34
+ RetryPromptPart,
35
+ ToolCallPart,
36
+ ToolReturnPart,
37
+ UserPromptPart,
38
+ )
29
39
  from pydantic_ai.ui import UIEventStream
30
40
  from pydantic_ai.ui.vercel_ai import VercelAIAdapter, VercelAIEventStream
31
41
  from pydantic_ai.ui.vercel_ai.request_types import RequestData, UIMessage
@@ -62,8 +72,73 @@ class LoggedEventStream(VercelAIEventStream[ToolkitContext, Any]):
62
72
  yield chunk
63
73
 
64
74
 
65
- class LoggedAdapter(VercelAIAdapter[ToolkitContext, Any]):
66
- """The Vercel adapter, building :class:`LoggedEventStream`."""
75
+ @dataclass
76
+ class ConversationAdapter(VercelAIAdapter[ToolkitContext, Any]):
77
+ """The Vercel adapter for a stored conversation.
78
+
79
+ The app sends its whole message list on every turn, while the stored
80
+ ``history`` already holds all of it but what the turn adds. Taking the
81
+ client's copies too would store every earlier message again, so only the
82
+ additions are kept: the final message's prompt, and the answers to calls
83
+ the history leaves pending. Approvals reach the run as deferred results.
84
+ It also builds :class:`LoggedEventStream`.
85
+ """
86
+
87
+ history: Sequence[ModelMessage] = ()
88
+
89
+ def sanitize_messages(
90
+ self, messages: Sequence[ModelMessage], *, deferred_tool_results: DeferredToolResults | None = None
91
+ ) -> list[ModelMessage]:
92
+ """Keep what the client's messages add to the stored history, sanitized.
93
+
94
+ Args:
95
+ messages: The client's messages, loaded from the request.
96
+ deferred_tool_results: The approvals the request carries, if any.
97
+
98
+ Returns:
99
+ One request holding the new answers and prompt, or nothing when the
100
+ client added neither.
101
+ """
102
+ additions = self._additions(messages)
103
+ return super().sanitize_messages(additions, deferred_tool_results=deferred_tool_results)
104
+
105
+ def _additions(self, messages: Sequence[ModelMessage]) -> list[ModelMessage]:
106
+ """The client's answers to pending calls, then its new prompt.
107
+
108
+ Args:
109
+ messages: The client's messages, loaded from the request.
110
+
111
+ Returns:
112
+ One request holding the additions, or an empty list.
113
+ """
114
+ called = {part.tool_call_id for part in self._parts(self.history, ToolCallPart)}
115
+ answered = {part.tool_call_id for part in self._parts(self.history, (ToolReturnPart, RetryPromptPart))}
116
+ answers = [
117
+ part
118
+ for part in self._parts(messages, (ToolReturnPart, RetryPromptPart))
119
+ if part.tool_call_id in called - answered
120
+ ]
121
+ final = messages[-1] if messages else None
122
+ prompts = (
123
+ [part for part in final.parts if isinstance(part, UserPromptPart)]
124
+ if isinstance(final, ModelRequest)
125
+ else []
126
+ )
127
+ parts = [*answers, *prompts]
128
+ return [ModelRequest(parts=parts)] if parts else []
129
+
130
+ @staticmethod
131
+ def _parts(messages: Sequence[ModelMessage], part_type: type | tuple[type, ...]) -> list[Any]:
132
+ """The parts of a type, across the messages.
133
+
134
+ Args:
135
+ messages: The messages to read.
136
+ part_type: The part class, or classes, to collect.
137
+
138
+ Returns:
139
+ The matching parts, in order.
140
+ """
141
+ return [part for message in messages for part in message.parts if isinstance(part, part_type)]
67
142
 
68
143
  def build_event_stream(self) -> UIEventStream[RequestData, BaseChunk, ToolkitContext, Any]:
69
144
  """Build the event stream that also logs failures.
@@ -253,12 +328,14 @@ async def chat(
253
328
  len(messages),
254
329
  )
255
330
 
256
- return await LoggedAdapter.dispatch_request(
331
+ history = ModelMessagesTypeAdapter.validate_python(conversation.messages)
332
+ return await ConversationAdapter.dispatch_request(
257
333
  request,
258
334
  agent=agent,
259
335
  sdk_version=SDK_VERSION,
260
336
  deps=context,
261
- message_history=ModelMessagesTypeAdapter.validate_python(conversation.messages),
337
+ history=history,
338
+ message_history=history,
262
339
  conversation_id=str(conversation.id),
263
340
  usage_limits=TURN_LIMITS,
264
341
  on_complete=save,