livekit-plugins-anthropic 1.0.22__py3-none-any.whl → 1.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.
@@ -17,7 +17,7 @@ from __future__ import annotations
17
17
  import os
18
18
  from collections.abc import Awaitable
19
19
  from dataclasses import dataclass
20
- from typing import Any, Literal
20
+ from typing import Any, Literal, cast
21
21
 
22
22
  import httpx
23
23
 
@@ -25,7 +25,7 @@ import anthropic
25
25
  from livekit.agents import APIConnectionError, APIStatusError, APITimeoutError, llm
26
26
  from livekit.agents.llm import ToolChoice
27
27
  from livekit.agents.llm.chat_context import ChatContext
28
- from livekit.agents.llm.tool_context import FunctionTool
28
+ from livekit.agents.llm.tool_context import FunctionTool, RawFunctionTool
29
29
  from livekit.agents.types import (
30
30
  DEFAULT_API_CONNECT_OPTIONS,
31
31
  NOT_GIVEN,
@@ -35,7 +35,7 @@ from livekit.agents.types import (
35
35
  from livekit.agents.utils import is_given
36
36
 
37
37
  from .models import ChatModels
38
- from .utils import to_chat_ctx, to_fnc_ctx
38
+ from .utils import CACHE_CONTROL_EPHEMERAL, to_fnc_ctx
39
39
 
40
40
 
41
41
  @dataclass
@@ -118,7 +118,7 @@ class LLM(llm.LLM):
118
118
  self,
119
119
  *,
120
120
  chat_ctx: ChatContext,
121
- tools: list[FunctionTool] | None = None,
121
+ tools: list[FunctionTool | RawFunctionTool] | None = None,
122
122
  conn_options: APIConnectOptions = DEFAULT_API_CONNECT_OPTIONS,
123
123
  parallel_tool_calls: NotGivenOr[bool] = NOT_GIVEN,
124
124
  tool_choice: NotGivenOr[ToolChoice] = NOT_GIVEN,
@@ -141,8 +141,10 @@ class LLM(llm.LLM):
141
141
  extra["max_tokens"] = self._opts.max_tokens if is_given(self._opts.max_tokens) else 1024
142
142
 
143
143
  if tools:
144
- extra["tools"] = to_fnc_ctx(tools, self._opts.caching)
145
- tool_choice = tool_choice if is_given(tool_choice) else self._opts.tool_choice
144
+ extra["tools"] = to_fnc_ctx(tools, self._opts.caching or None)
145
+ tool_choice = (
146
+ cast(ToolChoice, tool_choice) if is_given(tool_choice) else self._opts.tool_choice
147
+ )
146
148
  if is_given(tool_choice):
147
149
  anthropic_tool_choice: dict[str, Any] | None = {"type": "auto"}
148
150
  if isinstance(tool_choice, dict) and tool_choice.get("type") == "function":
@@ -166,15 +168,38 @@ class LLM(llm.LLM):
166
168
  anthropic_tool_choice["disable_parallel_tool_use"] = not parallel_tool_calls
167
169
  extra["tool_choice"] = anthropic_tool_choice
168
170
 
169
- anthropic_ctx, system_message = to_chat_ctx(chat_ctx, id(self), caching=self._opts.caching)
170
-
171
- if system_message:
172
- extra["system"] = [system_message]
171
+ anthropic_ctx, extra_data = chat_ctx.to_provider_format(format="anthropic")
172
+ messages = cast(list[anthropic.types.MessageParam], anthropic_ctx)
173
+ if extra_data.system_messages:
174
+ extra["system"] = [
175
+ anthropic.types.TextBlockParam(text=content, type="text")
176
+ for content in extra_data.system_messages
177
+ ]
178
+
179
+ # add cache control
180
+ if self._opts.caching == "ephemeral":
181
+ if extra.get("system"):
182
+ extra["system"][-1]["cache_control"] = CACHE_CONTROL_EPHEMERAL
183
+
184
+ seen_assistant = False
185
+ for msg in reversed(messages):
186
+ if (
187
+ msg["role"] == "assistant"
188
+ and (content := msg["content"])
189
+ and not seen_assistant
190
+ ):
191
+ content[-1]["cache_control"] = CACHE_CONTROL_EPHEMERAL # type: ignore
192
+ seen_assistant = True
193
+
194
+ elif msg["role"] == "user" and (content := msg["content"]) and seen_assistant:
195
+ content[-1]["cache_control"] = CACHE_CONTROL_EPHEMERAL # type: ignore
196
+ break
173
197
 
174
198
  stream = self._client.messages.create(
175
- messages=anthropic_ctx,
199
+ messages=messages,
176
200
  model=self._opts.model,
177
201
  stream=True,
202
+ timeout=conn_options.timeout,
178
203
  **extra,
179
204
  )
180
205
 
@@ -194,7 +219,7 @@ class LLMStream(llm.LLMStream):
194
219
  *,
195
220
  anthropic_stream: Awaitable[anthropic.AsyncStream[anthropic.types.RawMessageStreamEvent]],
196
221
  chat_ctx: llm.ChatContext,
197
- tools: list[FunctionTool],
222
+ tools: list[FunctionTool | RawFunctionTool],
198
223
  conn_options: APIConnectOptions,
199
224
  ) -> None:
200
225
  super().__init__(llm, chat_ctx=chat_ctx, tools=tools, conn_options=conn_options)
@@ -228,18 +253,20 @@ class LLMStream(llm.LLMStream):
228
253
  self._event_ch.send_nowait(chat_chunk)
229
254
  retryable = False
230
255
 
256
+ # https://docs.anthropic.com/en/docs/build-with-claude/prompt-caching#tracking-cache-performance
257
+ prompt_token = (
258
+ self._input_tokens + self._cache_creation_tokens + self._cache_read_tokens
259
+ )
231
260
  self._event_ch.send_nowait(
232
261
  llm.ChatChunk(
233
262
  id=self._request_id,
234
263
  usage=llm.CompletionUsage(
235
264
  completion_tokens=self._output_tokens,
236
- prompt_tokens=self._input_tokens,
237
- total_tokens=self._input_tokens
238
- + self._output_tokens
239
- + self._cache_creation_tokens
240
- + self._cache_read_tokens,
241
- cache_creation_input_tokens=self._cache_creation_tokens,
242
- cache_read_input_tokens=self._cache_read_tokens,
265
+ prompt_tokens=prompt_token,
266
+ total_tokens=prompt_token + self._output_tokens,
267
+ prompt_cached_tokens=self._cache_read_tokens,
268
+ cache_creation_tokens=self._cache_creation_tokens,
269
+ cache_read_tokens=self._cache_read_tokens,
243
270
  ),
244
271
  )
245
272
  )
@@ -1,147 +1,54 @@
1
- import base64
2
- import json
3
- from typing import Any, Literal
1
+ from typing import Literal, Optional, Union
4
2
 
5
3
  import anthropic
6
4
  from livekit.agents import llm
7
- from livekit.agents.llm import FunctionTool
8
-
5
+ from livekit.agents.llm import FunctionTool, RawFunctionTool
6
+ from livekit.agents.llm.tool_context import (
7
+ get_raw_function_info,
8
+ is_function_tool,
9
+ is_raw_function_tool,
10
+ )
11
+
12
+ # We can define up to 4 cache breakpoints, we will add them at:
13
+ # - the last tool definition
14
+ # - the last system message
15
+ # - the last assistant message
16
+ # - the last user message before the last assistant message
17
+ # https://docs.anthropic.com/en/docs/build-with-claude/prompt-caching#structuring-your-prompt
9
18
  CACHE_CONTROL_EPHEMERAL = anthropic.types.CacheControlEphemeralParam(type="ephemeral")
10
19
 
11
- __all__ = ["to_fnc_ctx", "to_chat_ctx"]
20
+ __all__ = ["to_fnc_ctx", "CACHE_CONTROL_EPHEMERAL"]
12
21
 
13
22
 
14
23
  def to_fnc_ctx(
15
- fncs: list[FunctionTool], caching: Literal["ephemeral"] | None
24
+ fncs: list[Union[FunctionTool, RawFunctionTool]], caching: Optional[Literal["ephemeral"]]
16
25
  ) -> list[anthropic.types.ToolParam]:
17
26
  tools: list[anthropic.types.ToolParam] = []
18
- for i, fnc in enumerate(fncs):
19
- cache_ctrl = (
20
- CACHE_CONTROL_EPHEMERAL if (i == len(fncs) - 1) and caching == "ephemeral" else None
21
- )
22
- tools.append(_build_anthropic_schema(fnc, cache_ctrl=cache_ctrl))
23
-
24
- return tools
25
-
26
-
27
- def to_chat_ctx(
28
- chat_ctx: llm.ChatContext,
29
- cache_key: Any,
30
- caching: Literal["ephemeral"] | None,
31
- ) -> list[anthropic.types.MessageParam]:
32
- messages: list[anthropic.types.MessageParam] = []
33
- system_message: anthropic.types.TextBlockParam | None = None
34
- current_role: str | None = None
35
- content: list[anthropic.types.TextBlockParam] = []
36
- for i, msg in enumerate(chat_ctx.items):
37
- if msg.type == "message" and msg.role == "system":
38
- for content in msg.content:
39
- if content and isinstance(content, str):
40
- system_message = anthropic.types.TextBlockParam(
41
- text=content,
42
- type="text",
43
- cache_control=CACHE_CONTROL_EPHEMERAL if caching == "ephemeral" else None,
44
- )
45
- continue
46
-
47
- cache_ctrl = (
48
- CACHE_CONTROL_EPHEMERAL
49
- if (i == len(chat_ctx.items) - 1) and caching == "ephemeral"
50
- else None
51
- )
52
- if msg.type == "message":
53
- role = "assistant" if msg.role == "assistant" else "user"
54
- elif msg.type == "function_call":
55
- role = "assistant"
56
- elif msg.type == "function_call_output":
57
- role = "user"
58
-
59
- if role != current_role:
60
- if current_role is not None and content:
61
- messages.append(anthropic.types.MessageParam(role=current_role, content=content))
62
- content = []
63
- current_role = role
27
+ for fnc in fncs:
28
+ tools.append(_build_anthropic_schema(fnc))
64
29
 
65
- if msg.type == "message":
66
- for c in msg.content:
67
- if c and isinstance(c, str):
68
- content.append(
69
- anthropic.types.TextBlockParam(
70
- text=c, type="text", cache_control=cache_ctrl
71
- )
72
- )
73
- elif isinstance(c, llm.ImageContent):
74
- content.append(_to_image_content(c, cache_key, cache_ctrl=cache_ctrl))
75
- elif msg.type == "function_call":
76
- content.append(
77
- anthropic.types.ToolUseBlockParam(
78
- id=msg.call_id,
79
- type="tool_use",
80
- name=msg.name,
81
- input=json.loads(msg.arguments or "{}"),
82
- cache_control=cache_ctrl,
83
- )
84
- )
85
- elif msg.type == "function_call_output":
86
- content.append(
87
- anthropic.types.ToolResultBlockParam(
88
- tool_use_id=msg.call_id,
89
- type="tool_result",
90
- content=msg.output,
91
- cache_control=cache_ctrl,
92
- )
93
- )
30
+ if tools and caching == "ephemeral":
31
+ tools[-1]["cache_control"] = CACHE_CONTROL_EPHEMERAL
94
32
 
95
- if current_role is not None and content:
96
- messages.append(anthropic.types.MessageParam(role=current_role, content=content))
97
-
98
- # ensure the messages starts with a "user" message
99
- if not messages or messages[0]["role"] != "user":
100
- messages.insert(
101
- 0,
102
- anthropic.types.MessageParam(
103
- role="user",
104
- content=[anthropic.types.TextBlockParam(text="(empty)", type="text")],
105
- ),
106
- )
107
-
108
- return messages, system_message
109
-
110
-
111
- def _to_image_content(
112
- image: llm.ImageContent,
113
- cache_key: Any,
114
- cache_ctrl: anthropic.types.CacheControlEphemeralParam | None,
115
- ) -> anthropic.types.ImageBlockParam:
116
- img = llm.utils.serialize_image(image)
117
- if img.external_url:
118
- return {
119
- "type": "image",
120
- "source": {"type": "url", "url": img.external_url},
121
- "cache_control": cache_ctrl,
122
- }
123
- if cache_key not in image._cache:
124
- image._cache[cache_key] = img.data_bytes
125
- b64_data = base64.b64encode(image._cache[cache_key]).decode("utf-8")
126
- return {
127
- "type": "image",
128
- "source": {
129
- "type": "base64",
130
- "data": f"data:{img.mime_type};base64,{b64_data}",
131
- "media_type": img.mime_type,
132
- },
133
- "cache_control": cache_ctrl,
134
- }
33
+ return tools
135
34
 
136
35
 
137
36
  def _build_anthropic_schema(
138
- function_tool: FunctionTool,
139
- cache_ctrl: anthropic.types.CacheControlEphemeralParam | None = None,
37
+ function_tool: Union[FunctionTool, RawFunctionTool],
140
38
  ) -> anthropic.types.ToolParam:
141
- fnc = llm.utils.build_legacy_openai_schema(function_tool, internally_tagged=True)
142
- return anthropic.types.ToolParam(
143
- name=fnc["name"],
144
- description=fnc["description"] or "",
145
- input_schema=fnc["parameters"],
146
- cache_control=cache_ctrl,
147
- )
39
+ if is_function_tool(function_tool):
40
+ fnc = llm.utils.build_legacy_openai_schema(function_tool, internally_tagged=True)
41
+ return anthropic.types.ToolParam(
42
+ name=fnc["name"],
43
+ description=fnc["description"] or "",
44
+ input_schema=fnc["parameters"],
45
+ )
46
+ elif is_raw_function_tool(function_tool):
47
+ info = get_raw_function_info(function_tool)
48
+ return anthropic.types.ToolParam(
49
+ name=info.name,
50
+ description=info.raw_schema.get("description", ""),
51
+ input_schema=info.raw_schema.get("parameters", {}),
52
+ )
53
+ else:
54
+ raise ValueError("Invalid function tool")
@@ -12,4 +12,4 @@
12
12
  # See the License for the specific language governing permissions and
13
13
  # limitations under the License.
14
14
 
15
- __version__ = "1.0.22"
15
+ __version__ = "1.1.0"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: livekit-plugins-anthropic
3
- Version: 1.0.22
3
+ Version: 1.1.0
4
4
  Summary: Agent Framework plugin for services from Anthropic
5
5
  Project-URL: Documentation, https://docs.livekit.io
6
6
  Project-URL: Website, https://livekit.io/
@@ -19,7 +19,8 @@ Classifier: Topic :: Multimedia :: Video
19
19
  Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
20
20
  Requires-Python: >=3.9.0
21
21
  Requires-Dist: anthropic>=0.41
22
- Requires-Dist: livekit-agents>=1.0.22
22
+ Requires-Dist: httpx
23
+ Requires-Dist: livekit-agents>=1.1.0
23
24
  Description-Content-Type: text/markdown
24
25
 
25
26
  # Anthropic plugin for LiveKit Agents
@@ -0,0 +1,10 @@
1
+ livekit/plugins/anthropic/__init__.py,sha256=oZc3LY5BrjXy-hYMb0wyvXgqXRW-ikAlo5xGh2gM0Nc,1304
2
+ livekit/plugins/anthropic/llm.py,sha256=FhzN2ualvDu48iktJIm7MFNR_i9T06QPX8USAtDBBuY,14217
3
+ livekit/plugins/anthropic/log.py,sha256=fG1pYSY88AnT738gZrmzF9FO4l4BdGENj3VKHMQB3Yo,72
4
+ livekit/plugins/anthropic/models.py,sha256=wyTr2nl6SL4ylN6s4mHJcqtmgV2mjJysZo89FknWdhI,213
5
+ livekit/plugins/anthropic/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
+ livekit/plugins/anthropic/utils.py,sha256=CojebB7CWjX6JyObIrM8eRLgli07NGnp84mGDPrLQ1s,1907
7
+ livekit/plugins/anthropic/version.py,sha256=7SjyflIFTjH0djSotKGIRoRykPCqMpVYetIlvHMFuh0,600
8
+ livekit_plugins_anthropic-1.1.0.dist-info/METADATA,sha256=u9CAQed356V3xbZxbjRKH-02tAq2UZEMhfHjfr-0HY0,1452
9
+ livekit_plugins_anthropic-1.1.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
10
+ livekit_plugins_anthropic-1.1.0.dist-info/RECORD,,
@@ -1,10 +0,0 @@
1
- livekit/plugins/anthropic/__init__.py,sha256=oZc3LY5BrjXy-hYMb0wyvXgqXRW-ikAlo5xGh2gM0Nc,1304
2
- livekit/plugins/anthropic/llm.py,sha256=Q0U5ZIufGtR5abMdfMkZrANz7Ri4RxZLQYocDZOhK1Y,12884
3
- livekit/plugins/anthropic/log.py,sha256=fG1pYSY88AnT738gZrmzF9FO4l4BdGENj3VKHMQB3Yo,72
4
- livekit/plugins/anthropic/models.py,sha256=wyTr2nl6SL4ylN6s4mHJcqtmgV2mjJysZo89FknWdhI,213
5
- livekit/plugins/anthropic/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
- livekit/plugins/anthropic/utils.py,sha256=Nfl9dGCZGDEJAHj_f-TmePr8bKJrc8IwM6Houjev4DE,5158
7
- livekit/plugins/anthropic/version.py,sha256=-8dkOE2vDSF9WN8VoBrSwU2sb5YBGFuwPnSQXQ-uaYM,601
8
- livekit_plugins_anthropic-1.0.22.dist-info/METADATA,sha256=k3W_EpA4q1RQSlsDN-3CSa-VcqJ45upY7eV5jhI8xgY,1433
9
- livekit_plugins_anthropic-1.0.22.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
10
- livekit_plugins_anthropic-1.0.22.dist-info/RECORD,,