livekit-plugins-anthropic 0.2.13__py3-none-any.whl → 1.0.0.dev5__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.
@@ -14,56 +14,40 @@
14
14
 
15
15
  from __future__ import annotations
16
16
 
17
- import base64
18
- import inspect
19
- import json
20
17
  import os
18
+ from collections.abc import Awaitable
21
19
  from dataclasses import dataclass
22
- from typing import (
23
- Any,
24
- Awaitable,
25
- List,
26
- Literal,
27
- Union,
28
- cast,
29
- get_args,
30
- get_origin,
31
- )
20
+ from typing import Any, Literal
32
21
 
33
22
  import httpx
34
- from livekit import rtc
35
- from livekit.agents import (
36
- APIConnectionError,
37
- APIStatusError,
38
- APITimeoutError,
39
- llm,
40
- utils,
41
- )
42
- from livekit.agents.llm import LLMCapabilities, ToolChoice
43
- from livekit.agents.llm.function_context import (
44
- _create_ai_function_info,
45
- _is_optional_type,
46
- )
47
- from livekit.agents.types import DEFAULT_API_CONNECT_OPTIONS, APIConnectOptions
48
23
 
49
24
  import anthropic
50
-
51
- from .log import logger
52
- from .models import (
53
- ChatModels,
25
+ from livekit.agents import APIConnectionError, APIStatusError, APITimeoutError, llm
26
+ from livekit.agents.llm import ToolChoice
27
+ from livekit.agents.llm.chat_context import ChatContext
28
+ from livekit.agents.llm.tool_context import FunctionTool
29
+ from livekit.agents.types import (
30
+ DEFAULT_API_CONNECT_OPTIONS,
31
+ NOT_GIVEN,
32
+ APIConnectOptions,
33
+ NotGivenOr,
54
34
  )
35
+ from livekit.agents.utils import is_given
55
36
 
56
- CACHE_CONTROL_EPHEMERAL = anthropic.types.CacheControlEphemeralParam(type="ephemeral")
37
+ from .models import ChatModels
38
+ from .utils import to_chat_ctx, to_fnc_ctx
57
39
 
58
40
 
59
41
  @dataclass
60
- class LLMOptions:
42
+ class _LLMOptions:
61
43
  model: str | ChatModels
62
- user: str | None
63
- temperature: float | None
64
- parallel_tool_calls: bool | None
65
- tool_choice: Union[ToolChoice, Literal["auto", "required", "none"]] | None
66
- caching: Literal["ephemeral"] | None = None
44
+ user: NotGivenOr[str]
45
+ temperature: NotGivenOr[float]
46
+ parallel_tool_calls: NotGivenOr[bool]
47
+ tool_choice: NotGivenOr[ToolChoice | Literal["auto", "required", "none"]]
48
+ caching: NotGivenOr[Literal["ephemeral"]]
49
+ top_k: NotGivenOr[int]
50
+ max_tokens: NotGivenOr[int]
67
51
  """If set to "ephemeral", the system prompt, tools, and chat history will be cached."""
68
52
 
69
53
 
@@ -72,14 +56,16 @@ class LLM(llm.LLM):
72
56
  self,
73
57
  *,
74
58
  model: str | ChatModels = "claude-3-5-sonnet-20241022",
75
- api_key: str | None = None,
76
- base_url: str | None = None,
77
- user: str | None = None,
59
+ api_key: NotGivenOr[str] = NOT_GIVEN,
60
+ base_url: NotGivenOr[str] = NOT_GIVEN,
61
+ user: NotGivenOr[str] = NOT_GIVEN,
78
62
  client: anthropic.AsyncClient | None = None,
79
- temperature: float | None = None,
80
- parallel_tool_calls: bool | None = None,
81
- tool_choice: Union[ToolChoice, Literal["auto", "required", "none"]] = "auto",
82
- caching: Literal["ephemeral"] | None = None,
63
+ top_k: NotGivenOr[int] = NOT_GIVEN,
64
+ max_tokens: NotGivenOr[int] = NOT_GIVEN,
65
+ temperature: NotGivenOr[float] = NOT_GIVEN,
66
+ parallel_tool_calls: NotGivenOr[bool] = NOT_GIVEN,
67
+ tool_choice: NotGivenOr[ToolChoice | Literal["auto", "required", "none"]] = NOT_GIVEN,
68
+ caching: NotGivenOr[Literal["ephemeral"]] = NOT_GIVEN,
83
69
  ) -> None:
84
70
  """
85
71
  Create a new instance of Anthropic LLM.
@@ -98,29 +84,25 @@ class LLM(llm.LLM):
98
84
  caching (Literal["ephemeral"] | None): If set to "ephemeral", caching will be enabled for the system prompt, tools, and chat history.
99
85
  """
100
86
 
101
- super().__init__(
102
- capabilities=LLMCapabilities(
103
- requires_persistent_functions=True,
104
- supports_choices_on_int=True,
105
- )
106
- )
87
+ super().__init__()
107
88
 
108
- # throw an error on our end
109
- api_key = api_key or os.environ.get("ANTHROPIC_API_KEY")
110
- if api_key is None:
111
- raise ValueError("Anthropic API key is required")
112
-
113
- self._opts = LLMOptions(
89
+ self._opts = _LLMOptions(
114
90
  model=model,
115
91
  user=user,
116
92
  temperature=temperature,
117
93
  parallel_tool_calls=parallel_tool_calls,
118
94
  tool_choice=tool_choice,
119
95
  caching=caching,
96
+ top_k=top_k,
97
+ max_tokens=max_tokens,
120
98
  )
121
- self._client = client or anthropic.AsyncClient(
122
- api_key=api_key,
123
- base_url=base_url,
99
+ api_key = api_key or os.environ.get("ANTHROPIC_API_KEY")
100
+ if not is_given(api_key):
101
+ raise ValueError("Anthropic API key is required")
102
+
103
+ self._client = anthropic.AsyncClient(
104
+ api_key=api_key or None,
105
+ base_url=base_url or None,
124
106
  http_client=httpx.AsyncClient(
125
107
  timeout=5.0,
126
108
  follow_redirects=True,
@@ -135,42 +117,33 @@ class LLM(llm.LLM):
135
117
  def chat(
136
118
  self,
137
119
  *,
138
- chat_ctx: llm.ChatContext,
120
+ chat_ctx: ChatContext,
121
+ tools: list[FunctionTool] | None = None,
139
122
  conn_options: APIConnectOptions = DEFAULT_API_CONNECT_OPTIONS,
140
- fnc_ctx: llm.FunctionContext | None = None,
141
- temperature: float | None = None,
142
- n: int | None = 1,
143
- parallel_tool_calls: bool | None = None,
144
- tool_choice: Union[ToolChoice, Literal["auto", "required", "none"]]
145
- | None = None,
146
- ) -> "LLMStream":
147
- if temperature is None:
148
- temperature = self._opts.temperature
149
- if parallel_tool_calls is None:
150
- parallel_tool_calls = self._opts.parallel_tool_calls
151
- if tool_choice is None:
152
- tool_choice = self._opts.tool_choice
153
-
154
- opts: dict[str, Any] = dict()
155
- if fnc_ctx and len(fnc_ctx.ai_functions) > 0:
156
- fncs_desc: list[anthropic.types.ToolParam] = []
157
- for i, fnc in enumerate(fnc_ctx.ai_functions.values()):
158
- # caching last tool will cache all the tools if caching is enabled
159
- cache_ctrl = (
160
- CACHE_CONTROL_EPHEMERAL
161
- if (i == len(fnc_ctx.ai_functions) - 1)
162
- and self._opts.caching == "ephemeral"
163
- else None
164
- )
165
- fncs_desc.append(
166
- _build_function_description(
167
- fnc,
168
- cache_ctrl=cache_ctrl,
169
- )
170
- )
123
+ parallel_tool_calls: NotGivenOr[bool] = NOT_GIVEN,
124
+ tool_choice: NotGivenOr[ToolChoice | Literal["auto", "required", "none"]] = NOT_GIVEN,
125
+ extra_kwargs: NotGivenOr[dict[str, Any]] = NOT_GIVEN,
126
+ ) -> LLMStream:
127
+ extra = {}
128
+
129
+ if is_given(extra_kwargs):
130
+ extra.update(extra_kwargs)
131
+
132
+ if is_given(self._opts.user):
133
+ extra["user"] = self._opts.user
134
+
135
+ if is_given(self._opts.temperature):
136
+ extra["temperature"] = self._opts.temperature
137
+
138
+ if is_given(self._opts.top_k):
139
+ extra["top_k"] = self._opts.top_k
171
140
 
172
- opts["tools"] = fncs_desc
173
- if tool_choice is not None:
141
+ extra["max_tokens"] = self._opts.max_tokens if is_given(self._opts.max_tokens) else 1024
142
+
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
146
+ if is_given(tool_choice):
174
147
  anthropic_tool_choice: dict[str, Any] | None = {"type": "auto"}
175
148
  if isinstance(tool_choice, ToolChoice):
176
149
  if tool_choice.type == "function":
@@ -182,41 +155,35 @@ class LLM(llm.LLM):
182
155
  if tool_choice == "required":
183
156
  anthropic_tool_choice = {"type": "any"}
184
157
  elif tool_choice == "none":
185
- opts["tools"] = []
158
+ extra["tools"] = []
186
159
  anthropic_tool_choice = None
187
- if anthropic_tool_choice is not None:
188
- if parallel_tool_calls is False:
189
- anthropic_tool_choice["disable_parallel_tool_use"] = True
190
- opts["tool_choice"] = anthropic_tool_choice
160
+ if anthropic_tool_choice is not None:
161
+ parallel_tool_calls = (
162
+ parallel_tool_calls
163
+ if is_given(parallel_tool_calls)
164
+ else self._opts.parallel_tool_calls
165
+ )
166
+ if is_given(parallel_tool_calls):
167
+ anthropic_tool_choice["disable_parallel_tool_use"] = not parallel_tool_calls
168
+ extra["tool_choice"] = anthropic_tool_choice
191
169
 
192
- latest_system_message: anthropic.types.TextBlockParam | None = (
193
- _latest_system_message(chat_ctx, caching=self._opts.caching)
194
- )
195
- if latest_system_message:
196
- opts["system"] = [latest_system_message]
170
+ anthropic_ctx, system_message = to_chat_ctx(chat_ctx, id(self), caching=self._opts.caching)
197
171
 
198
- anthropic_ctx = _build_anthropic_context(
199
- chat_ctx.messages,
200
- id(self),
201
- caching=self._opts.caching,
202
- )
203
- collaped_anthropic_ctx = _merge_messages(anthropic_ctx)
172
+ if system_message:
173
+ extra["system"] = [system_message]
204
174
 
205
175
  stream = self._client.messages.create(
206
- max_tokens=opts.get("max_tokens", 1024),
207
- messages=collaped_anthropic_ctx,
176
+ messages=anthropic_ctx,
208
177
  model=self._opts.model,
209
- temperature=temperature or anthropic.NOT_GIVEN,
210
- top_k=n or anthropic.NOT_GIVEN,
211
178
  stream=True,
212
- **opts,
179
+ **extra,
213
180
  )
214
181
 
215
182
  return LLMStream(
216
183
  self,
217
184
  anthropic_stream=stream,
218
185
  chat_ctx=chat_ctx,
219
- fnc_ctx=fnc_ctx,
186
+ tools=tools,
220
187
  conn_options=conn_options,
221
188
  )
222
189
 
@@ -226,16 +193,12 @@ class LLMStream(llm.LLMStream):
226
193
  self,
227
194
  llm: LLM,
228
195
  *,
229
- anthropic_stream: Awaitable[
230
- anthropic.AsyncStream[anthropic.types.RawMessageStreamEvent]
231
- ],
196
+ anthropic_stream: Awaitable[anthropic.AsyncStream[anthropic.types.RawMessageStreamEvent]],
232
197
  chat_ctx: llm.ChatContext,
233
- fnc_ctx: llm.FunctionContext | None,
198
+ tools: list[FunctionTool] | None,
234
199
  conn_options: APIConnectOptions,
235
200
  ) -> None:
236
- super().__init__(
237
- llm, chat_ctx=chat_ctx, fnc_ctx=fnc_ctx, conn_options=conn_options
238
- )
201
+ super().__init__(llm, chat_ctx=chat_ctx, tools=tools, conn_options=conn_options)
239
202
  self._awaitable_anthropic_stream = anthropic_stream
240
203
  self._anthropic_stream: (
241
204
  anthropic.AsyncStream[anthropic.types.RawMessageStreamEvent] | None
@@ -268,7 +231,7 @@ class LLMStream(llm.LLMStream):
268
231
 
269
232
  self._event_ch.send_nowait(
270
233
  llm.ChatChunk(
271
- request_id=self._request_id,
234
+ id=self._request_id,
272
235
  usage=llm.CompletionUsage(
273
236
  completion_tokens=self._output_tokens,
274
237
  prompt_tokens=self._input_tokens,
@@ -281,29 +244,25 @@ class LLMStream(llm.LLMStream):
281
244
  ),
282
245
  )
283
246
  )
284
- except anthropic.APITimeoutError:
285
- raise APITimeoutError(retryable=retryable)
247
+ except anthropic.APITimeoutError as e:
248
+ raise APITimeoutError(retryable=retryable) from e
286
249
  except anthropic.APIStatusError as e:
287
250
  raise APIStatusError(
288
251
  e.message,
289
252
  status_code=e.status_code,
290
253
  request_id=e.request_id,
291
254
  body=e.body,
292
- )
255
+ ) from e
293
256
  except Exception as e:
294
257
  raise APIConnectionError(retryable=retryable) from e
295
258
 
296
- def _parse_event(
297
- self, event: anthropic.types.RawMessageStreamEvent
298
- ) -> llm.ChatChunk | None:
259
+ def _parse_event(self, event: anthropic.types.RawMessageStreamEvent) -> llm.ChatChunk | None:
299
260
  if event.type == "message_start":
300
261
  self._request_id = event.message.id
301
262
  self._input_tokens = event.message.usage.input_tokens
302
263
  self._output_tokens = event.message.usage.output_tokens
303
264
  if event.message.usage.cache_creation_input_tokens:
304
- self._cache_creation_tokens = (
305
- event.message.usage.cache_creation_input_tokens
306
- )
265
+ self._cache_creation_tokens = event.message.usage.cache_creation_input_tokens
307
266
  if event.message.usage.cache_read_input_tokens:
308
267
  self._cache_read_tokens = event.message.usage.cache_read_input_tokens
309
268
  elif event.type == "message_delta":
@@ -318,7 +277,7 @@ class LLMStream(llm.LLMStream):
318
277
  if delta.type == "text_delta":
319
278
  text = delta.text
320
279
 
321
- if self._fnc_ctx is not None:
280
+ if self._tools is not None:
322
281
  # anthropic may inject COC when using functions
323
282
  if text.startswith("<thinking>"):
324
283
  self._ignoring_cot = True
@@ -330,306 +289,32 @@ class LLMStream(llm.LLMStream):
330
289
  return None
331
290
 
332
291
  return llm.ChatChunk(
333
- request_id=self._request_id,
334
- choices=[
335
- llm.Choice(
336
- delta=llm.ChoiceDelta(content=text, role="assistant")
337
- )
338
- ],
292
+ id=self._request_id,
293
+ delta=llm.ChoiceDelta(content=text, role="assistant"),
339
294
  )
340
295
  elif delta.type == "input_json_delta":
341
296
  assert self._fnc_raw_arguments is not None
342
297
  self._fnc_raw_arguments += delta.partial_json
343
298
 
344
299
  elif event.type == "content_block_stop":
345
- if self._tool_call_id is not None and self._fnc_ctx:
300
+ if self._tool_call_id is not None:
346
301
  assert self._fnc_name is not None
347
302
  assert self._fnc_raw_arguments is not None
348
303
 
349
- fnc_info = _create_ai_function_info(
350
- self._fnc_ctx,
351
- self._tool_call_id,
352
- self._fnc_name,
353
- self._fnc_raw_arguments,
354
- )
355
- self._function_calls_info.append(fnc_info)
356
-
357
304
  chat_chunk = llm.ChatChunk(
358
- request_id=self._request_id,
359
- choices=[
360
- llm.Choice(
361
- delta=llm.ChoiceDelta(
362
- role="assistant", tool_calls=[fnc_info]
363
- ),
364
- )
365
- ],
305
+ id=self._request_id,
306
+ delta=llm.ChoiceDelta(
307
+ role="assistant",
308
+ tool_calls=[
309
+ llm.FunctionToolCall(
310
+ arguments=self._fnc_raw_arguments or "",
311
+ name=self._fnc_name or "",
312
+ call_id=self._tool_call_id or "",
313
+ )
314
+ ],
315
+ ),
366
316
  )
367
317
  self._tool_call_id = self._fnc_raw_arguments = self._fnc_name = None
368
318
  return chat_chunk
369
319
 
370
320
  return None
371
-
372
-
373
- def _latest_system_message(
374
- chat_ctx: llm.ChatContext, caching: Literal["ephemeral"] | None = None
375
- ) -> anthropic.types.TextBlockParam | None:
376
- latest_system_message: llm.ChatMessage | None = None
377
- for m in chat_ctx.messages:
378
- if m.role == "system":
379
- latest_system_message = m
380
- continue
381
-
382
- latest_system_str = ""
383
- if latest_system_message:
384
- if isinstance(latest_system_message.content, str):
385
- latest_system_str = latest_system_message.content
386
- elif isinstance(latest_system_message.content, list):
387
- latest_system_str = " ".join(
388
- [c for c in latest_system_message.content if isinstance(c, str)]
389
- )
390
- if latest_system_str:
391
- system_text_block = anthropic.types.TextBlockParam(
392
- text=latest_system_str,
393
- type="text",
394
- cache_control=CACHE_CONTROL_EPHEMERAL if caching == "ephemeral" else None,
395
- )
396
- return system_text_block
397
- return None
398
-
399
-
400
- def _merge_messages(
401
- messages: List[anthropic.types.MessageParam],
402
- ) -> List[anthropic.types.MessageParam]:
403
- # Anthropic enforces alternating messages
404
- combined_messages: list[anthropic.types.MessageParam] = []
405
- for m in messages:
406
- if len(combined_messages) == 0 or m["role"] != combined_messages[-1]["role"]:
407
- combined_messages.append(m)
408
- continue
409
- last_message = combined_messages[-1]
410
- if not isinstance(last_message["content"], list) or not isinstance(
411
- m["content"], list
412
- ):
413
- logger.error("message content is not a list")
414
- continue
415
-
416
- last_message["content"].extend(m["content"])
417
-
418
- if len(combined_messages) == 0 or combined_messages[0]["role"] != "user":
419
- combined_messages.insert(
420
- 0, {"role": "user", "content": [{"type": "text", "text": "(empty)"}]}
421
- )
422
-
423
- return combined_messages
424
-
425
-
426
- def _build_anthropic_context(
427
- chat_ctx: List[llm.ChatMessage],
428
- cache_key: Any,
429
- caching: Literal["ephemeral"] | None,
430
- ) -> List[anthropic.types.MessageParam]:
431
- result: List[anthropic.types.MessageParam] = []
432
- for i, msg in enumerate(chat_ctx):
433
- # caching last message will cache whole chat history if caching is enabled
434
- cache_ctrl = (
435
- CACHE_CONTROL_EPHEMERAL
436
- if ((i == len(chat_ctx) - 1) and caching == "ephemeral")
437
- else None
438
- )
439
- a_msg = _build_anthropic_message(msg, cache_key, cache_ctrl=cache_ctrl)
440
-
441
- if a_msg:
442
- result.append(a_msg)
443
- return result
444
-
445
-
446
- def _build_anthropic_message(
447
- msg: llm.ChatMessage,
448
- cache_key: Any,
449
- cache_ctrl: anthropic.types.CacheControlEphemeralParam | None,
450
- ) -> anthropic.types.MessageParam | None:
451
- if msg.role == "user" or msg.role == "assistant":
452
- a_msg: anthropic.types.MessageParam = {
453
- "role": msg.role,
454
- "content": [],
455
- }
456
- assert isinstance(a_msg["content"], list)
457
- a_content = a_msg["content"]
458
-
459
- # add content if provided
460
- if isinstance(msg.content, str) and msg.content:
461
- a_msg["content"].append(
462
- anthropic.types.TextBlockParam(
463
- text=msg.content,
464
- type="text",
465
- cache_control=cache_ctrl,
466
- )
467
- )
468
- elif isinstance(msg.content, list):
469
- for cnt in msg.content:
470
- if isinstance(cnt, str) and cnt:
471
- content: anthropic.types.TextBlockParam = (
472
- anthropic.types.TextBlockParam(
473
- text=cnt,
474
- type="text",
475
- cache_control=cache_ctrl,
476
- )
477
- )
478
- a_content.append(content)
479
- elif isinstance(cnt, llm.ChatImage):
480
- a_content.append(
481
- _build_anthropic_image_content(cnt, cache_key, cache_ctrl)
482
- )
483
- if msg.tool_calls is not None:
484
- for fnc in msg.tool_calls:
485
- tool_use = anthropic.types.ToolUseBlockParam(
486
- id=fnc.tool_call_id,
487
- type="tool_use",
488
- name=fnc.function_info.name,
489
- input=fnc.arguments,
490
- cache_control=cache_ctrl,
491
- )
492
- a_content.append(tool_use)
493
-
494
- return a_msg
495
- elif msg.role == "tool":
496
- if isinstance(msg.content, dict):
497
- msg.content = json.dumps(msg.content)
498
- if not isinstance(msg.content, str):
499
- logger.warning("tool message content is not a string or dict")
500
- return None
501
- if not msg.tool_call_id:
502
- return None
503
-
504
- u_content = anthropic.types.ToolResultBlockParam(
505
- tool_use_id=msg.tool_call_id,
506
- type="tool_result",
507
- content=msg.content,
508
- is_error=msg.tool_exception is not None,
509
- cache_control=cache_ctrl,
510
- )
511
- return {
512
- "role": "user",
513
- "content": [u_content],
514
- }
515
-
516
- return None
517
-
518
-
519
- def _build_anthropic_image_content(
520
- image: llm.ChatImage,
521
- cache_key: Any,
522
- cache_ctrl: anthropic.types.CacheControlEphemeralParam | None,
523
- ) -> anthropic.types.ImageBlockParam:
524
- if isinstance(image.image, str): # image is a URL
525
- if not image.image.startswith("data:"):
526
- raise ValueError("LiveKit Anthropic Plugin: Image URLs must be data URLs")
527
-
528
- try:
529
- header, b64_data = image.image.split(",", 1)
530
- media_type = header.split(";")[0].split(":")[1]
531
-
532
- supported_types = {"image/jpeg", "image/png", "image/webp", "image/gif"}
533
- if media_type not in supported_types:
534
- raise ValueError(
535
- f"LiveKit Anthropic Plugin: Unsupported media type {media_type}. Must be jpeg, png, webp, or gif"
536
- )
537
-
538
- return {
539
- "type": "image",
540
- "source": {
541
- "type": "base64",
542
- "data": b64_data,
543
- "media_type": cast(
544
- Literal["image/jpeg", "image/png", "image/gif", "image/webp"],
545
- media_type,
546
- ),
547
- },
548
- "cache_control": cache_ctrl,
549
- }
550
- except (ValueError, IndexError) as e:
551
- raise ValueError(
552
- f"LiveKit Anthropic Plugin: Invalid image data URL {str(e)}"
553
- )
554
- elif isinstance(image.image, rtc.VideoFrame): # image is a VideoFrame
555
- if cache_key not in image._cache:
556
- # inside our internal implementation, we allow to put extra metadata to
557
- # each ChatImage (avoid to reencode each time we do a chatcompletion request)
558
- opts = utils.images.EncodeOptions()
559
- if image.inference_width and image.inference_height:
560
- opts.resize_options = utils.images.ResizeOptions(
561
- width=image.inference_width,
562
- height=image.inference_height,
563
- strategy="scale_aspect_fit",
564
- )
565
-
566
- encoded_data = utils.images.encode(image.image, opts)
567
- image._cache[cache_key] = base64.b64encode(encoded_data).decode("utf-8")
568
-
569
- return {
570
- "type": "image",
571
- "source": {
572
- "type": "base64",
573
- "data": image._cache[cache_key],
574
- "media_type": "image/jpeg",
575
- },
576
- "cache_control": cache_ctrl,
577
- }
578
-
579
- raise ValueError(
580
- "LiveKit Anthropic Plugin: ChatImage must be an rtc.VideoFrame or a data URL"
581
- )
582
-
583
-
584
- def _build_function_description(
585
- fnc_info: llm.function_context.FunctionInfo,
586
- cache_ctrl: anthropic.types.CacheControlEphemeralParam | None,
587
- ) -> anthropic.types.ToolParam:
588
- def build_schema_field(arg_info: llm.function_context.FunctionArgInfo):
589
- def type2str(t: type) -> str:
590
- if t is str:
591
- return "string"
592
- elif t in (int, float):
593
- return "number"
594
- elif t is bool:
595
- return "boolean"
596
-
597
- raise ValueError(f"unsupported type {t} for ai_property")
598
-
599
- p: dict[str, Any] = {}
600
- if arg_info.default is inspect.Parameter.empty:
601
- p["required"] = True
602
- else:
603
- p["required"] = False
604
-
605
- if arg_info.description:
606
- p["description"] = arg_info.description
607
-
608
- _, inner_th = _is_optional_type(arg_info.type)
609
-
610
- if get_origin(inner_th) is list:
611
- inner_type = get_args(inner_th)[0]
612
- p["type"] = "array"
613
- p["items"] = {}
614
- p["items"]["type"] = type2str(inner_type)
615
-
616
- if arg_info.choices:
617
- p["items"]["enum"] = arg_info.choices
618
- else:
619
- p["type"] = type2str(inner_th)
620
- if arg_info.choices:
621
- p["enum"] = arg_info.choices
622
-
623
- return p
624
-
625
- input_schema: dict[str, object] = {"type": "object"}
626
-
627
- for arg_info in fnc_info.arguments.values():
628
- input_schema[arg_info.name] = build_schema_field(arg_info)
629
-
630
- return anthropic.types.ToolParam(
631
- name=fnc_info.name,
632
- description=fnc_info.description,
633
- input_schema=input_schema,
634
- cache_control=cache_ctrl,
635
- )
@@ -0,0 +1,140 @@
1
+ import base64
2
+ import json
3
+ from typing import Any, Literal
4
+
5
+ import anthropic
6
+ from livekit.agents import llm
7
+ from livekit.agents.llm import FunctionTool
8
+
9
+ CACHE_CONTROL_EPHEMERAL = anthropic.types.CacheControlEphemeralParam(type="ephemeral")
10
+
11
+ __all__ = ["to_fnc_ctx", "to_chat_ctx"]
12
+
13
+
14
+ def to_fnc_ctx(
15
+ fncs: list[FunctionTool], caching: Literal["ephemeral"] | None
16
+ ) -> list[anthropic.types.ToolParam]:
17
+ 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 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
64
+
65
+ if msg.type == "message":
66
+ for c in msg.content:
67
+ if 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
+ )
94
+
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", content=[anthropic.types.TextBlockParam(text="(empty)", type="text")]
104
+ ),
105
+ )
106
+
107
+ return messages, system_message
108
+
109
+
110
+ def _to_image_content(
111
+ image: llm.ImageContent,
112
+ cache_key: Any,
113
+ cache_ctrl: anthropic.types.CacheControlEphemeralParam | None,
114
+ ) -> anthropic.types.ImageBlockParam:
115
+ img = llm.utils.serialize_image(image)
116
+ if cache_key not in image._cache:
117
+ image._cache[cache_key] = img.data_bytes
118
+ b64_data = base64.b64encode(image._cache[cache_key]).decode("utf-8")
119
+ return {
120
+ "type": "image",
121
+ "source": {
122
+ "type": "base64",
123
+ "data": f"data:{img.media_type};base64,{b64_data}",
124
+ "media_type": img.media_type,
125
+ },
126
+ "cache_control": cache_ctrl,
127
+ }
128
+
129
+
130
+ def _build_anthropic_schema(
131
+ function_tool: FunctionTool,
132
+ cache_ctrl: anthropic.types.CacheControlEphemeralParam | None = None,
133
+ ) -> anthropic.types.ToolParam:
134
+ fnc = llm.utils.build_legacy_openai_schema(function_tool, internally_tagged=True)
135
+ return anthropic.types.ToolParam(
136
+ name=fnc["name"],
137
+ description=fnc["description"] or "",
138
+ input_schema=fnc["parameters"],
139
+ cache_control=cache_ctrl,
140
+ )
@@ -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.13"
15
+ __version__ = "1.0.0.dev5"
@@ -1,36 +1,26 @@
1
- Metadata-Version: 2.2
1
+ Metadata-Version: 2.4
2
2
  Name: livekit-plugins-anthropic
3
- Version: 0.2.13
3
+ Version: 1.0.0.dev5
4
4
  Summary: Agent Framework plugin for services from Anthropic
5
- Home-page: https://github.com/livekit/agents
6
- License: Apache-2.0
7
5
  Project-URL: Documentation, https://docs.livekit.io
8
6
  Project-URL: Website, https://livekit.io/
9
7
  Project-URL: Source, https://github.com/livekit/agents
10
- Keywords: webrtc,realtime,audio,video,livekit
8
+ Author-email: LiveKit <support@livekit.io>
9
+ License-Expression: Apache-2.0
10
+ Keywords: audio,livekit,realtime,video,webrtc
11
11
  Classifier: Intended Audience :: Developers
12
12
  Classifier: License :: OSI Approved :: Apache Software License
13
- Classifier: Topic :: Multimedia :: Sound/Audio
14
- Classifier: Topic :: Multimedia :: Video
15
- Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
16
13
  Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3 :: Only
17
15
  Classifier: Programming Language :: Python :: 3.9
18
16
  Classifier: Programming Language :: Python :: 3.10
19
- Classifier: Programming Language :: Python :: 3 :: Only
17
+ Classifier: Topic :: Multimedia :: Sound/Audio
18
+ Classifier: Topic :: Multimedia :: Video
19
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
20
20
  Requires-Python: >=3.9.0
21
- Description-Content-Type: text/markdown
22
- Requires-Dist: livekit-agents<1.0.0,>=0.12.16
23
21
  Requires-Dist: anthropic>=0.34
24
- Dynamic: classifier
25
- Dynamic: description
26
- Dynamic: description-content-type
27
- Dynamic: home-page
28
- Dynamic: keywords
29
- Dynamic: license
30
- Dynamic: project-url
31
- Dynamic: requires-dist
32
- Dynamic: requires-python
33
- Dynamic: summary
22
+ Requires-Dist: livekit-agents>=1.0.0.dev5
23
+ Description-Content-Type: text/markdown
34
24
 
35
25
  # LiveKit Plugins Anthropic
36
26
 
@@ -0,0 +1,10 @@
1
+ livekit/plugins/anthropic/__init__.py,sha256=1WCyNEaR6qBsX54qJQM0SeY-QHIucww16PLXcSnMqRo,1175
2
+ livekit/plugins/anthropic/llm.py,sha256=T22doOXWfmw-xSbY2Ts6jCQxENOA9pQTcbPQn-_3Eao,12969
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=tr7lQwIWP5hzNQl1XE6L91htsd7x8fPzfXfh9eVoWH0,4939
7
+ livekit/plugins/anthropic/version.py,sha256=pXgCpV03nQI-5Kk-74NFyAdw1htj2cx6unwQHipEcfE,605
8
+ livekit_plugins_anthropic-1.0.0.dev5.dist-info/METADATA,sha256=N8A6WjOyZxgFXr3LQeBhfA6LEkZEtwOIWKeqV0XoA6s,1283
9
+ livekit_plugins_anthropic-1.0.0.dev5.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
10
+ livekit_plugins_anthropic-1.0.0.dev5.dist-info/RECORD,,
@@ -1,5 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (75.8.2)
2
+ Generator: hatchling 1.27.0
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
-
@@ -1,10 +0,0 @@
1
- livekit/plugins/anthropic/__init__.py,sha256=1WCyNEaR6qBsX54qJQM0SeY-QHIucww16PLXcSnMqRo,1175
2
- livekit/plugins/anthropic/llm.py,sha256=dtIA1qWxMPWFxG4QbAeQ-xztmJZxRxBzYxqLFty59dA,23374
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/version.py,sha256=DC7zV-PQt-EaJK0DNm_kMuF_1mO7oPlaMVvGD0eDMwA,601
7
- livekit_plugins_anthropic-0.2.13.dist-info/METADATA,sha256=M4I1liRz8JcHrEGBAIKqKGzm49s9_6BNLHCzVTFxzsk,1489
8
- livekit_plugins_anthropic-0.2.13.dist-info/WHEEL,sha256=jB7zZ3N9hIM9adW7qlTAyycLYW9npaWKLRzaoVcLKcM,91
9
- livekit_plugins_anthropic-0.2.13.dist-info/top_level.txt,sha256=OoDok3xUmXbZRvOrfvvXB-Juu4DX79dlq188E19YHoo,8
10
- livekit_plugins_anthropic-0.2.13.dist-info/RECORD,,
@@ -1 +0,0 @@
1
- livekit