11from __future__ import annotations
22
33import os
4- from typing import Any , Dict , List , Optional
4+ from typing import List
55
6- from langchain_core .messages import AIMessage , BaseMessage , HumanMessage , ToolMessage
6+ from langchain_core .messages import AIMessage , BaseMessage , HumanMessage , SystemMessage , ToolMessage
7+ from eval_protocol .human_id import generate_id
8+ import json
79
810from eval_protocol .models import Message
911
@@ -14,10 +16,8 @@ def _dbg_enabled() -> bool:
1416
1517def _dbg_print (* args ):
1618 if _dbg_enabled ():
17- try :
18- print (* args )
19- except Exception :
20- pass
19+ # Best-effort debug print without broad exception handling
20+ print (* args )
2121
2222
2323def serialize_lc_message_to_ep (msg : BaseMessage ) -> Message :
@@ -36,88 +36,126 @@ def serialize_lc_message_to_ep(msg: BaseMessage) -> Message:
3636 return ep_msg
3737
3838 if isinstance (msg , AIMessage ):
39- content = ""
39+ # Extract visible content and hidden reasoning content if present
40+ content_text = ""
41+ reasoning_texts : List [str ] = []
42+
4043 if isinstance (msg .content , str ):
41- content = msg .content
44+ content_text = msg .content
4245 elif isinstance (msg .content , list ):
43- parts : List [str ] = []
46+ text_parts : List [str ] = []
4447 for item in msg .content :
4548 if isinstance (item , dict ):
46- if item .get ("type" ) == "text" :
47- parts .append (str (item .get ("text" , "" )))
49+ item_type = item .get ("type" )
50+ if item_type == "text" :
51+ text_parts .append (str (item .get ("text" , "" )))
52+ elif item_type in ("reasoning" , "thinking" , "thought" ):
53+ # Some providers return dedicated reasoning parts
54+ maybe_text = item .get ("text" ) or item .get ("content" )
55+ if isinstance (maybe_text , str ):
56+ reasoning_texts .append (maybe_text )
4857 elif isinstance (item , str ):
49- parts .append (item )
50- content = "\n " .join (parts )
51-
52- tool_calls_payload : Optional [List [Dict [str , Any ]]] = None
53-
54- def _normalize_tool_calls (tc_list : List [Any ]) -> List [Dict [str , Any ]]:
55- mapped : List [Dict [str , Any ]] = []
56- for call in tc_list :
57- if not isinstance (call , dict ):
58- continue
59- try :
60- call_id = call .get ("id" ) or "toolcall_0"
61- if isinstance (call .get ("function" ), dict ):
62- fn = call ["function" ]
63- fn_name = fn .get ("name" ) or call .get ("name" ) or "tool"
64- fn_args = fn .get ("arguments" )
65- else :
66- fn_name = call .get ("name" ) or "tool"
67- fn_args = call .get ("arguments" ) if call .get ("arguments" ) is not None else call .get ("args" )
68- if not isinstance (fn_args , str ):
69- import json as _json
70-
71- fn_args = _json .dumps (fn_args or {}, ensure_ascii = False )
72- mapped .append (
58+ text_parts .append (item )
59+ content_text = "\n " .join ([t for t in text_parts if t ])
60+
61+ # Additional place providers may attach reasoning
62+ additional_kwargs = getattr (msg , "additional_kwargs" , None )
63+ if isinstance (additional_kwargs , dict ):
64+ rk = additional_kwargs .get ("reasoning_content" )
65+ if isinstance (rk , str ) and rk :
66+ reasoning_texts .append (rk )
67+
68+ # Fireworks and others sometimes nest under `reasoning` or `metadata`
69+ nested_reasoning = additional_kwargs .get ("reasoning" )
70+ if isinstance (nested_reasoning , dict ):
71+ inner = nested_reasoning .get ("content" ) or nested_reasoning .get ("text" )
72+ if isinstance (inner , str ) and inner :
73+ reasoning_texts .append (inner )
74+
75+ # Capture tool calls and function_call if present on AIMessage
76+ def _normalize_tool_calls (raw_tcs ):
77+ normalized = []
78+ for tc in raw_tcs or []:
79+ if isinstance (tc , dict ) and "function" in tc :
80+ # Assume already OpenAI style
81+ fn = tc .get ("function" , {})
82+ # Ensure arguments is a string
83+ args = fn .get ("arguments" )
84+ if not isinstance (args , str ):
85+ try :
86+ args = json .dumps (args )
87+ except Exception :
88+ args = str (args )
89+ normalized .append (
90+ {
91+ "id" : tc .get ("id" ) or generate_id (),
92+ "type" : tc .get ("type" ) or "function" ,
93+ "function" : {"name" : fn .get ("name" , "" ), "arguments" : args },
94+ }
95+ )
96+ elif isinstance (tc , dict ) and ("name" in tc ) and ("args" in tc or "arguments" in tc ):
97+ # LangChain tool schema → OpenAI function-call schema
98+ name = tc .get ("name" , "" )
99+ args_val = tc .get ("args" , tc .get ("arguments" , {}))
100+ if not isinstance (args_val , str ):
101+ try :
102+ args_val = json .dumps (args_val )
103+ except Exception :
104+ args_val = str (args_val )
105+ normalized .append (
106+ {
107+ "id" : tc .get ("id" ) or generate_id (),
108+ "type" : "function" ,
109+ "function" : {"name" : name , "arguments" : args_val },
110+ }
111+ )
112+ else :
113+ # Best-effort: stringify unknown formats
114+ normalized .append (
73115 {
74- "id" : call_id ,
116+ "id" : generate_id () ,
75117 "type" : "function" ,
76- "function" : {"name" : fn_name , "arguments" : fn_args },
118+ "function" : {
119+ "name" : str (tc .get ("name" , "tool" )) if isinstance (tc , dict ) else "tool" ,
120+ "arguments" : json .dumps (tc ) if not isinstance (tc , str ) else tc ,
121+ },
77122 }
78123 )
79- except Exception :
80- continue
81- return mapped
82-
83- ak = getattr (msg , "additional_kwargs" , None )
84- if isinstance (ak , dict ):
85- tc = ak .get ("tool_calls" )
86- if isinstance (tc , list ) and tc :
87- mapped = _normalize_tool_calls (tc )
88- if mapped :
89- tool_calls_payload = mapped
90-
91- if tool_calls_payload is None :
92- raw_attr_tc = getattr (msg , "tool_calls" , None )
93- if isinstance (raw_attr_tc , list ) and raw_attr_tc :
94- mapped = _normalize_tool_calls (raw_attr_tc )
95- if mapped :
96- tool_calls_payload = mapped
97-
98- # Extract reasoning/thinking parts into reasoning_content
99- reasoning_content = None
100- if isinstance (msg .content , list ):
101- collected = [
102- it .get ("thinking" , "" ) for it in msg .content if isinstance (it , dict ) and it .get ("type" ) == "thinking"
103- ]
104- if collected :
105- reasoning_content = "\n \n " .join ([s for s in collected if s ]) or None
106-
107- # Message.tool_calls expects List[ChatCompletionMessageToolCall] | None.
108- # We pass through Dicts at runtime but avoid type error by casting.
124+ return normalized if normalized else None
125+
126+ extracted_tool_calls = None
127+ tc_attr = getattr (msg , "tool_calls" , None )
128+ if isinstance (tc_attr , list ):
129+ extracted_tool_calls = _normalize_tool_calls (tc_attr )
130+
131+ if extracted_tool_calls is None and isinstance (additional_kwargs , dict ):
132+ maybe_tc = additional_kwargs .get ("tool_calls" )
133+ if isinstance (maybe_tc , list ):
134+ extracted_tool_calls = _normalize_tool_calls (maybe_tc )
135+
136+ extracted_function_call = None
137+ fc_attr = getattr (msg , "function_call" , None )
138+ if fc_attr :
139+ extracted_function_call = fc_attr
140+ if extracted_function_call is None and isinstance (additional_kwargs , dict ):
141+ maybe_fc = additional_kwargs .get ("function_call" )
142+ if maybe_fc :
143+ extracted_function_call = maybe_fc
144+
109145 ep_msg = Message (
110146 role = "assistant" ,
111- content = content ,
112- tool_calls = tool_calls_payload , # type: ignore[arg-type]
113- reasoning_content = reasoning_content ,
147+ content = content_text ,
148+ reasoning_content = ("\n " .join (reasoning_texts ) if reasoning_texts else None ),
149+ tool_calls = extracted_tool_calls , # type: ignore[arg-type]
150+ function_call = extracted_function_call , # type: ignore[arg-type]
114151 )
115152 _dbg_print (
116153 "[EP-Ser] -> EP Message:" ,
117154 {
118155 "role" : ep_msg .role ,
119156 "content_len" : len (ep_msg .content or "" ),
120- "tool_calls" : len (ep_msg .tool_calls or []) if isinstance (ep_msg .tool_calls , list ) else 0 ,
157+ "has_reasoning" : bool (ep_msg .reasoning_content ),
158+ "has_tool_calls" : bool (ep_msg .tool_calls ),
121159 },
122160 )
123161 return ep_msg
@@ -141,3 +179,36 @@ def _normalize_tool_calls(tc_list: List[Any]) -> List[Dict[str, Any]]:
141179 ep_msg = Message (role = getattr (msg , "type" , "assistant" ), content = str (getattr (msg , "content" , "" )))
142180 _dbg_print ("[EP-Ser] -> EP Message (fallback):" , {"role" : ep_msg .role , "len" : len (ep_msg .content or "" )})
143181 return ep_msg
182+
183+
184+ def serialize_ep_messages_to_lc (messages : List [Message ]) -> List [BaseMessage ]:
185+ """Convert eval_protocol Message objects to LangChain BaseMessage list.
186+
187+ - Flattens content parts into strings when content is a list
188+ - Maps EP roles to LC message classes
189+ """
190+ lc_messages : List [BaseMessage ] = []
191+ for m in messages or []:
192+ content = m .content
193+ if isinstance (content , list ):
194+ text_parts : List [str ] = []
195+ for part in content :
196+ try :
197+ text_parts .append (getattr (part , "text" , "" ))
198+ except AttributeError :
199+ pass
200+ content = "\n " .join ([t for t in text_parts if t ])
201+ if content is None :
202+ content = ""
203+ text = str (content )
204+
205+ role = (m .role or "" ).lower ()
206+ if role == "user" :
207+ lc_messages .append (HumanMessage (content = text ))
208+ elif role == "assistant" :
209+ lc_messages .append (AIMessage (content = text ))
210+ elif role == "system" :
211+ lc_messages .append (SystemMessage (content = text ))
212+ else :
213+ lc_messages .append (HumanMessage (content = text ))
214+ return lc_messages
0 commit comments