Skip to content

Commit b021371

Browse files
committed
add to_runnable decorator & lcc tool message add name and tool_call_id
1 parent 2a6eeb2 commit b021371

File tree

5 files changed

+159
-2
lines changed

5 files changed

+159
-2
lines changed

CHANGLOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
## [0.1.25] - 2026-01-22
2+
### Added
3+
- add to_runnable decorator
4+
- lcc tool message add name and tool_call_id
5+
16
## [0.1.24] - 2026-01-16
27
### Added
38
- client init set default client if not exist

cozeloop/decorator/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,4 @@
55

66
coze_loop_decorator= CozeLoopDecorator()
77
observe = coze_loop_decorator.observe
8+
to_runnable = coze_loop_decorator.to_runnable

cozeloop/decorator/decorator.py

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
from typing import Optional, Callable, Any, overload, Dict, Generic, Iterator, TypeVar, List, cast, AsyncIterator
55
from functools import wraps
66

7+
from langchain_core.runnables import RunnableLambda, RunnableConfig
8+
79
from cozeloop import Client, Span, start_span
810
from cozeloop.decorator.utils import is_async_func, is_gen_func, is_async_gen_func, is_class_func
911

@@ -312,6 +314,153 @@ async def async_stream_wrapper(*args: Any, **kwargs: Any):
312314
return decorator(func)
313315

314316

317+
def to_runnable(
318+
self,
319+
func: Callable = None,
320+
) -> Callable:
321+
"""
322+
Decorator to be RunnableLambda.
323+
324+
:param func: The function to be decorated, Requirements are as follows:
325+
1. When the func is called, parameter config(RunnableConfig) is required, you must use the config containing cozeloop callback handler of 'current request', otherwise, the trace may be lost!
326+
327+
Examples:
328+
@to_runnable
329+
def runnable_func(my_input: dict) -> str:
330+
return input
331+
332+
async def scorer_leader(state: MyState) -> dict | str:
333+
await runnable_func({"a": "111", "b": 222, "c": "333"}, config=state.config) # config is required
334+
"""
335+
336+
def decorator(func: Callable):
337+
338+
@wraps(func)
339+
def sync_wrapper(*args: Any, **kwargs: Any):
340+
config = kwargs.pop("config", None)
341+
config = _convert_config(config)
342+
res = None
343+
try:
344+
inp = {
345+
"args": args,
346+
"kwargs": kwargs
347+
}
348+
res = RunnableLambda(_param_wrapped_func).invoke(input=inp, config=config)
349+
if hasattr(res, "__iter__"):
350+
return res
351+
except StopIteration:
352+
pass
353+
except Exception as e:
354+
raise e
355+
finally:
356+
if res is not None:
357+
return res
358+
359+
@wraps(func)
360+
async def async_wrapper(*args: Any, **kwargs: Any):
361+
config = kwargs.pop("config", None)
362+
config = _convert_config(config)
363+
res = None
364+
try:
365+
inp = {
366+
"args": args,
367+
"kwargs": kwargs
368+
}
369+
res = await RunnableLambda(_param_wrapped_func_async).ainvoke(input=inp, config=config)
370+
if hasattr(res, "__aiter__"):
371+
return res
372+
except StopIteration:
373+
pass
374+
except StopAsyncIteration:
375+
pass
376+
except Exception as e:
377+
if e.args and e.args[0] == 'coroutine raised StopIteration': # coroutine StopIteration
378+
pass
379+
else:
380+
raise e
381+
finally:
382+
if res is not None:
383+
return res
384+
385+
@wraps(func)
386+
def gen_wrapper(*args: Any, **kwargs: Any):
387+
config = kwargs.pop("config", None)
388+
config = _convert_config(config)
389+
try:
390+
inp = {
391+
"args": args,
392+
"kwargs": kwargs
393+
}
394+
gen = RunnableLambda(_param_wrapped_func).invoke(input=inp, config=config)
395+
try:
396+
for item in gen:
397+
yield item
398+
except StopIteration:
399+
pass
400+
except Exception as e:
401+
raise e
402+
403+
@wraps(func)
404+
async def async_gen_wrapper(*args: Any, **kwargs: Any):
405+
config = kwargs.pop("config", None)
406+
config = _convert_config(config)
407+
try:
408+
inp = {
409+
"args": args,
410+
"kwargs": kwargs
411+
}
412+
gen = RunnableLambda(_param_wrapped_func_async).invoke(input=inp, config=config)
413+
items = []
414+
try:
415+
async for item in gen:
416+
items.append(item)
417+
yield item
418+
finally:
419+
pass
420+
except StopIteration:
421+
pass
422+
except StopAsyncIteration:
423+
pass
424+
except Exception as e:
425+
if e.args and e.args[0] == 'coroutine raised StopIteration':
426+
pass
427+
else:
428+
raise e
429+
430+
# for convert parameter
431+
def _param_wrapped_func(input_dict: dict) -> Any:
432+
args = input_dict.get("args", ())
433+
kwargs = input_dict.get("kwargs", {})
434+
return func(*args, **kwargs)
435+
436+
async def _param_wrapped_func_async(input_dict: dict) -> Any:
437+
args = input_dict.get("args", ())
438+
kwargs = input_dict.get("kwargs", {})
439+
return await func(*args, **kwargs)
440+
441+
def _convert_config(config: RunnableConfig = None) -> RunnableConfig | None:
442+
if config is None:
443+
config = RunnableConfig(run_name=func.__name__)
444+
config['run_name'] = func.__name__
445+
elif isinstance(config, dict):
446+
config['run_name'] = func.__name__
447+
return config
448+
449+
if is_async_gen_func(func):
450+
return async_gen_wrapper
451+
if is_gen_func(func):
452+
return gen_wrapper
453+
elif is_async_func(func):
454+
return async_wrapper
455+
else:
456+
return sync_wrapper
457+
458+
if func is None:
459+
return decorator
460+
else:
461+
return decorator(func)
462+
463+
315464
class _CozeLoopTraceStream(Generic[S]):
316465
def __init__(
317466
self,

cozeloop/integration/langchain/trace_model/llm_model.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,8 @@ class Message:
5353
tool_calls: List[ToolCall] = None
5454
metadata: Optional[dict] = None
5555
reasoning_content: Optional[str] = None
56+
name: Optional[str] = None
57+
tool_call_id: Optional[str] = None
5658

5759
def __post_init__(self):
5860
if self.role is not None and (self.role == 'AIMessageChunk' or self.role == 'ai'):
@@ -155,7 +157,7 @@ def __init__(self, messages: List[Union[BaseMessage, List[BaseMessage]]], invoca
155157
if message.additional_kwargs is not None and message.additional_kwargs.get('name', ''):
156158
name = message.additional_kwargs.get('name', '')
157159
tool_call = ToolCall(id=message.tool_call_id, type=message.type, function=ToolFunction(name=name))
158-
self._messages.append(Message(role=message.type, content=message.content, tool_calls=[tool_call]))
160+
self._messages.append(Message(role=message.type, content=message.content, tool_calls=[tool_call], name=name, tool_call_id=message.tool_call_id))
159161
else:
160162
self._messages.append(Message(role=message.type, content=message.content))
161163

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "cozeloop"
3-
version = "0.1.24"
3+
version = "0.1.25"
44
description = "coze loop sdk"
55
authors = ["JiangQi715 <jiangqi.rrt@bytedance.com>"]
66
license = "MIT"

0 commit comments

Comments
 (0)