Skip to content

Commit c8836b9

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

5 files changed

Lines changed: 156 additions & 2 deletions

File tree

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: 146 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,150 @@ 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 = _get_config(**kwargs)
341+
res = None
342+
try:
343+
inp = {
344+
"args": args,
345+
"kwargs": kwargs
346+
}
347+
res = RunnableLambda(_param_wrapped_func).invoke(input=inp, config=config, **kwargs)
348+
if hasattr(res, "__iter__"):
349+
return res
350+
except StopIteration:
351+
pass
352+
except Exception as e:
353+
raise e
354+
finally:
355+
if res is not None:
356+
return res
357+
358+
@wraps(func)
359+
async def async_wrapper(*args: Any, **kwargs: Any):
360+
config = _get_config(**kwargs)
361+
res = None
362+
try:
363+
inp = {
364+
"args": args,
365+
"kwargs": kwargs
366+
}
367+
res = await RunnableLambda(_param_wrapped_func_async).ainvoke(input=inp, config=config, **kwargs)
368+
if hasattr(res, "__aiter__"):
369+
return res
370+
except StopIteration:
371+
pass
372+
except StopAsyncIteration:
373+
pass
374+
except Exception as e:
375+
if e.args and e.args[0] == 'coroutine raised StopIteration': # coroutine StopIteration
376+
pass
377+
else:
378+
raise e
379+
finally:
380+
if res is not None:
381+
return res
382+
383+
@wraps(func)
384+
def gen_wrapper(*args: Any, **kwargs: Any):
385+
config = _get_config(**kwargs)
386+
try:
387+
inp = {
388+
"args": args,
389+
"kwargs": kwargs
390+
}
391+
gen = RunnableLambda(_param_wrapped_func).invoke(input=inp, config=config, **kwargs)
392+
try:
393+
for item in gen:
394+
yield item
395+
except StopIteration:
396+
pass
397+
except Exception as e:
398+
raise e
399+
400+
@wraps(func)
401+
async def async_gen_wrapper(*args: Any, **kwargs: Any):
402+
config = _get_config(**kwargs)
403+
try:
404+
inp = {
405+
"args": args,
406+
"kwargs": kwargs
407+
}
408+
gen = RunnableLambda(_param_wrapped_func_async).invoke(input=inp, config=config, **kwargs)
409+
items = []
410+
try:
411+
async for item in gen:
412+
items.append(item)
413+
yield item
414+
finally:
415+
pass
416+
except StopIteration:
417+
pass
418+
except StopAsyncIteration:
419+
pass
420+
except Exception as e:
421+
if e.args and e.args[0] == 'coroutine raised StopIteration':
422+
pass
423+
else:
424+
raise e
425+
426+
# for convert parameter
427+
def _param_wrapped_func(input_dict: dict) -> Any:
428+
args = input_dict.get("args", ())
429+
kwargs = input_dict.get("kwargs", {})
430+
return func(*args, **kwargs)
431+
432+
async def _param_wrapped_func_async(input_dict: dict) -> Any:
433+
args = input_dict.get("args", ())
434+
kwargs = input_dict.get("kwargs", {})
435+
return await func(*args, **kwargs)
436+
437+
def _get_config(**kwargs: Any) -> RunnableConfig | None:
438+
config = kwargs.pop("config", None)
439+
if config is None:
440+
config = RunnableConfig(run_name=func.__name__)
441+
config['run_name'] = func.__name__
442+
elif isinstance(config, dict):
443+
config['run_name'] = func.__name__
444+
return config
445+
446+
if is_async_gen_func(func):
447+
return async_gen_wrapper
448+
if is_gen_func(func):
449+
return gen_wrapper
450+
elif is_async_func(func):
451+
return async_wrapper
452+
else:
453+
return sync_wrapper
454+
455+
if func is None:
456+
return decorator
457+
else:
458+
return decorator(func)
459+
460+
315461
class _CozeLoopTraceStream(Generic[S]):
316462
def __init__(
317463
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)