arize-phoenix 5.6.0__py3-none-any.whl → 5.7.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.

Potentially problematic release.


This version of arize-phoenix might be problematic. Click here for more details.

Files changed (34) hide show
  1. {arize_phoenix-5.6.0.dist-info → arize_phoenix-5.7.0.dist-info}/METADATA +2 -2
  2. {arize_phoenix-5.6.0.dist-info → arize_phoenix-5.7.0.dist-info}/RECORD +34 -25
  3. phoenix/config.py +42 -0
  4. phoenix/server/api/helpers/playground_clients.py +671 -0
  5. phoenix/server/api/helpers/playground_registry.py +70 -0
  6. phoenix/server/api/helpers/playground_spans.py +325 -0
  7. phoenix/server/api/input_types/ChatCompletionInput.py +38 -0
  8. phoenix/server/api/input_types/GenerativeModelInput.py +17 -0
  9. phoenix/server/api/input_types/InvocationParameters.py +156 -13
  10. phoenix/server/api/input_types/TemplateOptions.py +10 -0
  11. phoenix/server/api/mutations/__init__.py +4 -0
  12. phoenix/server/api/mutations/chat_mutations.py +374 -0
  13. phoenix/server/api/queries.py +41 -52
  14. phoenix/server/api/schema.py +42 -10
  15. phoenix/server/api/subscriptions.py +326 -595
  16. phoenix/server/api/types/ChatCompletionSubscriptionPayload.py +44 -0
  17. phoenix/server/api/types/GenerativeProvider.py +27 -3
  18. phoenix/server/api/types/Span.py +37 -0
  19. phoenix/server/api/types/TemplateLanguage.py +9 -0
  20. phoenix/server/app.py +61 -13
  21. phoenix/server/main.py +14 -1
  22. phoenix/server/static/.vite/manifest.json +9 -9
  23. phoenix/server/static/assets/{components-C70HJiXz.js → components-Csu8UKOs.js} +114 -114
  24. phoenix/server/static/assets/{index-DLe1Oo3l.js → index-Bk5C9EA7.js} +1 -1
  25. phoenix/server/static/assets/{pages-C8-Sl7JI.js → pages-UeWaKXNs.js} +328 -268
  26. phoenix/server/templates/index.html +1 -0
  27. phoenix/services.py +4 -0
  28. phoenix/session/session.py +15 -1
  29. phoenix/utilities/template_formatters.py +11 -1
  30. phoenix/version.py +1 -1
  31. {arize_phoenix-5.6.0.dist-info → arize_phoenix-5.7.0.dist-info}/WHEEL +0 -0
  32. {arize_phoenix-5.6.0.dist-info → arize_phoenix-5.7.0.dist-info}/entry_points.txt +0 -0
  33. {arize_phoenix-5.6.0.dist-info → arize_phoenix-5.7.0.dist-info}/licenses/IP_NOTICE +0 -0
  34. {arize_phoenix-5.6.0.dist-info → arize_phoenix-5.7.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,671 @@
1
+ import importlib.util
2
+ from abc import ABC, abstractmethod
3
+ from collections.abc import AsyncIterator, Callable, Iterator
4
+ from typing import (
5
+ TYPE_CHECKING,
6
+ Any,
7
+ Mapping,
8
+ Optional,
9
+ Union,
10
+ )
11
+
12
+ from openinference.instrumentation import safe_json_dumps
13
+ from openinference.semconv.trace import SpanAttributes
14
+ from strawberry import UNSET
15
+ from strawberry.scalars import JSON as JSONScalarType
16
+ from typing_extensions import TypeAlias, assert_never
17
+
18
+ from phoenix.server.api.helpers.playground_registry import (
19
+ PROVIDER_DEFAULT,
20
+ register_llm_client,
21
+ )
22
+ from phoenix.server.api.input_types.GenerativeModelInput import GenerativeModelInput
23
+ from phoenix.server.api.input_types.InvocationParameters import (
24
+ BoundedFloatInvocationParameter,
25
+ CanonicalParameterName,
26
+ IntInvocationParameter,
27
+ InvocationParameter,
28
+ InvocationParameterInput,
29
+ JSONInvocationParameter,
30
+ StringListInvocationParameter,
31
+ extract_parameter,
32
+ validate_invocation_parameters,
33
+ )
34
+ from phoenix.server.api.types.ChatCompletionMessageRole import ChatCompletionMessageRole
35
+ from phoenix.server.api.types.ChatCompletionSubscriptionPayload import (
36
+ FunctionCallChunk,
37
+ TextChunk,
38
+ ToolCallChunk,
39
+ )
40
+ from phoenix.server.api.types.GenerativeProvider import GenerativeProviderKey
41
+
42
+ if TYPE_CHECKING:
43
+ from anthropic.types import MessageParam
44
+ from openai.types import CompletionUsage
45
+ from openai.types.chat import (
46
+ ChatCompletionMessageParam,
47
+ ChatCompletionMessageToolCallParam,
48
+ )
49
+
50
+ DependencyName: TypeAlias = str
51
+ SetSpanAttributesFn: TypeAlias = Callable[[Mapping[str, Any]], None]
52
+ ChatCompletionChunk: TypeAlias = Union[TextChunk, ToolCallChunk]
53
+
54
+
55
+ class PlaygroundStreamingClient(ABC):
56
+ def __init__(
57
+ self,
58
+ model: GenerativeModelInput,
59
+ api_key: Optional[str] = None,
60
+ ) -> None:
61
+ self._attributes: dict[str, Any] = dict()
62
+
63
+ @classmethod
64
+ @abstractmethod
65
+ def dependencies(cls) -> list[DependencyName]:
66
+ # A list of dependency names this client needs to run
67
+ ...
68
+
69
+ @classmethod
70
+ @abstractmethod
71
+ def supported_invocation_parameters(cls) -> list[InvocationParameter]: ...
72
+
73
+ @abstractmethod
74
+ async def chat_completion_create(
75
+ self,
76
+ messages: list[
77
+ tuple[ChatCompletionMessageRole, str, Optional[str], Optional[list[JSONScalarType]]]
78
+ ],
79
+ tools: list[JSONScalarType],
80
+ **invocation_parameters: Any,
81
+ ) -> AsyncIterator[ChatCompletionChunk]:
82
+ # a yield statement is needed to satisfy the type-checker
83
+ # https://mypy.readthedocs.io/en/stable/more_types.html#asynchronous-iterators
84
+ yield TextChunk(content="")
85
+
86
+ @classmethod
87
+ def construct_invocation_parameters(
88
+ cls, invocation_parameters: list[InvocationParameterInput]
89
+ ) -> dict[str, Any]:
90
+ supported_params = cls.supported_invocation_parameters()
91
+ params = {param.invocation_name: param for param in supported_params}
92
+
93
+ formatted_invocation_parameters = dict()
94
+
95
+ for param_input in invocation_parameters:
96
+ invocation_name = param_input.invocation_name
97
+ if invocation_name not in params:
98
+ raise ValueError(f"Unsupported invocation parameter: {invocation_name}")
99
+
100
+ param_def = params[invocation_name]
101
+ value = extract_parameter(param_def, param_input)
102
+ if value is not UNSET:
103
+ formatted_invocation_parameters[invocation_name] = value
104
+ validate_invocation_parameters(supported_params, formatted_invocation_parameters)
105
+ return formatted_invocation_parameters
106
+
107
+ @classmethod
108
+ def dependencies_are_installed(cls) -> bool:
109
+ try:
110
+ for dependency in cls.dependencies():
111
+ if importlib.util.find_spec(dependency) is None:
112
+ return False
113
+ return True
114
+ except ValueError:
115
+ # happens in some cases if the spec is None
116
+ return False
117
+
118
+ @property
119
+ def attributes(self) -> dict[str, Any]:
120
+ return self._attributes
121
+
122
+
123
+ @register_llm_client(
124
+ provider_key=GenerativeProviderKey.OPENAI,
125
+ model_names=[
126
+ PROVIDER_DEFAULT,
127
+ "gpt-4o",
128
+ "gpt-4o-2024-08-06",
129
+ "gpt-4o-2024-05-13",
130
+ "chatgpt-4o-latest",
131
+ "gpt-4o-mini",
132
+ "gpt-4o-mini-2024-07-18",
133
+ "gpt-4-turbo",
134
+ "gpt-4-turbo-2024-04-09",
135
+ "gpt-4-turbo-preview",
136
+ "gpt-4-0125-preview",
137
+ "gpt-4-1106-preview",
138
+ "gpt-4",
139
+ "gpt-4-0613",
140
+ "gpt-3.5-turbo-0125",
141
+ "gpt-3.5-turbo",
142
+ "gpt-3.5-turbo-1106",
143
+ "gpt-3.5-turbo-instruct",
144
+ ],
145
+ )
146
+ class OpenAIStreamingClient(PlaygroundStreamingClient):
147
+ def __init__(
148
+ self,
149
+ model: GenerativeModelInput,
150
+ api_key: Optional[str] = None,
151
+ ) -> None:
152
+ from openai import AsyncOpenAI
153
+
154
+ super().__init__(model=model, api_key=api_key)
155
+ self.client = AsyncOpenAI(api_key=api_key)
156
+ self.model_name = model.name
157
+
158
+ @classmethod
159
+ def dependencies(cls) -> list[DependencyName]:
160
+ return ["openai"]
161
+
162
+ @classmethod
163
+ def supported_invocation_parameters(cls) -> list[InvocationParameter]:
164
+ return [
165
+ BoundedFloatInvocationParameter(
166
+ invocation_name="temperature",
167
+ canonical_name=CanonicalParameterName.TEMPERATURE,
168
+ label="Temperature",
169
+ default_value=0.0,
170
+ min_value=0.0,
171
+ max_value=2.0,
172
+ ),
173
+ IntInvocationParameter(
174
+ invocation_name="max_tokens",
175
+ canonical_name=CanonicalParameterName.MAX_COMPLETION_TOKENS,
176
+ label="Max Tokens",
177
+ default_value=UNSET,
178
+ ),
179
+ BoundedFloatInvocationParameter(
180
+ invocation_name="frequency_penalty",
181
+ label="Frequency Penalty",
182
+ default_value=UNSET,
183
+ min_value=-2.0,
184
+ max_value=2.0,
185
+ ),
186
+ BoundedFloatInvocationParameter(
187
+ invocation_name="presence_penalty",
188
+ label="Presence Penalty",
189
+ default_value=UNSET,
190
+ min_value=-2.0,
191
+ max_value=2.0,
192
+ ),
193
+ StringListInvocationParameter(
194
+ invocation_name="stop",
195
+ canonical_name=CanonicalParameterName.STOP_SEQUENCES,
196
+ label="Stop Sequences",
197
+ default_value=UNSET,
198
+ ),
199
+ BoundedFloatInvocationParameter(
200
+ invocation_name="top_p",
201
+ canonical_name=CanonicalParameterName.TOP_P,
202
+ label="Top P",
203
+ default_value=UNSET,
204
+ min_value=0.0,
205
+ max_value=1.0,
206
+ ),
207
+ IntInvocationParameter(
208
+ invocation_name="seed",
209
+ canonical_name=CanonicalParameterName.RANDOM_SEED,
210
+ label="Seed",
211
+ default_value=UNSET,
212
+ ),
213
+ JSONInvocationParameter(
214
+ invocation_name="tool_choice",
215
+ label="Tool Choice",
216
+ canonical_name=CanonicalParameterName.TOOL_CHOICE,
217
+ default_value=UNSET,
218
+ hidden=True,
219
+ ),
220
+ JSONInvocationParameter(
221
+ invocation_name="response_format",
222
+ label="Response Format",
223
+ canonical_name=CanonicalParameterName.RESPONSE_FORMAT,
224
+ default_value=UNSET,
225
+ ),
226
+ ]
227
+
228
+ async def chat_completion_create(
229
+ self,
230
+ messages: list[
231
+ tuple[ChatCompletionMessageRole, str, Optional[str], Optional[list[JSONScalarType]]]
232
+ ],
233
+ tools: list[JSONScalarType],
234
+ **invocation_parameters: Any,
235
+ ) -> AsyncIterator[ChatCompletionChunk]:
236
+ from openai import NOT_GIVEN
237
+ from openai.types.chat import ChatCompletionStreamOptionsParam
238
+
239
+ # Convert standard messages to OpenAI messages
240
+ openai_messages = [self.to_openai_chat_completion_param(*message) for message in messages]
241
+ tool_call_ids: dict[int, str] = {}
242
+ token_usage: Optional["CompletionUsage"] = None
243
+ async for chunk in await self.client.chat.completions.create(
244
+ messages=openai_messages,
245
+ model=self.model_name,
246
+ stream=True,
247
+ stream_options=ChatCompletionStreamOptionsParam(include_usage=True),
248
+ tools=tools or NOT_GIVEN,
249
+ **invocation_parameters,
250
+ ):
251
+ if (usage := chunk.usage) is not None:
252
+ token_usage = usage
253
+ continue
254
+ choice = chunk.choices[0]
255
+ delta = choice.delta
256
+ if choice.finish_reason is None:
257
+ if isinstance(chunk_content := delta.content, str):
258
+ text_chunk = TextChunk(content=chunk_content)
259
+ yield text_chunk
260
+ if (tool_calls := delta.tool_calls) is not None:
261
+ for tool_call_index, tool_call in enumerate(tool_calls):
262
+ tool_call_id = (
263
+ tool_call.id
264
+ if tool_call.id is not None
265
+ else tool_call_ids[tool_call_index]
266
+ )
267
+ tool_call_ids[tool_call_index] = tool_call_id
268
+ if (function := tool_call.function) is not None:
269
+ tool_call_chunk = ToolCallChunk(
270
+ id=tool_call_id,
271
+ function=FunctionCallChunk(
272
+ name=function.name or "",
273
+ arguments=function.arguments or "",
274
+ ),
275
+ )
276
+ yield tool_call_chunk
277
+ if token_usage is not None:
278
+ self._attributes.update(dict(self._llm_token_counts(token_usage)))
279
+
280
+ def to_openai_chat_completion_param(
281
+ self,
282
+ role: ChatCompletionMessageRole,
283
+ content: JSONScalarType,
284
+ tool_call_id: Optional[str] = None,
285
+ tool_calls: Optional[list[JSONScalarType]] = None,
286
+ ) -> "ChatCompletionMessageParam":
287
+ from openai.types.chat import (
288
+ ChatCompletionAssistantMessageParam,
289
+ ChatCompletionSystemMessageParam,
290
+ ChatCompletionToolMessageParam,
291
+ ChatCompletionUserMessageParam,
292
+ )
293
+
294
+ if role is ChatCompletionMessageRole.USER:
295
+ return ChatCompletionUserMessageParam(
296
+ {
297
+ "content": content,
298
+ "role": "user",
299
+ }
300
+ )
301
+ if role is ChatCompletionMessageRole.SYSTEM:
302
+ return ChatCompletionSystemMessageParam(
303
+ {
304
+ "content": content,
305
+ "role": "system",
306
+ }
307
+ )
308
+ if role is ChatCompletionMessageRole.AI:
309
+ if tool_calls is None:
310
+ return ChatCompletionAssistantMessageParam(
311
+ {
312
+ "content": content,
313
+ "role": "assistant",
314
+ }
315
+ )
316
+ else:
317
+ return ChatCompletionAssistantMessageParam(
318
+ {
319
+ "content": content,
320
+ "role": "assistant",
321
+ "tool_calls": [
322
+ self.to_openai_tool_call_param(tool_call) for tool_call in tool_calls
323
+ ],
324
+ }
325
+ )
326
+ if role is ChatCompletionMessageRole.TOOL:
327
+ if tool_call_id is None:
328
+ raise ValueError("tool_call_id is required for tool messages")
329
+ return ChatCompletionToolMessageParam(
330
+ {"content": content, "role": "tool", "tool_call_id": tool_call_id}
331
+ )
332
+ assert_never(role)
333
+
334
+ def to_openai_tool_call_param(
335
+ self,
336
+ tool_call: JSONScalarType,
337
+ ) -> "ChatCompletionMessageToolCallParam":
338
+ from openai.types.chat import ChatCompletionMessageToolCallParam
339
+
340
+ return ChatCompletionMessageToolCallParam(
341
+ id=tool_call.get("id", ""),
342
+ function={
343
+ "name": tool_call.get("function", {}).get("name", ""),
344
+ "arguments": safe_json_dumps(tool_call.get("function", {}).get("arguments", "")),
345
+ },
346
+ type="function",
347
+ )
348
+
349
+ @staticmethod
350
+ def _llm_token_counts(usage: "CompletionUsage") -> Iterator[tuple[str, Any]]:
351
+ yield LLM_TOKEN_COUNT_PROMPT, usage.prompt_tokens
352
+ yield LLM_TOKEN_COUNT_COMPLETION, usage.completion_tokens
353
+ yield LLM_TOKEN_COUNT_TOTAL, usage.total_tokens
354
+
355
+
356
+ @register_llm_client(
357
+ provider_key=GenerativeProviderKey.OPENAI,
358
+ model_names=[
359
+ "o1-preview",
360
+ "o1-preview-2024-09-12",
361
+ "o1-mini",
362
+ "o1-mini-2024-09-12",
363
+ ],
364
+ )
365
+ class OpenAIO1StreamingClient(OpenAIStreamingClient):
366
+ @classmethod
367
+ def supported_invocation_parameters(cls) -> list[InvocationParameter]:
368
+ return [
369
+ IntInvocationParameter(
370
+ invocation_name="max_completion_tokens",
371
+ canonical_name=CanonicalParameterName.MAX_COMPLETION_TOKENS,
372
+ label="Max Completion Tokens",
373
+ default_value=UNSET,
374
+ ),
375
+ IntInvocationParameter(
376
+ invocation_name="seed",
377
+ canonical_name=CanonicalParameterName.RANDOM_SEED,
378
+ label="Seed",
379
+ default_value=UNSET,
380
+ ),
381
+ JSONInvocationParameter(
382
+ invocation_name="tool_choice",
383
+ label="Tool Choice",
384
+ canonical_name=CanonicalParameterName.TOOL_CHOICE,
385
+ default_value=UNSET,
386
+ hidden=True,
387
+ ),
388
+ ]
389
+
390
+ async def chat_completion_create(
391
+ self,
392
+ messages: list[
393
+ tuple[ChatCompletionMessageRole, str, Optional[str], Optional[list[JSONScalarType]]]
394
+ ],
395
+ tools: list[JSONScalarType],
396
+ **invocation_parameters: Any,
397
+ ) -> AsyncIterator[ChatCompletionChunk]:
398
+ from openai import NOT_GIVEN
399
+
400
+ # Convert standard messages to OpenAI messages
401
+ unfiltered_openai_messages = [
402
+ self.to_openai_o1_chat_completion_param(*message) for message in messages
403
+ ]
404
+
405
+ # filter out unsupported messages
406
+ openai_messages: list[ChatCompletionMessageParam] = [
407
+ message for message in unfiltered_openai_messages if message is not None
408
+ ]
409
+
410
+ tool_call_ids: dict[int, str] = {}
411
+
412
+ response = await self.client.chat.completions.create(
413
+ messages=openai_messages,
414
+ model=self.model_name,
415
+ tools=tools or NOT_GIVEN,
416
+ **invocation_parameters,
417
+ )
418
+
419
+ choice = response.choices[0]
420
+ message = choice.message
421
+ content = message.content
422
+
423
+ text_chunk = TextChunk(content=content)
424
+ yield text_chunk
425
+
426
+ if (tool_calls := message.tool_calls) is not None:
427
+ for tool_call_index, tool_call in enumerate(tool_calls):
428
+ tool_call_id = (
429
+ tool_call.id
430
+ if tool_call.id is not None
431
+ else tool_call_ids.get(tool_call_index, f"tool_call_{tool_call_index}")
432
+ )
433
+ tool_call_ids[tool_call_index] = tool_call_id
434
+ if (function := tool_call.function) is not None:
435
+ tool_call_chunk = ToolCallChunk(
436
+ id=tool_call_id,
437
+ function=FunctionCallChunk(
438
+ name=function.name or "",
439
+ arguments=function.arguments or "",
440
+ ),
441
+ )
442
+ yield tool_call_chunk
443
+
444
+ if (usage := response.usage) is not None:
445
+ self._attributes.update(dict(self._llm_token_counts(usage)))
446
+
447
+ def to_openai_o1_chat_completion_param(
448
+ self,
449
+ role: ChatCompletionMessageRole,
450
+ content: JSONScalarType,
451
+ tool_call_id: Optional[str] = None,
452
+ tool_calls: Optional[list[JSONScalarType]] = None,
453
+ ) -> Optional["ChatCompletionMessageParam"]:
454
+ from openai.types.chat import (
455
+ ChatCompletionAssistantMessageParam,
456
+ ChatCompletionToolMessageParam,
457
+ ChatCompletionUserMessageParam,
458
+ )
459
+
460
+ if role is ChatCompletionMessageRole.USER:
461
+ return ChatCompletionUserMessageParam(
462
+ {
463
+ "content": content,
464
+ "role": "user",
465
+ }
466
+ )
467
+ if role is ChatCompletionMessageRole.SYSTEM:
468
+ return None # System messages are not supported for o1 models
469
+ if role is ChatCompletionMessageRole.AI:
470
+ if tool_calls is None:
471
+ return ChatCompletionAssistantMessageParam(
472
+ {
473
+ "content": content,
474
+ "role": "assistant",
475
+ }
476
+ )
477
+ else:
478
+ return ChatCompletionAssistantMessageParam(
479
+ {
480
+ "content": content,
481
+ "role": "assistant",
482
+ "tool_calls": [
483
+ self.to_openai_tool_call_param(tool_call) for tool_call in tool_calls
484
+ ],
485
+ }
486
+ )
487
+ if role is ChatCompletionMessageRole.TOOL:
488
+ if tool_call_id is None:
489
+ raise ValueError("tool_call_id is required for tool messages")
490
+ return ChatCompletionToolMessageParam(
491
+ {"content": content, "role": "tool", "tool_call_id": tool_call_id}
492
+ )
493
+ assert_never(role)
494
+
495
+ @staticmethod
496
+ def _llm_token_counts(usage: "CompletionUsage") -> Iterator[tuple[str, Any]]:
497
+ yield LLM_TOKEN_COUNT_PROMPT, usage.prompt_tokens
498
+ yield LLM_TOKEN_COUNT_COMPLETION, usage.completion_tokens
499
+ yield LLM_TOKEN_COUNT_TOTAL, usage.total_tokens
500
+
501
+
502
+ @register_llm_client(
503
+ provider_key=GenerativeProviderKey.AZURE_OPENAI,
504
+ model_names=[
505
+ PROVIDER_DEFAULT,
506
+ ],
507
+ )
508
+ class AzureOpenAIStreamingClient(OpenAIStreamingClient):
509
+ def __init__(
510
+ self,
511
+ model: GenerativeModelInput,
512
+ api_key: Optional[str] = None,
513
+ ):
514
+ from openai import AsyncAzureOpenAI
515
+
516
+ super().__init__(model=model, api_key=api_key)
517
+ if model.endpoint is None or model.api_version is None:
518
+ raise ValueError("endpoint and api_version are required for Azure OpenAI models")
519
+ self.client = AsyncAzureOpenAI(
520
+ api_key=api_key,
521
+ azure_endpoint=model.endpoint,
522
+ api_version=model.api_version,
523
+ )
524
+
525
+
526
+ @register_llm_client(
527
+ provider_key=GenerativeProviderKey.ANTHROPIC,
528
+ model_names=[
529
+ PROVIDER_DEFAULT,
530
+ "claude-3-5-sonnet-20240620",
531
+ "claude-3-opus-20240229",
532
+ "claude-3-sonnet-20240229",
533
+ "claude-3-haiku-20240307",
534
+ ],
535
+ )
536
+ class AnthropicStreamingClient(PlaygroundStreamingClient):
537
+ def __init__(
538
+ self,
539
+ model: GenerativeModelInput,
540
+ api_key: Optional[str] = None,
541
+ ) -> None:
542
+ import anthropic
543
+
544
+ super().__init__(model=model, api_key=api_key)
545
+ self.client = anthropic.AsyncAnthropic(api_key=api_key)
546
+ self.model_name = model.name
547
+
548
+ @classmethod
549
+ def dependencies(cls) -> list[DependencyName]:
550
+ return ["anthropic"]
551
+
552
+ @classmethod
553
+ def supported_invocation_parameters(cls) -> list[InvocationParameter]:
554
+ return [
555
+ IntInvocationParameter(
556
+ invocation_name="max_tokens",
557
+ canonical_name=CanonicalParameterName.MAX_COMPLETION_TOKENS,
558
+ label="Max Tokens",
559
+ default_value=UNSET,
560
+ required=True,
561
+ ),
562
+ BoundedFloatInvocationParameter(
563
+ invocation_name="temperature",
564
+ canonical_name=CanonicalParameterName.TEMPERATURE,
565
+ label="Temperature",
566
+ default_value=UNSET,
567
+ min_value=0.0,
568
+ max_value=1.0,
569
+ ),
570
+ StringListInvocationParameter(
571
+ invocation_name="stop_sequences",
572
+ canonical_name=CanonicalParameterName.STOP_SEQUENCES,
573
+ label="Stop Sequences",
574
+ default_value=UNSET,
575
+ ),
576
+ BoundedFloatInvocationParameter(
577
+ invocation_name="top_p",
578
+ canonical_name=CanonicalParameterName.TOP_P,
579
+ label="Top P",
580
+ default_value=UNSET,
581
+ min_value=0.0,
582
+ max_value=1.0,
583
+ ),
584
+ JSONInvocationParameter(
585
+ invocation_name="tool_choice",
586
+ label="Tool Choice",
587
+ canonical_name=CanonicalParameterName.TOOL_CHOICE,
588
+ default_value=UNSET,
589
+ hidden=True,
590
+ ),
591
+ ]
592
+
593
+ async def chat_completion_create(
594
+ self,
595
+ messages: list[
596
+ tuple[ChatCompletionMessageRole, str, Optional[str], Optional[list[JSONScalarType]]]
597
+ ],
598
+ tools: list[JSONScalarType],
599
+ **invocation_parameters: Any,
600
+ ) -> AsyncIterator[ChatCompletionChunk]:
601
+ import anthropic.lib.streaming as anthropic_streaming
602
+ import anthropic.types as anthropic_types
603
+
604
+ anthropic_messages, system_prompt = self._build_anthropic_messages(messages)
605
+
606
+ anthropic_params = {
607
+ "messages": anthropic_messages,
608
+ "model": self.model_name,
609
+ "system": system_prompt,
610
+ "max_tokens": 1024,
611
+ **invocation_parameters,
612
+ }
613
+ async with self.client.messages.stream(**anthropic_params) as stream:
614
+ async for event in stream:
615
+ if isinstance(event, anthropic_types.RawMessageStartEvent):
616
+ self._attributes.update(
617
+ {LLM_TOKEN_COUNT_PROMPT: event.message.usage.input_tokens}
618
+ )
619
+ elif isinstance(event, anthropic_streaming.TextEvent):
620
+ yield TextChunk(content=event.text)
621
+ elif isinstance(event, anthropic_streaming.MessageStopEvent):
622
+ self._attributes.update(
623
+ {LLM_TOKEN_COUNT_COMPLETION: event.message.usage.output_tokens}
624
+ )
625
+ elif isinstance(
626
+ event,
627
+ (
628
+ anthropic_types.RawContentBlockStartEvent,
629
+ anthropic_types.RawContentBlockDeltaEvent,
630
+ anthropic_types.RawMessageDeltaEvent,
631
+ anthropic_streaming.ContentBlockStopEvent,
632
+ ),
633
+ ):
634
+ # event types emitted by the stream that don't contain useful information
635
+ pass
636
+ elif isinstance(event, anthropic_streaming.InputJsonEvent):
637
+ raise NotImplementedError
638
+ else:
639
+ assert_never(event)
640
+
641
+ def _build_anthropic_messages(
642
+ self,
643
+ messages: list[tuple[ChatCompletionMessageRole, str, Optional[str], Optional[list[str]]]],
644
+ ) -> tuple[list["MessageParam"], str]:
645
+ anthropic_messages: list["MessageParam"] = []
646
+ system_prompt = ""
647
+ for role, content, _tool_call_id, _tool_calls in messages:
648
+ if role == ChatCompletionMessageRole.USER:
649
+ anthropic_messages.append({"role": "user", "content": content})
650
+ elif role == ChatCompletionMessageRole.AI:
651
+ anthropic_messages.append({"role": "assistant", "content": content})
652
+ elif role == ChatCompletionMessageRole.SYSTEM:
653
+ system_prompt += content + "\n"
654
+ elif role == ChatCompletionMessageRole.TOOL:
655
+ raise NotImplementedError
656
+ else:
657
+ assert_never(role)
658
+
659
+ return anthropic_messages, system_prompt
660
+
661
+
662
+ def initialize_playground_clients() -> None:
663
+ """
664
+ Ensure that all playground clients are registered at import time.
665
+ """
666
+ pass
667
+
668
+
669
+ LLM_TOKEN_COUNT_PROMPT = SpanAttributes.LLM_TOKEN_COUNT_PROMPT
670
+ LLM_TOKEN_COUNT_COMPLETION = SpanAttributes.LLM_TOKEN_COUNT_COMPLETION
671
+ LLM_TOKEN_COUNT_TOTAL = SpanAttributes.LLM_TOKEN_COUNT_TOTAL