livekit-plugins-anthropic 0.2.1__py3-none-any.whl → 0.2.3__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.
@@ -35,3 +35,12 @@ class AnthropicPlugin(Plugin):
35
35
 
36
36
 
37
37
  Plugin.register_plugin(AnthropicPlugin())
38
+
39
+ # Cleanup docs of unexported modules
40
+ _module = dir()
41
+ NOT_IN_ALL = [m for m in _module if m not in __all__]
42
+
43
+ __pdoc__ = {}
44
+
45
+ for n in NOT_IN_ALL:
46
+ __pdoc__[n] = False
@@ -23,7 +23,13 @@ from typing import Any, Awaitable, List, Tuple, get_args, get_origin
23
23
 
24
24
  import httpx
25
25
  from livekit import rtc
26
- from livekit.agents import llm, utils
26
+ from livekit.agents import (
27
+ APIConnectionError,
28
+ APIStatusError,
29
+ APITimeoutError,
30
+ llm,
31
+ utils,
32
+ )
27
33
 
28
34
  import anthropic
29
35
 
@@ -37,6 +43,7 @@ from .models import (
37
43
  class LLMOptions:
38
44
  model: str | ChatModels
39
45
  user: str | None
46
+ temperature: float | None
40
47
 
41
48
 
42
49
  class LLM(llm.LLM):
@@ -48,6 +55,7 @@ class LLM(llm.LLM):
48
55
  base_url: str | None = None,
49
56
  user: str | None = None,
50
57
  client: anthropic.AsyncClient | None = None,
58
+ temperature: float | None = None,
51
59
  ) -> None:
52
60
  """
53
61
  Create a new instance of Anthropic LLM.
@@ -55,13 +63,14 @@ class LLM(llm.LLM):
55
63
  ``api_key`` must be set to your Anthropic API key, either using the argument or by setting
56
64
  the ``ANTHROPIC_API_KEY`` environmental variable.
57
65
  """
66
+ super().__init__()
58
67
 
59
68
  # throw an error on our end
60
69
  api_key = api_key or os.environ.get("ANTHROPIC_API_KEY")
61
70
  if api_key is None:
62
71
  raise ValueError("Anthropic API key is required")
63
72
 
64
- self._opts = LLMOptions(model=model, user=user)
73
+ self._opts = LLMOptions(model=model, user=user, temperature=temperature)
65
74
  self._client = client or anthropic.AsyncClient(
66
75
  api_key=api_key,
67
76
  base_url=base_url,
@@ -85,6 +94,9 @@ class LLM(llm.LLM):
85
94
  n: int | None = 1,
86
95
  parallel_tool_calls: bool | None = None,
87
96
  ) -> "LLMStream":
97
+ if temperature is None:
98
+ temperature = self._opts.temperature
99
+
88
100
  opts: dict[str, Any] = dict()
89
101
  if fnc_ctx and len(fnc_ctx.ai_functions) > 0:
90
102
  fncs_desc: list[anthropic.types.ToolParam] = []
@@ -99,8 +111,9 @@ class LLM(llm.LLM):
99
111
  latest_system_message = _latest_system_message(chat_ctx)
100
112
  anthropic_ctx = _build_anthropic_context(chat_ctx.messages, id(self))
101
113
  collaped_anthropic_ctx = _merge_messages(anthropic_ctx)
114
+
102
115
  stream = self._client.messages.create(
103
- max_tokens=opts.get("max_tokens", 1000),
116
+ max_tokens=opts.get("max_tokens", 1024),
104
117
  system=latest_system_message,
105
118
  messages=collaped_anthropic_ctx,
106
119
  model=self._opts.model,
@@ -110,12 +123,15 @@ class LLM(llm.LLM):
110
123
  **opts,
111
124
  )
112
125
 
113
- return LLMStream(anthropic_stream=stream, chat_ctx=chat_ctx, fnc_ctx=fnc_ctx)
126
+ return LLMStream(
127
+ self, anthropic_stream=stream, chat_ctx=chat_ctx, fnc_ctx=fnc_ctx
128
+ )
114
129
 
115
130
 
116
131
  class LLMStream(llm.LLMStream):
117
132
  def __init__(
118
133
  self,
134
+ llm: LLM,
119
135
  *,
120
136
  anthropic_stream: Awaitable[
121
137
  anthropic.AsyncStream[anthropic.types.RawMessageStreamEvent]
@@ -123,7 +139,7 @@ class LLMStream(llm.LLMStream):
123
139
  chat_ctx: llm.ChatContext,
124
140
  fnc_ctx: llm.FunctionContext | None,
125
141
  ) -> None:
126
- super().__init__(chat_ctx=chat_ctx, fnc_ctx=fnc_ctx)
142
+ super().__init__(llm, chat_ctx=chat_ctx, fnc_ctx=fnc_ctx)
127
143
  self._awaitable_anthropic_stream = anthropic_stream
128
144
  self._anthropic_stream: (
129
145
  anthropic.AsyncStream[anthropic.types.RawMessageStreamEvent] | None
@@ -134,89 +150,113 @@ class LLMStream(llm.LLMStream):
134
150
  self._fnc_name: str | None = None
135
151
  self._fnc_raw_arguments: str | None = None
136
152
 
137
- async def aclose(self) -> None:
138
- if self._anthropic_stream:
139
- await self._anthropic_stream.close()
140
-
141
- return await super().aclose()
142
-
143
- async def __anext__(self):
144
- if not self._anthropic_stream:
145
- self._anthropic_stream = await self._awaitable_anthropic_stream
146
-
147
- fn_calling_enabled = self._fnc_ctx is not None
148
- ignore = False
149
-
150
- async for event in self._anthropic_stream:
151
- if event.type == "message_start":
152
- pass
153
- elif event.type == "message_delta":
154
- pass
155
- elif event.type == "message_stop":
156
- pass
157
- elif event.type == "content_block_start":
158
- if event.content_block.type == "tool_use":
159
- self._tool_call_id = event.content_block.id
160
- self._fnc_raw_arguments = ""
161
- self._fnc_name = event.content_block.name
162
- elif event.type == "content_block_delta":
163
- delta = event.delta
164
- if delta.type == "text_delta":
165
- text = delta.text
166
-
167
- # Anthropic seems to add a prompt when tool calling is enabled
168
- # where responses always start with a "<thinking>" block containing
169
- # the LLM's chain of thought. It's very verbose and not useful for voice
170
- # applications.
171
- if fn_calling_enabled:
172
- if text.startswith("<thinking>"):
173
- ignore = True
174
-
175
- if "</thinking>" in text:
176
- text = text.split("</thinking>")[-1]
177
- ignore = False
178
-
179
- if ignore:
180
- continue
181
-
182
- return llm.ChatChunk(
183
- choices=[
184
- llm.Choice(
185
- delta=llm.ChoiceDelta(content=text, role="assistant")
186
- )
187
- ]
188
- )
189
- elif delta.type == "input_json_delta":
190
- assert self._fnc_raw_arguments is not None
191
- self._fnc_raw_arguments += delta.partial_json
192
-
193
- elif event.type == "content_block_stop":
194
- if self._tool_call_id is not None and self._fnc_ctx:
195
- assert self._fnc_name is not None
196
- assert self._fnc_raw_arguments is not None
197
- fnc_info = _create_ai_function_info(
198
- self._fnc_ctx,
199
- self._tool_call_id,
200
- self._fnc_name,
201
- self._fnc_raw_arguments,
153
+ self._request_id: str = ""
154
+ self._ignoring_cot = False # ignore chain of thought
155
+ self._input_tokens = 0
156
+ self._output_tokens = 0
157
+
158
+ async def _main_task(self) -> None:
159
+ try:
160
+ if not self._anthropic_stream:
161
+ self._anthropic_stream = await self._awaitable_anthropic_stream
162
+
163
+ async with self._anthropic_stream as stream:
164
+ async for event in stream:
165
+ chat_chunk = self._parse_event(event)
166
+ if chat_chunk is not None:
167
+ self._event_ch.send_nowait(chat_chunk)
168
+
169
+ self._event_ch.send_nowait(
170
+ llm.ChatChunk(
171
+ request_id=self._request_id,
172
+ usage=llm.CompletionUsage(
173
+ completion_tokens=self._output_tokens,
174
+ prompt_tokens=self._input_tokens,
175
+ total_tokens=self._input_tokens + self._output_tokens,
176
+ ),
202
177
  )
203
- self._function_calls_info.append(fnc_info)
204
- chunk = llm.ChatChunk(
205
- choices=[
206
- llm.Choice(
207
- delta=llm.ChoiceDelta(
208
- role="assistant", tool_calls=[fnc_info]
209
- ),
210
- index=0,
211
- )
212
- ]
213
- )
214
- self._tool_call_id = None
215
- self._fnc_raw_arguments = None
216
- self._fnc_name = None
217
- return chunk
178
+ )
179
+ except anthropic.APITimeoutError:
180
+ raise APITimeoutError()
181
+ except anthropic.APIStatusError as e:
182
+ raise APIStatusError(
183
+ e.message,
184
+ status_code=e.status_code,
185
+ request_id=e.request_id,
186
+ body=e.body,
187
+ )
188
+ except Exception as e:
189
+ raise APIConnectionError() from e
190
+
191
+ def _parse_event(
192
+ self, event: anthropic.types.RawMessageStreamEvent
193
+ ) -> llm.ChatChunk | None:
194
+ if event.type == "message_start":
195
+ self._request_id = event.message.id
196
+ self._input_tokens = event.message.usage.input_tokens
197
+ self._output_tokens = event.message.usage.output_tokens
198
+ elif event.type == "message_delta":
199
+ self._output_tokens += event.usage.output_tokens
200
+ elif event.type == "content_block_start":
201
+ if event.content_block.type == "tool_use":
202
+ self._tool_call_id = event.content_block.id
203
+ self._fnc_name = event.content_block.name
204
+ self._fnc_raw_arguments = ""
205
+ elif event.type == "content_block_delta":
206
+ delta = event.delta
207
+ if delta.type == "text_delta":
208
+ text = delta.text
209
+
210
+ if self._fnc_ctx is not None:
211
+ # anthropic may inject COC when using functions
212
+ if text.startswith("<thinking>"):
213
+ self._ignoring_cot = True
214
+ elif self._ignoring_cot and "</thinking>" in text:
215
+ text = text.split("</thinking>")[-1]
216
+ self._ignoring_cot = False
217
+
218
+ if self._ignoring_cot:
219
+ return None
220
+
221
+ return llm.ChatChunk(
222
+ request_id=self._request_id,
223
+ choices=[
224
+ llm.Choice(
225
+ delta=llm.ChoiceDelta(content=text, role="assistant")
226
+ )
227
+ ],
228
+ )
229
+ elif delta.type == "input_json_delta":
230
+ assert self._fnc_raw_arguments is not None
231
+ self._fnc_raw_arguments += delta.partial_json
232
+
233
+ elif event.type == "content_block_stop":
234
+ if self._tool_call_id is not None and self._fnc_ctx:
235
+ assert self._fnc_name is not None
236
+ assert self._fnc_raw_arguments is not None
237
+
238
+ fnc_info = _create_ai_function_info(
239
+ self._fnc_ctx,
240
+ self._tool_call_id,
241
+ self._fnc_name,
242
+ self._fnc_raw_arguments,
243
+ )
244
+ self._function_calls_info.append(fnc_info)
245
+
246
+ chat_chunk = llm.ChatChunk(
247
+ request_id=self._request_id,
248
+ choices=[
249
+ llm.Choice(
250
+ delta=llm.ChoiceDelta(
251
+ role="assistant", tool_calls=[fnc_info]
252
+ ),
253
+ )
254
+ ],
255
+ )
256
+ self._tool_call_id = self._fnc_raw_arguments = self._fnc_name = None
257
+ return chat_chunk
218
258
 
219
- raise StopAsyncIteration
259
+ return None
220
260
 
221
261
 
222
262
  def _latest_system_message(chat_ctx: llm.ChatContext) -> str:
@@ -286,7 +326,7 @@ def _build_anthropic_message(
286
326
  a_content = a_msg["content"]
287
327
 
288
328
  # add content if provided
289
- if isinstance(msg.content, str):
329
+ if isinstance(msg.content, str) and msg.content:
290
330
  a_msg["content"].append(
291
331
  anthropic.types.TextBlock(
292
332
  text=msg.content,
@@ -295,7 +335,7 @@ def _build_anthropic_message(
295
335
  )
296
336
  elif isinstance(msg.content, list):
297
337
  for cnt in msg.content:
298
- if isinstance(cnt, str):
338
+ if isinstance(cnt, str) and cnt:
299
339
  content: anthropic.types.TextBlock = anthropic.types.TextBlock(
300
340
  text=cnt,
301
341
  type="text",
@@ -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__ = "0.2.1"
15
+ __version__ = "0.2.3"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: livekit-plugins-anthropic
3
- Version: 0.2.1
3
+ Version: 0.2.3
4
4
  Summary: Agent Framework plugin for services from Anthropic
5
5
  Home-page: https://github.com/livekit/agents
6
6
  License: Apache-2.0
@@ -19,8 +19,8 @@ Classifier: Programming Language :: Python :: 3.10
19
19
  Classifier: Programming Language :: Python :: 3 :: Only
20
20
  Requires-Python: >=3.9.0
21
21
  Description-Content-Type: text/markdown
22
- Requires-Dist: livekit-agents ~=0.8
23
- Requires-Dist: anthropic ~=0.34
22
+ Requires-Dist: livekit-agents >=0.11
23
+ Requires-Dist: anthropic >=0.34
24
24
 
25
25
  # LiveKit Plugins Anthropic
26
26
 
@@ -0,0 +1,10 @@
1
+ livekit/plugins/anthropic/__init__.py,sha256=1WCyNEaR6qBsX54qJQM0SeY-QHIucww16PLXcSnMqRo,1175
2
+ livekit/plugins/anthropic/llm.py,sha256=6rNtLJOfvB72gmmkFk8qN-N8fKnm-vhflpiBKTU_gqM,18921
3
+ livekit/plugins/anthropic/log.py,sha256=fG1pYSY88AnT738gZrmzF9FO4l4BdGENj3VKHMQB3Yo,72
4
+ livekit/plugins/anthropic/models.py,sha256=AVEhrEtKfWxsd-R03u7R74hcKjJq4oDVSTukvoPQGb0,179
5
+ livekit/plugins/anthropic/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
+ livekit/plugins/anthropic/version.py,sha256=A5eBQ2sWvNCBt2aefaSvtvK0XRwtGa-rgF4340O8cjk,600
7
+ livekit_plugins_anthropic-0.2.3.dist-info/METADATA,sha256=dttEt-3lS_LDVl0XZkgnINXr560oPngIyXyzeJvih6c,1265
8
+ livekit_plugins_anthropic-0.2.3.dist-info/WHEEL,sha256=P9jw-gEje8ByB7_hXoICnHtVCrEwMQh-630tKvQWehc,91
9
+ livekit_plugins_anthropic-0.2.3.dist-info/top_level.txt,sha256=OoDok3xUmXbZRvOrfvvXB-Juu4DX79dlq188E19YHoo,8
10
+ livekit_plugins_anthropic-0.2.3.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (74.1.2)
2
+ Generator: setuptools (75.3.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -1,10 +0,0 @@
1
- livekit/plugins/anthropic/__init__.py,sha256=g6KUqOfZo9DIBwBD98u6QOWY7pr8ZYJJ61fk3AWpoa4,1006
2
- livekit/plugins/anthropic/llm.py,sha256=s6Xt_5kuvypG1ZLUq_aYaYO_ZaTgagyM4b1c5iI2WUo,17652
3
- livekit/plugins/anthropic/log.py,sha256=fG1pYSY88AnT738gZrmzF9FO4l4BdGENj3VKHMQB3Yo,72
4
- livekit/plugins/anthropic/models.py,sha256=AVEhrEtKfWxsd-R03u7R74hcKjJq4oDVSTukvoPQGb0,179
5
- livekit/plugins/anthropic/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
- livekit/plugins/anthropic/version.py,sha256=tb-eucPKT0bz3vQDaI1ltUZEhGKRFOFhN3U5ZIv_-xY,600
7
- livekit_plugins_anthropic-0.2.1.dist-info/METADATA,sha256=-m1Q9gn3H1gRoVjFb1uIDfyJ5TP6p1HEMrh-zDyGeQs,1264
8
- livekit_plugins_anthropic-0.2.1.dist-info/WHEEL,sha256=cVxcB9AmuTcXqmwrtPhNK88dr7IR_b6qagTj0UvIEbY,91
9
- livekit_plugins_anthropic-0.2.1.dist-info/top_level.txt,sha256=OoDok3xUmXbZRvOrfvvXB-Juu4DX79dlq188E19YHoo,8
10
- livekit_plugins_anthropic-0.2.1.dist-info/RECORD,,