Skip to content

Commit a243909

Browse files
kasarolzzwCoda-bot
andauthored
[feat][prompt] prompt as a service (ptaas) (#28)
ptaas Co-Authored-By: Coda <coda@bytedance.com>
1 parent c6316ff commit a243909

23 files changed

+2136
-41
lines changed

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,4 +34,5 @@ venv.bak/
3434
/.idea
3535
/.vscode
3636
/output
37-
dist/
37+
dist/
38+
.coda/

cozeloop/__init__.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
close,
1919
get_prompt,
2020
prompt_format,
21+
execute_prompt,
22+
aexecute_prompt,
2123
start_span,
2224
get_span_from_context,
2325
get_span_from_header,
@@ -30,4 +32,4 @@
3032
ENV_JWT_OAUTH_PUBLIC_KEY_ID
3133
)
3234

33-
from .span import SpanContext, Span
35+
from .span import SpanContext, Span

cozeloop/_client.py

Lines changed: 112 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,14 @@
77
import os
88
import threading
99
from datetime import datetime
10-
from typing import Dict, Any, List, Optional, Callable
10+
from typing import Dict, Any, List, Optional, Callable, Union
1111

1212
import httpx
1313

1414
from cozeloop.client import Client
1515
from cozeloop._noop import NOOP_SPAN, _NoopClient
16-
from cozeloop.entities.prompt import Prompt, Message, PromptVariable
16+
from cozeloop.entities.prompt import Prompt, Message, PromptVariable, ExecuteResult
17+
from cozeloop.entities.stream import StreamReader
1718
from cozeloop.internal import consts, httpclient
1819
from cozeloop.internal.consts import ClientClosedError
1920
from cozeloop.internal.httpclient import Auth
@@ -269,6 +270,62 @@ def prompt_format(self, prompt: Prompt, variables: Dict[str, PromptVariable]) ->
269270
raise ClientClosedError()
270271
return self._prompt_provider.prompt_format(prompt, variables)
271272

273+
def execute_prompt(
274+
self,
275+
prompt_key: str,
276+
*,
277+
version: Optional[str] = None,
278+
label: Optional[str] = None,
279+
variable_vals: Optional[Dict[str, Any]] = None,
280+
messages: Optional[List[Message]] = None,
281+
stream: bool = False,
282+
timeout: Optional[int] = None
283+
) -> Union[ExecuteResult, StreamReader[ExecuteResult]]:
284+
"""
285+
执行Prompt请求
286+
287+
:param timeout: 请求超时时间(秒),可选,默认为600秒(10分钟)
288+
"""
289+
if self._closed:
290+
raise ClientClosedError()
291+
return self._prompt_provider.execute_prompt(
292+
prompt_key,
293+
version=version,
294+
label=label,
295+
variable_vals=variable_vals,
296+
messages=messages,
297+
stream=stream,
298+
timeout=timeout
299+
)
300+
301+
async def aexecute_prompt(
302+
self,
303+
prompt_key: str,
304+
*,
305+
version: Optional[str] = None,
306+
label: Optional[str] = None,
307+
variable_vals: Optional[Dict[str, Any]] = None,
308+
messages: Optional[List[Message]] = None,
309+
stream: bool = False,
310+
timeout: Optional[int] = None
311+
) -> Union[ExecuteResult, StreamReader[ExecuteResult]]:
312+
"""
313+
异步执行Prompt请求
314+
315+
:param timeout: 请求超时时间(秒),可选,默认为600秒(10分钟)
316+
"""
317+
if self._closed:
318+
raise ClientClosedError()
319+
return await self._prompt_provider.aexecute_prompt(
320+
prompt_key,
321+
version=version,
322+
label=label,
323+
variable_vals=variable_vals,
324+
messages=messages,
325+
stream=stream,
326+
timeout=timeout
327+
)
328+
272329
def start_span(
273330
self,
274331
name: str,
@@ -368,6 +425,58 @@ def prompt_format(prompt: Prompt, variables: Dict[str, Any]) -> List[Message]:
368425
return get_default_client().prompt_format(prompt, variables)
369426

370427

428+
def execute_prompt(
429+
prompt_key: str,
430+
*,
431+
version: Optional[str] = None,
432+
label: Optional[str] = None,
433+
variable_vals: Optional[Dict[str, Any]] = None,
434+
messages: Optional[List[Message]] = None,
435+
stream: bool = False,
436+
timeout: Optional[int] = None
437+
) -> Union[ExecuteResult, StreamReader[ExecuteResult]]:
438+
"""
439+
执行Prompt请求
440+
441+
:param timeout: 请求超时时间(秒),可选,默认为600秒(10分钟)
442+
"""
443+
return get_default_client().execute_prompt(
444+
prompt_key,
445+
version=version,
446+
label=label,
447+
variable_vals=variable_vals,
448+
messages=messages,
449+
stream=stream,
450+
timeout=timeout
451+
)
452+
453+
454+
async def aexecute_prompt(
455+
prompt_key: str,
456+
*,
457+
version: Optional[str] = None,
458+
label: Optional[str] = None,
459+
variable_vals: Optional[Dict[str, Any]] = None,
460+
messages: Optional[List[Message]] = None,
461+
stream: bool = False,
462+
timeout: Optional[int] = None
463+
) -> Union[ExecuteResult, StreamReader[ExecuteResult]]:
464+
"""
465+
异步执行Prompt请求
466+
467+
:param timeout: 请求超时时间(秒),可选,默认为600秒(10分钟)
468+
"""
469+
return await get_default_client().aexecute_prompt(
470+
prompt_key,
471+
version=version,
472+
label=label,
473+
variable_vals=variable_vals,
474+
messages=messages,
475+
stream=stream,
476+
timeout=timeout
477+
)
478+
479+
371480
def start_span(name: str, span_type: str, *, start_time: Optional[int] = None,
372481
child_of: Optional[SpanContext] = None) -> Span:
373482
return get_default_client().start_span(name, span_type, start_time=start_time, child_of=child_of)
@@ -382,4 +491,4 @@ def get_span_from_header(header: Dict[str, str]) -> SpanContext:
382491

383492

384493
def flush() -> None:
385-
return get_default_client().flush()
494+
return get_default_client().flush()

cozeloop/_noop.py

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,11 @@
22
# SPDX-License-Identifier: MIT
33

44
import logging
5-
from typing import Dict, Optional, List
5+
from typing import Dict, Optional, List, Union, Any
66

77
from cozeloop.client import Client
8-
from cozeloop.entities.prompt import Prompt, Message, PromptVariable
8+
from cozeloop.entities.prompt import Prompt, Message, PromptVariable, ExecuteResult
9+
from cozeloop.entities.stream import StreamReader
910
from cozeloop.internal.trace.noop_span import NoopSpan
1011
from cozeloop.span import SpanContext, Span
1112

@@ -35,6 +36,32 @@ def prompt_format(self, prompt: Prompt, variables: Dict[str, PromptVariable]) ->
3536
logger.warning(f"Noop client not supported. {self.new_exception}")
3637
raise self.new_exception
3738

39+
def execute_prompt(
40+
self,
41+
prompt_key: str,
42+
*,
43+
version: Optional[str] = None,
44+
label: Optional[str] = None,
45+
variable_vals: Optional[Dict[str, Any]] = None,
46+
messages: Optional[List[Message]] = None,
47+
stream: bool = False
48+
) -> Union[ExecuteResult, StreamReader[ExecuteResult]]:
49+
logger.warning(f"Noop client not supported. {self.new_exception}")
50+
raise self.new_exception
51+
52+
async def aexecute_prompt(
53+
self,
54+
prompt_key: str,
55+
*,
56+
version: Optional[str] = None,
57+
label: Optional[str] = None,
58+
variable_vals: Optional[Dict[str, Any]] = None,
59+
messages: Optional[List[Message]] = None,
60+
stream: bool = False
61+
) -> Union[ExecuteResult, StreamReader[ExecuteResult]]:
62+
logger.warning(f"Noop client not supported. {self.new_exception}")
63+
raise self.new_exception
64+
3865
def start_span(self, name: str, span_type: str, *, start_time: Optional[int] = None,
3966
child_of: Optional[SpanContext] = None, start_new_trace: bool = False) -> Span:
4067
logger.warning(f"Noop client not supported. {self.new_exception}")
@@ -49,4 +76,4 @@ def get_span_from_header(self, header: Dict[str, str]) -> SpanContext:
4976
return NOOP_SPAN
5077

5178
def flush(self) -> None:
52-
logger.warning(f"Noop client not supported. {self.new_exception}")
79+
logger.warning(f"Noop client not supported. {self.new_exception}")

cozeloop/entities/prompt.py

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@
33

44
from enum import Enum
55
from typing import List, Optional, Union
6-
7-
from pydantic import BaseModel, Field, ConfigDict
6+
from typing import List, Optional, Union, Dict, Any
7+
from pydantic import BaseModel
88

99

1010
class TemplateType(str, Enum):
@@ -47,19 +47,36 @@ class ToolChoiceType(str, Enum):
4747
class ContentType(str, Enum):
4848
TEXT = "text"
4949
IMAGE_URL = "image_url"
50+
BASE64_DATA = "base64_data"
5051
MULTI_PART_VARIABLE = "multi_part_variable"
5152

5253

5354
class ContentPart(BaseModel):
5455
type: ContentType
5556
text: Optional[str] = None
5657
image_url: Optional[str] = None
58+
base64_data: Optional[str] = None
59+
60+
61+
class FunctionCall(BaseModel):
62+
name: str
63+
arguments: Optional[str] = None
64+
65+
66+
class ToolCall(BaseModel):
67+
index: int
68+
id: str
69+
type: ToolType
70+
function_call: Optional[FunctionCall] = None
5771

5872

5973
class Message(BaseModel):
6074
role: Role
75+
reasoning_content: Optional[str] = None
6176
content: Optional[str] = None
6277
parts: Optional[List[ContentPart]] = None
78+
tool_call_id: Optional[str] = None
79+
tool_calls: Optional[List[ToolCall]] = None
6380

6481

6582
class VariableDef(BaseModel):
@@ -109,5 +126,27 @@ class Prompt(BaseModel):
109126
llm_config: Optional[LLMConfig] = None
110127

111128

129+
class ExecuteParam(BaseModel):
130+
"""Execute参数"""
131+
prompt_key: str
132+
version: str = ""
133+
label: str = ""
134+
variable_vals: Optional[Dict[str, Any]] = None
135+
messages: Optional[List[Message]] = None
136+
137+
138+
class TokenUsage(BaseModel):
139+
"""Token使用统计"""
140+
input_tokens: int = 0
141+
output_tokens: int = 0
142+
143+
144+
class ExecuteResult(BaseModel):
145+
"""Execute结果"""
146+
message: Optional[Message] = None
147+
finish_reason: Optional[str] = None
148+
usage: Optional[TokenUsage] = None
149+
150+
112151
MessageLikeObject = Union[Message, List[Message]]
113-
PromptVariable = Union[str, MessageLikeObject]
152+
PromptVariable = Union[str, MessageLikeObject]

cozeloop/entities/stream.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
2+
# SPDX-License-Identifier: MIT
3+
4+
from abc import ABC, abstractmethod
5+
from typing import TypeVar, Generic, AsyncIterator, Iterator
6+
7+
T = TypeVar('T')
8+
9+
10+
class StreamReader(ABC, Generic[T]):
11+
"""流式读取器接口"""
12+
13+
@abstractmethod
14+
def __iter__(self) -> Iterator[T]:
15+
"""支持同步迭代 - for循环直接读取"""
16+
pass
17+
18+
@abstractmethod
19+
def __next__(self) -> T:
20+
"""支持next()函数调用"""
21+
pass
22+
23+
@abstractmethod
24+
def __aiter__(self) -> AsyncIterator[T]:
25+
"""支持异步迭代 - async for循环直接读取"""
26+
pass
27+
28+
@abstractmethod
29+
async def __anext__(self) -> T:
30+
"""支持async next()调用"""
31+
pass
32+
33+
@abstractmethod
34+
def close(self):
35+
"""关闭流"""
36+
pass

cozeloop/internal/consts/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
DEFAULT_PROMPT_CACHE_REFRESH_INTERVAL = 60
1616
DEFAULT_TIMEOUT = 3
1717
DEFAULT_UPLOAD_TIMEOUT = 30
18+
DEFAULT_PROMPT_EXECUTE_TIMEOUT = 600 # 10分钟,专用于execute_prompt和aexecute_prompt方法
1819

1920
LOG_ID_HEADER = "x-tt-logid"
2021
AUTHORIZE_HEADER = "Authorization"
@@ -118,4 +119,4 @@
118119
OUTPUT: MAX_BYTES_OF_ONE_TAG_VALUE_OF_INPUT_OUTPUT,
119120
}
120121

121-
BAGGAGE_SPECIAL_CHARS = {"=", ","}
122+
BAGGAGE_SPECIAL_CHARS = {"=", ","}

0 commit comments

Comments
 (0)