livekit-plugins-anthropic 0.2.0__py3-none-any.whl → 0.2.2__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,17 +43,19 @@ 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):
43
50
  def __init__(
44
51
  self,
45
52
  *,
46
- model: str | ChatModels = "claude-3-opus-20240229",
53
+ model: str | ChatModels = "claude-3-haiku-20240307",
47
54
  api_key: str | None = None,
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] = []
@@ -100,7 +112,7 @@ class LLM(llm.LLM):
100
112
  anthropic_ctx = _build_anthropic_context(chat_ctx.messages, id(self))
101
113
  collaped_anthropic_ctx = _merge_messages(anthropic_ctx)
102
114
  stream = self._client.messages.create(
103
- max_tokens=opts.get("max_tokens", 1000),
115
+ max_tokens=opts.get("max_tokens", 1024),
104
116
  system=latest_system_message,
105
117
  messages=collaped_anthropic_ctx,
106
118
  model=self._opts.model,
@@ -110,12 +122,15 @@ class LLM(llm.LLM):
110
122
  **opts,
111
123
  )
112
124
 
113
- return LLMStream(anthropic_stream=stream, chat_ctx=chat_ctx, fnc_ctx=fnc_ctx)
125
+ return LLMStream(
126
+ self, anthropic_stream=stream, chat_ctx=chat_ctx, fnc_ctx=fnc_ctx
127
+ )
114
128
 
115
129
 
116
130
  class LLMStream(llm.LLMStream):
117
131
  def __init__(
118
132
  self,
133
+ llm: LLM,
119
134
  *,
120
135
  anthropic_stream: Awaitable[
121
136
  anthropic.AsyncStream[anthropic.types.RawMessageStreamEvent]
@@ -123,7 +138,7 @@ class LLMStream(llm.LLMStream):
123
138
  chat_ctx: llm.ChatContext,
124
139
  fnc_ctx: llm.FunctionContext | None,
125
140
  ) -> None:
126
- super().__init__(chat_ctx=chat_ctx, fnc_ctx=fnc_ctx)
141
+ super().__init__(llm, chat_ctx=chat_ctx, fnc_ctx=fnc_ctx)
127
142
  self._awaitable_anthropic_stream = anthropic_stream
128
143
  self._anthropic_stream: (
129
144
  anthropic.AsyncStream[anthropic.types.RawMessageStreamEvent] | None
@@ -134,70 +149,113 @@ class LLMStream(llm.LLMStream):
134
149
  self._fnc_name: str | None = None
135
150
  self._fnc_raw_arguments: str | None = None
136
151
 
137
- async def aclose(self) -> None:
138
- if self._anthropic_stream:
139
- await self._anthropic_stream.close()
152
+ self._request_id: str = ""
153
+ self._ignoring_cot = False # ignore chain of thought
154
+ self._input_tokens = 0
155
+ self._output_tokens = 0
140
156
 
141
- return await super().aclose()
142
-
143
- async def __anext__(self):
157
+ async def _main_task(self) -> None:
144
158
  if not self._anthropic_stream:
145
159
  self._anthropic_stream = await self._awaitable_anthropic_stream
146
160
 
147
- async for event in self._anthropic_stream:
148
- if event.type == "message_start":
149
- pass
150
- elif event.type == "message_delta":
151
- pass
152
- elif event.type == "message_stop":
153
- pass
154
- elif event.type == "content_block_start":
155
- if event.content_block.type == "tool_use":
156
- self._tool_call_id = event.content_block.id
157
- self._fnc_raw_arguments = ""
158
- self._fnc_name = event.content_block.name
159
- elif event.type == "content_block_delta":
160
- delta = event.delta
161
- if delta.type == "text_delta":
162
- return llm.ChatChunk(
163
- choices=[
164
- llm.Choice(
165
- delta=llm.ChoiceDelta(
166
- content=delta.text, role="assistant"
167
- )
168
- )
169
- ]
170
- )
171
- elif delta.type == "input_json_delta":
172
- assert self._fnc_raw_arguments is not None
173
- self._fnc_raw_arguments += delta.partial_json
174
- elif event.type == "content_block_stop":
175
- if self._tool_call_id is not None and self._fnc_ctx:
176
- assert self._fnc_name is not None
177
- assert self._fnc_raw_arguments is not None
178
- fnc_info = _create_ai_function_info(
179
- self._fnc_ctx,
180
- self._tool_call_id,
181
- self._fnc_name,
182
- self._fnc_raw_arguments,
183
- )
184
- self._function_calls_info.append(fnc_info)
185
- chunk = llm.ChatChunk(
186
- choices=[
187
- llm.Choice(
188
- delta=llm.ChoiceDelta(
189
- role="assistant", tool_calls=[fnc_info]
190
- ),
191
- index=0,
192
- )
193
- ]
161
+ try:
162
+ async with self._anthropic_stream as stream:
163
+ async for event in stream:
164
+ chat_chunk = self._parse_event(event)
165
+ if chat_chunk is not None:
166
+ self._event_ch.send_nowait(chat_chunk)
167
+
168
+ self._event_ch.send_nowait(
169
+ llm.ChatChunk(
170
+ request_id=self._request_id,
171
+ usage=llm.CompletionUsage(
172
+ completion_tokens=self._output_tokens,
173
+ prompt_tokens=self._input_tokens,
174
+ total_tokens=self._input_tokens + self._output_tokens,
175
+ ),
194
176
  )
195
- self._tool_call_id = None
196
- self._fnc_raw_arguments = None
197
- self._fnc_name = None
198
- return chunk
177
+ )
178
+ except anthropic.APITimeoutError:
179
+ raise APITimeoutError()
180
+ except anthropic.APIStatusError as e:
181
+ raise APIStatusError(
182
+ e.message,
183
+ status_code=e.status_code,
184
+ request_id=e.request_id,
185
+ body=e.body,
186
+ )
187
+ except Exception as e:
188
+ raise APIConnectionError() from e
189
+
190
+ def _parse_event(
191
+ self, event: anthropic.types.RawMessageStreamEvent
192
+ ) -> llm.ChatChunk | None:
193
+ if event.type == "message_start":
194
+ self._request_id = event.message.id
195
+ self._input_tokens = event.message.usage.input_tokens
196
+ self._output_tokens = event.message.usage.output_tokens
197
+ elif event.type == "message_delta":
198
+ self._output_tokens += event.usage.output_tokens
199
+ elif event.type == "content_block_start":
200
+ if event.content_block.type == "tool_use":
201
+ self._tool_call_id = event.content_block.id
202
+ self._fnc_name = event.content_block.name
203
+ self._fnc_raw_arguments = ""
204
+ elif event.type == "content_block_delta":
205
+ delta = event.delta
206
+ if delta.type == "text_delta":
207
+ text = delta.text
208
+
209
+ if self._fnc_ctx is not None:
210
+ # anthropic may inject COC when using functions
211
+ if text.startswith("<thinking>"):
212
+ self._ignoring_cot = True
213
+ elif self._ignoring_cot and "</thinking>" in text:
214
+ text = text.split("</thinking>")[-1]
215
+ self._ignoring_cot = False
216
+
217
+ if self._ignoring_cot:
218
+ return None
219
+
220
+ return llm.ChatChunk(
221
+ request_id=self._request_id,
222
+ choices=[
223
+ llm.Choice(
224
+ delta=llm.ChoiceDelta(content=text, role="assistant")
225
+ )
226
+ ],
227
+ )
228
+ elif delta.type == "input_json_delta":
229
+ assert self._fnc_raw_arguments is not None
230
+ self._fnc_raw_arguments += delta.partial_json
231
+
232
+ elif event.type == "content_block_stop":
233
+ if self._tool_call_id is not None and self._fnc_ctx:
234
+ assert self._fnc_name is not None
235
+ assert self._fnc_raw_arguments is not None
236
+
237
+ fnc_info = _create_ai_function_info(
238
+ self._fnc_ctx,
239
+ self._tool_call_id,
240
+ self._fnc_name,
241
+ self._fnc_raw_arguments,
242
+ )
243
+ self._function_calls_info.append(fnc_info)
244
+
245
+ chat_chunk = llm.ChatChunk(
246
+ request_id=self._request_id,
247
+ choices=[
248
+ llm.Choice(
249
+ delta=llm.ChoiceDelta(
250
+ role="assistant", tool_calls=[fnc_info]
251
+ ),
252
+ )
253
+ ],
254
+ )
255
+ self._tool_call_id = self._fnc_raw_arguments = self._fnc_name = None
256
+ return chat_chunk
199
257
 
200
- raise StopAsyncIteration
258
+ return None
201
259
 
202
260
 
203
261
  def _latest_system_message(chat_ctx: llm.ChatContext) -> str:
@@ -249,13 +307,15 @@ def _build_anthropic_context(
249
307
  ) -> List[anthropic.types.MessageParam]:
250
308
  result: List[anthropic.types.MessageParam] = []
251
309
  for msg in chat_ctx:
252
- a_msg = _build_anthropic_message(msg, cache_key)
310
+ a_msg = _build_anthropic_message(msg, cache_key, chat_ctx)
253
311
  if a_msg:
254
312
  result.append(a_msg)
255
313
  return result
256
314
 
257
315
 
258
- def _build_anthropic_message(msg: llm.ChatMessage, cache_key: Any):
316
+ def _build_anthropic_message(
317
+ msg: llm.ChatMessage, cache_key: Any, chat_ctx: List[llm.ChatMessage]
318
+ ) -> anthropic.types.MessageParam | None:
259
319
  if msg.role == "user" or msg.role == "assistant":
260
320
  a_msg: anthropic.types.MessageParam = {
261
321
  "role": msg.role,
@@ -282,38 +342,35 @@ def _build_anthropic_message(msg: llm.ChatMessage, cache_key: Any):
282
342
  a_content.append(content)
283
343
  elif isinstance(cnt, llm.ChatImage):
284
344
  a_content.append(_build_anthropic_image_content(cnt, cache_key))
285
- return a_msg
286
- elif msg.role == "tool":
287
- ant_msg: anthropic.types.MessageParam = {
288
- "role": "assistant",
289
- "content": [],
290
- }
291
- assert isinstance(ant_msg["content"], list)
292
- # make sure to provide when function has been called inside the context
293
- # (+ raw_arguments)
345
+
294
346
  if msg.tool_calls is not None:
295
347
  for fnc in msg.tool_calls:
296
- ant_msg["content"].append(
297
- {
298
- "id": fnc.tool_call_id,
299
- "type": "tool_use",
300
- "input": fnc.arguments,
301
- "name": fnc.function_info.name,
302
- }
348
+ tool_use = anthropic.types.ToolUseBlockParam(
349
+ id=fnc.tool_call_id,
350
+ type="tool_use",
351
+ name=fnc.function_info.name,
352
+ input=fnc.arguments,
303
353
  )
304
- if isinstance(msg.content, str):
305
- ant_msg["content"].append(
306
- {
307
- "tool_use_id": fnc.tool_call_id,
308
- "type": "tool_result",
309
- "content": msg.content,
310
- }
311
- )
312
- else:
313
- logger.warning(
314
- "tool result content is not a string, this is not supported by anthropic"
315
- )
316
- return ant_msg
354
+ a_content.append(tool_use)
355
+
356
+ return a_msg
357
+ elif msg.role == "tool":
358
+ if not isinstance(msg.content, str):
359
+ logger.warning("tool message content is not a string")
360
+ return None
361
+ if not msg.tool_call_id:
362
+ return None
363
+
364
+ u_content = anthropic.types.ToolResultBlockParam(
365
+ tool_use_id=msg.tool_call_id,
366
+ type="tool_result",
367
+ content=msg.content,
368
+ is_error=msg.tool_exception is not None,
369
+ )
370
+ return {
371
+ "role": "user",
372
+ "content": [u_content],
373
+ }
317
374
 
318
375
  return None
319
376
 
@@ -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.0"
15
+ __version__ = "0.2.2"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: livekit-plugins-anthropic
3
- Version: 0.2.0
3
+ Version: 0.2.2
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=mcTBYT3_ZVAWx9ZnCUj_96NM44dF6SF1R0ZLMUQt79Y,18888
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=vseqf_ctDD3TBGPSArXT_dFZvNHkuJc4_8GgQSvrKrM,600
7
+ livekit_plugins_anthropic-0.2.2.dist-info/METADATA,sha256=KWqm8V_Ooo_sGKJ2JNAVrfOCu1sfuSLLBHkOC4tZHfQ,1265
8
+ livekit_plugins_anthropic-0.2.2.dist-info/WHEEL,sha256=P9jw-gEje8ByB7_hXoICnHtVCrEwMQh-630tKvQWehc,91
9
+ livekit_plugins_anthropic-0.2.2.dist-info/top_level.txt,sha256=OoDok3xUmXbZRvOrfvvXB-Juu4DX79dlq188E19YHoo,8
10
+ livekit_plugins_anthropic-0.2.2.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=SJo_opc9_2rKYvcDW8-ltuOD-p7QUc0oROGDHu04htY,17162
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=cLFCdnm5S21CiJ5UJBcqfRvvFkCQ8p6M5fFUJVJkEiM,600
7
- livekit_plugins_anthropic-0.2.0.dist-info/METADATA,sha256=1VWzsOFCxwtoB2m-NVZgKPoPI8xwsZctTbZJO8FYxbI,1264
8
- livekit_plugins_anthropic-0.2.0.dist-info/WHEEL,sha256=cVxcB9AmuTcXqmwrtPhNK88dr7IR_b6qagTj0UvIEbY,91
9
- livekit_plugins_anthropic-0.2.0.dist-info/top_level.txt,sha256=OoDok3xUmXbZRvOrfvvXB-Juu4DX79dlq188E19YHoo,8
10
- livekit_plugins_anthropic-0.2.0.dist-info/RECORD,,