diff --git a/adalflow/adalflow/components/agent/react.py b/adalflow/adalflow/components/agent/react.py index 60824847..f6336334 100644 --- a/adalflow/adalflow/components/agent/react.py +++ b/adalflow/adalflow/components/agent/react.py @@ -13,6 +13,7 @@ from adalflow.optim.parameter import Parameter, ParameterType from adalflow.core.func_tool import FunctionTool, AsyncCallable from adalflow.core.tool_manager import ToolManager +from adalflow.core.component import Component from adalflow.components.output_parsers import JsonOutputParser from adalflow.core.types import ( StepOutput, @@ -176,7 +177,7 @@ class ReActOutput(DataClass): answer: Any = field(metadata={"desc": "The final answer."}, default=None) -class ReActAgent(GradComponent): +class ReActAgent(Component): __doc__ = r"""ReActAgent uses generator as a planner that runs multiple and sequential functional call steps to generate the final response. Users need to set up: @@ -241,7 +242,7 @@ def __init__( # template for the planner template: Optional[str] = None, # allow users to customize the template context_variables: Optional[Dict] = None, # context variables - debug: bool = False, + debug: bool = True, ): super().__init__() template = template or DEFAULT_REACT_AGENT_SYSTEM_PROMPT @@ -267,7 +268,9 @@ def __init__( self._examples = examples + [example] output_parser = JsonOutputParser( - data_class=ouput_data_class, examples=self._examples, return_data_class=True + data_class=ouput_data_class, + examples=self._examples, + return_data_class=True, ) prompt_kwargs = { "tools": self.tool_manager.yaml_definitions, @@ -350,35 +353,110 @@ def _execute_action( if isinstance(response, Parameter): - try: + def handle_error(response: Parameter, e: str): + from adalflow.optim.grad_component import fun_to_grad_component - function_output_to_step_output = FunctionOutputToStepOutput() + print(f"action_step: {action_step}") + + @fun_to_grad_component + def set_step_output_with_error( + step_output: StepOutput, error: str, response: Any + ): + """Set the step_output with error.""" + step_output.observation = f"erro: {error} at {response.data}" + return step_output - # printc(f"response: {response}", color="yellow") - # TO FunctionExpression response.add_successor_map_fn( - successor=self.tool_manager, map_fn=lambda x: x.full_response + successor=set_step_output_with_error, map_fn=lambda x: x.data ) + return set_step_output_with_error.forward(action_step, e, response) + + try: + function_output_to_step_output = FunctionOutputToStepOutput() + # TO FunctionExpression func: Union[Function, Parameter] = self.tool_manager( - expr_or_fun=response, step="parse" + expr_or_fun=response, step="parse", map_fn=lambda x: x.data.data ) + # add action to the step_output + action_step.action = response.data.data + # parse failed if not isinstance(func, Parameter): raise ValueError( f"Expected Parameter, but got {type(func)}: {func}" ) + if isinstance(func, str): + # create dummy step output + from adalflow.optim.grad_component import fun_to_grad_component + + @fun_to_grad_component + def set_step_output_with_error( + step_output: StepOutput, data: FunctionExpression, error: str + ): + """Set the step_output with error.""" + step_output.observation = f"Error in parsing the FunctionExperession to Function: {error}" + return step_output + + response.add_successor_map_fn( + successor=set_step_output_with_error, + map_fn=lambda x: x.data.data, + ) + action_step = set_step_output_with_error.forward( + action_step, response, error=func + ) + return action_step + + except Exception as e: + e = f"{e} at parsing error at functionexpression: {response.data}" + return handle_error(response, e) + + try: + # printc(f"func: {func}", color="yellow") # replace the id if isinstance(func, Parameter): func.data.kwargs["id"] = id - result: Parameter = self.tool_manager(expr_or_fun=func, step="execute") + if self.debug: + printc(f"func: {func.data}", color="yellow") + + result: Parameter = self.tool_manager( + expr_or_fun=func, step="execute", map_fn=lambda x: x.data + ) + + if isinstance(result, str): + # create dummy step output + from adalflow.optim.grad_component import fun_to_grad_component + + @fun_to_grad_component + def set_step_output_with_error(step_output: StepOutput, data: str): + """Set the step_output with error.""" + step_output.observation = f"Error {data} in executing action." + + return step_output + + response.add_successor_map_fn( + successor=set_step_output_with_error, + map_fn=lambda x: x.data.data, + ) + action_step = set_step_output_with_error.forward( + action_step, response + ) + + return action_step + + except Exception as e: + e = f"{e} Error executing function: {func}" + return handle_error(response, e) + + try: # printc(f"result: {result}", color="red") result.add_successor_map_fn( successor=function_output_to_step_output, map_fn=lambda x: x.data ) response.add_successor_map_fn( - successor=function_output_to_step_output, map_fn=lambda x: x.data + successor=function_output_to_step_output, + map_fn=lambda x: x.data.data, ) func.add_successor_map_fn( successor=function_output_to_step_output, map_fn=lambda x: x.data @@ -391,12 +469,11 @@ def _execute_action( ) return action_step - except Exception as e: - log.error(f"Error executing {response}: {e}") - # pass the error as observation so that the agent can continue and correct the error in the next step - action_step.observation = f"Error executing {response}: {e}" - return action_step + e = f"{e} Error converting function output to step output: {result.data}" + + return handle_error(response, e) + else: return self._execute_action_eval_mode( @@ -433,19 +510,15 @@ def _execute_action_eval_mode( ) step_output.function = fun - result: FunctionOutput = self.tool_manager( expr_or_fun=fun, step="execute" ) step_output.observation = result.output - - # step_output = execute_action(step_output, id) if self.debug: printc(f"Step {step}: \n{step_output}\n_______\n", color="blue") return step_output else: if self.debug: - printc(f"Failed to parse response for step {step}", color="red") log.error(f"Failed to parse response for step {step}") return step_output @@ -513,9 +586,36 @@ def _run_one_step( try: if self.training and isinstance(response, Parameter): - # printc(f"response: {response}", color="yellow") - step_output: Parameter = self._execute_action(step_output, response, id) + if not isinstance(response.data, GeneratorOutput): + raise ValueError( + f"Expected GeneratorOutput, but got {type(response.data)}, value: {response.data}" + ) + + if not isinstance(response.data.data, FunctionExpression): + from adalflow.optim.grad_component import fun_to_grad_component + + @fun_to_grad_component + def set_step_output_with_error( + step_output: StepOutput, data: GeneratorOutput + ): + """Set the step_output with error.""" + step_output.observation = f"Error {data.error} in parsing response: {data.raw_response}, data type: {type(data.data)}" + return step_output + + response.add_successor_map_fn( + successor=set_step_output_with_error, + map_fn=lambda x: x.data, + ) + step_output = set_step_output_with_error.forward( + step_output, response + ) + + else: + + step_output: Parameter = self._execute_action( + step_output, response, id + ) # printc(f"step_output: {step_output}", color="red") if not isinstance(step_output, Parameter): @@ -537,6 +637,10 @@ def _run_one_step( step_history.add_successor_map_fn( successor=self.planner, map_fn=lambda x: x.data ) + if self.debug: + printc( + f"step_history: {step_history.get_prompt_data()}", color="red" + ) return step_history else: @@ -689,7 +793,7 @@ def _extra_repr(self) -> str: setup_env() - class App(GradComponent): + class App(Component): def __init__(self): super().__init__() self.llm_tool = Generator( @@ -719,6 +823,8 @@ def forward( ) -> Union[str, "Parameter"]: return self.react_agent(input, id=id) + # print(OutputParameter.__mro__) + app = App() app.train() output = app("I want to multiply 3 and 4.", id="123") diff --git a/adalflow/adalflow/components/output_parsers/dataclass_parser.py b/adalflow/adalflow/components/output_parsers/dataclass_parser.py index 6d2e56dd..39fda03c 100644 --- a/adalflow/adalflow/components/output_parsers/dataclass_parser.py +++ b/adalflow/adalflow/components/output_parsers/dataclass_parser.py @@ -4,12 +4,12 @@ from typing import Any, Literal, List, Optional import logging -from adalflow.core.component import Component from adalflow.core.prompt_builder import Prompt from adalflow.core.string_parser import YamlParser, JsonParser from adalflow.core.base_data_class import DataClass, DataClassFormatType from adalflow.core.base_data_class import ExcludeType, IncludeType + __all__ = ["DataClassParser"] log = logging.getLogger(__name__) @@ -42,7 +42,7 @@ """ -class DataClassParser(Component): +class DataClassParser: __doc__ = r"""Made the structured output even simpler compared with JsonOutputParser and YamlOutputParser. 1. Understands __input_fields__ and __output_fields__ from the DataClass (no need to use include/exclude to decide fields). @@ -166,6 +166,9 @@ def get_examples_str( examples_str = Prompt(template=EXAMPLES_FORMAT)(examples=str_examples) return examples_str + def __call__(self, *args, **kwargs): + return self.call(*args, **kwargs) + def call(self, input: str) -> Any: r"""Parse the output string to the desired format and return the parsed output.""" try: diff --git a/adalflow/adalflow/components/output_parsers/outputs.py b/adalflow/adalflow/components/output_parsers/outputs.py index 288cba67..807fb996 100644 --- a/adalflow/adalflow/components/output_parsers/outputs.py +++ b/adalflow/adalflow/components/output_parsers/outputs.py @@ -11,7 +11,6 @@ from typing import Dict, Any, Optional, List import logging -from adalflow.core.component import Component from adalflow.core.prompt_builder import Prompt from adalflow.core.string_parser import YamlParser, ListParser, JsonParser from adalflow.core.base_data_class import DataClass, DataClassFormatType @@ -69,7 +68,7 @@ YAML_OUTPUT_PARSER_OUTPUT_TYPE = Dict[str, Any] -class OutputParser(Component): +class OutputParser: __doc__ = r"""The abstract class for all output parsers. This interface helps users customize output parsers with consistent interfaces for the Generator. @@ -88,6 +87,9 @@ def format_instructions(self) -> str: r"""Return the formatted instructions to use in prompt for the output format.""" raise NotImplementedError("This is an abstract method.") + def __call__(self, *args: Any, **kwds: Any) -> Any: + return self.call(*args, **kwds) + def call(self, input: str) -> Any: r"""Parse the output string to the desired format and return the parsed output.""" raise NotImplementedError("This is an abstract method.") diff --git a/adalflow/adalflow/core/component.py b/adalflow/adalflow/core/component.py index b239f521..e72b3a9c 100644 --- a/adalflow/adalflow/core/component.py +++ b/adalflow/adalflow/core/component.py @@ -519,11 +519,31 @@ def named_parameters( # ) # plt.show() - # TODO: do we need to disable this format of calling instead use call and acall extensively? def __call__(self, *args, **kwargs): - r"""In default, we use sync call.""" - output = self.call(*args, **kwargs) - return output + from adalflow.optim.parameter import Parameter + + if self.training: + output = self.forward(*args, **kwargs) + print(f"{isinstance(output, Parameter)}") + + if not isinstance(output, Parameter): + raise ValueError( + f"Output should be of type Parameter, but got {type(output)}" + ) + return output + else: + output = self.call(*args, **kwargs) + if isinstance(output, Parameter): + raise ValueError( + f"Output should not be of type OutputParameter, but got {type(output)}" + ) + return output + + def forward(self, *args, **kwargs): + r"""Forward pass for training mode.""" + raise NotImplementedError( + f"Component {type(self).__name__} is missing the 'forward' method for training mode." + ) def call(self, *args, **kwargs): raise NotImplementedError( diff --git a/adalflow/adalflow/core/func_tool.py b/adalflow/adalflow/core/func_tool.py index ad135b74..7c5e0f1d 100644 --- a/adalflow/adalflow/core/func_tool.py +++ b/adalflow/adalflow/core/func_tool.py @@ -248,8 +248,6 @@ def sync_function_1(): # raise ValueError(f"Error: {e}") error = str(e) - print(f"typeof output: {type(output)}") - if isinstance(output, Parameter): if not self.training: raise ValueError( diff --git a/adalflow/adalflow/core/generator.py b/adalflow/adalflow/core/generator.py index 950682a5..a0fa2a87 100644 --- a/adalflow/adalflow/core/generator.py +++ b/adalflow/adalflow/core/generator.py @@ -518,6 +518,10 @@ def forward( } output = self.call(**input_args, id=id) + if not isinstance(output, GeneratorOutput): + raise ValueError( + f"Output should be of type GeneratorOutput, got {type(output)}" + ) # 2. Generate a Parameter object from the output combined_prompt_kwargs = compose_model_kwargs(self.prompt_kwargs, prompt_kwargs) # if self.data_map_func is None: @@ -528,19 +532,26 @@ def forward( ] log.debug(f"Predecessors: {predecessors} for generator {self.name}") - param_data = ( - # output.raw_response - output.data - if output and not output.error - else f"Error: {output.error}, raw_response: {output.raw_response}" - ) + + def data_to_prompt_map_fn(data: Parameter) -> str: + data: GeneratorOutput = data.data + if data.data is not None: + return data.data + if data.error is not None: + return f"Response: {data.raw_response} parsed with error: {data.error}" + return f"Response: {data.raw_response}" + + # TODO: all parameter should just wrap the whole output. + # this is for training. + param_data = output response: Parameter = OutputParameter( data=param_data, name=self.name + "_output", role_desc=f"Output from (llm) {self.name}", param_type=ParameterType.GENERATOR_OUTPUT, data_id=id, - full_response=output.data, # the data structure + full_response=output, # the data structure + data_in_prompt=data_to_prompt_map_fn, ) response.set_predecessors(predecessors) response.trace_forward_pass( @@ -582,10 +593,7 @@ def forward( backward_fn=self.backward, backward_engine=self.backward_engine, response=response, - prompt_kwargs={ - k: v.data if isinstance(v, Parameter) else v - for k, v in prompt_kwargs.items() - }, + prompt_kwargs=prompt_kwargs, template=self.template, prompt_str=self.get_prompt(**combined_prompt_kwargs), id=id, @@ -594,7 +602,6 @@ def forward( ) return response - # == pytorch custom autograd function == def backward( self, response: Parameter, # the output of the forward pass @@ -687,15 +694,15 @@ def _backward_through_all_predecessors( # instruction and objective is the same for all the children instruction_str, objective_str = None, None - # 1. Generate the conversation string + # 1. Generate the conversation input and output input_prompt_kwargs = { - k: v.data if isinstance(v, Parameter) else v + k: v.get_prompt_data() if isinstance(v, Parameter) else v for k, v in prompt_kwargs.items() } conversation_prompt_kwargs = { "input_value": input_prompt_kwargs, - "llm_output": response.data, + "llm_output": response.get_prompt_data(), } conversation_str = Prompt( @@ -704,7 +711,7 @@ def _backward_through_all_predecessors( )() all_pred_info = Prompt( - prompt_kwargs={"variables": children_params}, + prompt_kwargs={"variables": [p.get_param_info() for p in children_params]}, template=ALL_PRED_INFO, )() @@ -849,10 +856,8 @@ def _backward_through_one_predecessor( } conversation_prompt_kwargs = { - # "variable_name": pred.name, - # "variable_desc": pred.role_desc, "input_value": input_prompt_kwargs, - "llm_output": response.data, + "llm_output": response.get_prompt_data(), } conversation_str = Prompt( @@ -912,10 +917,6 @@ def _backward_through_one_predecessor( data=manual_response, raw_response=manual_response ) else: - # manual_response = f"You get score: {response._score}." - # gradient_output = GeneratorOutput( - # data=manual_response, raw_response=manual_response - # ) gradient_output: GeneratorOutput = backward_engine( prompt_kwargs=backward_engine_prompt_kwargs diff --git a/adalflow/adalflow/core/prompt_builder.py b/adalflow/adalflow/core/prompt_builder.py index 0d998b63..d5f3e9ae 100644 --- a/adalflow/adalflow/core/prompt_builder.py +++ b/adalflow/adalflow/core/prompt_builder.py @@ -7,9 +7,10 @@ from jinja2 import Template, Environment, StrictUndefined, meta -from adalflow.core.component import Component from adalflow.core.default_prompt_template import DEFAULT_ADALFLOW_SYSTEM_PROMPT from adalflow.optim.parameter import Parameter +from dataclasses import dataclass +from adalflow.core.base_data_class import DataClass logger = logging.getLogger(__name__) @@ -17,7 +18,8 @@ T = TypeVar("T") -class Prompt(Component): +@dataclass +class Prompt(DataClass): __doc__ = r"""Renders a text string(prompt) from a Jinja2 template string. In default, we use the :ref:`DEFAULT_ADALFLOW_SYSTEM_PROMPT` as the template. @@ -125,6 +127,9 @@ def print_prompt(self, **kwargs) -> str: except Exception as e: raise ValueError(f"Error rendering Jinja2 template: {e}") + def __call__(self, *args: Any, **kwds: Any) -> Any: + return self.call(*args, **kwds) + def call(self, **kwargs) -> str: """ Renders the prompt template with keyword arguments. Allow None values. diff --git a/adalflow/adalflow/core/string_parser.py b/adalflow/adalflow/core/string_parser.py index 3001b512..c7526c79 100644 --- a/adalflow/adalflow/core/string_parser.py +++ b/adalflow/adalflow/core/string_parser.py @@ -5,7 +5,6 @@ from typing import Dict, List, Union import logging -from adalflow.core.component import Component import adalflow.core.functional as F log = logging.getLogger(__name__) @@ -13,12 +12,15 @@ BOOLEAN_PARSER_OUTPUT_TYPE = bool -class Parser(Component): +class Parser: __doc__ = r"""Base class for all string parsers.""" def __init__(self): super().__init__() + def __call__(self, input: str) -> object: + return self.call(input) + def call(self, input: str) -> object: raise NotImplementedError( "Parser subclasses must implement the __call__ method" diff --git a/adalflow/adalflow/core/tool_manager.py b/adalflow/adalflow/core/tool_manager.py index 1db303e4..4ac17607 100644 --- a/adalflow/adalflow/core/tool_manager.py +++ b/adalflow/adalflow/core/tool_manager.py @@ -22,6 +22,7 @@ from adalflow.core.container import ComponentList from adalflow.optim.grad_component import GradComponent +from adalflow.core.component import Component from adalflow.core.func_tool import FunctionTool from adalflow.core.types import ( FunctionDefinition, @@ -57,7 +58,7 @@ def run_async_in_new_loop(coro): asyncio.set_event_loop(None) -class CallFunctionTool(GradComponent): +class CallFunctionTool(Component): __doc__ = """Contains other unit gradcomponent such as calling a FunctionTool""" @@ -76,11 +77,13 @@ def bicall( context: Dict[str, object] = {}, ): if isinstance(func, Parameter): - # data = func.successor_map_fn(func) printc(f"context: {context}", color="yellow") - tool: FunctionTool = context[func.data.name] + func_data: Function = func.map_to_successor(self) + if not isinstance(func_data, Function): + raise ValueError(f"Error parsing function expression: {func}") + tool: FunctionTool = context[func_data.name] print(f"tool training: {tool.training}") - output = tool.forward(*func.data.args, **func.data.kwargs) + output = tool.forward(*func_data.args, **func_data.kwargs) from adalflow.optim.grad_component import fun_to_grad_component @@ -108,25 +111,35 @@ class FunctionExperssionToFunction(GradComponent): def __init__(self): super().__init__() - def call(self, expr: FunctionExpression, context: Dict[str, object]): - print("DummpyGradComponent call") - print(expr) + def call(self, expr: FunctionExpression, context: Dict[str, object]) -> Function: + + assert isinstance( + expr, FunctionExpression + ), f"Expected FunctionExpression, got {type(expr)}" expr_str = expr.action func_name, args, kwargs = parse_function_call_expr(expr_str, context) - return Function( + printc( + f"func_name: {func_name}, args: {args}, kwargs: {kwargs}", color="yellow" + ) + output = Function( name=func_name, args=args, kwargs=kwargs, thought=expr.thought, ) + printc(f"output: {output}", color="yellow") + return output # TODO: good to track all the failed function calls # Tool manager is a task component -class ToolManager(GradComponent): +class ToolManager(Component): __doc__ = r""""Manage a list of tools, context, and all ways to execute functions. + + ToolManager is a task component that does not need its own backward function. + yaml and json definitions are for quick access to the definitions of the tools. If you need more specification, such as using exclude field, you can use the function_definitions. """ @@ -207,20 +220,27 @@ def function_definitions(self) -> List[FunctionDefinition]: return [tool.definition for tool in self.tools] def parse_func_expr( - self, expr: Union[FunctionExpression, Parameter] + self, + expr: Union[FunctionExpression, Parameter], + map_fn: Callable = lambda x: x.data, ) -> Union[Function, Parameter]: r"""Parse the function call expression.""" if isinstance(expr, Parameter): try: - dummy = FunctionExperssionToFunction() + func = FunctionExperssionToFunction() + expr.add_successor_map_fn(func, map_fn=map_fn) print("FunctionExperssionToFunction") - return dummy.forward(expr, context=self.context) + output = func.forward(expr, context=self.context) + print(f"output data: {output.data}") + return output except Exception as e: - log.error(f"Error {e} parsing function call expression: {expr}") - raise ValueError(f"Error {e} parsing function call expression: {expr}") + error_msg = ( + f"Error {e} parsing function call expression: {map_fn(expr)}" + ) + return error_msg else: try: expr_str = expr.action @@ -276,35 +296,50 @@ def forward( *, expr_or_fun: Union[FunctionExpression, Function, Parameter], step: Literal["parse", "execute"] = "execute", + map_fn: Callable = lambda x: x.data, # how to map the parameter to the needed data ) -> Union[FunctionOutput, Function, Parameter]: + "Run a forward pass on the tool manager such as parsing function expression or executing function." if isinstance(expr_or_fun, Parameter): + expr_or_fun_data = map_fn(expr_or_fun) + print(f"expr_or_fun_data: {expr_or_fun_data}") if step == "execute": - if isinstance(expr_or_fun.data, Function): - return self.execute_func(expr_or_fun) + if isinstance(expr_or_fun_data, Function): + return self.execute_func(expr_or_fun, map_fn=map_fn) else: raise NotImplementedError( - "Only function call expressions are supported for now." + "Only Function expressions are supported for now." ) else: - if isinstance(expr_or_fun.data, FunctionExpression): - return self.parse_func_expr(expr_or_fun) + if isinstance(expr_or_fun_data, FunctionExpression): + print(f"start parsing: {expr_or_fun_data}") + output = self.parse_func_expr(expr_or_fun, map_fn=map_fn) + print(f"output 3: {output.data}") + return output else: raise NotImplementedError( - f"Only function call expressions are supported for now. Got {expr_or_fun.data}" + f"Only function call expressions are supported for now. Got {expr_or_fun_data}" ) else: raise ValueError(f"expr_or_fun should be a Parameter. Got {expr_or_fun}") # return self.call(expr_or_fun=expr_or_fun, step=step) def execute_func( - self, func: Union[Function, Parameter] + self, func: Union[Function, Parameter], map_fn: Callable = lambda x: x.data ) -> Union[FunctionOutput, Parameter]: r"""Execute the function. If the function is async, use asyncio.run to execute it.""" if isinstance(func, Parameter): + try: + + call_func_tool = CallFunctionTool() + func.add_successor_map_fn(call_func_tool, map_fn=map_fn) + return call_func_tool.forward(func, context=self.context) + + except Exception as e: + log.error(f"Error {e} executing function: {func.data}") + error_msg = f"Error {e} executing function: {func.data}" + return error_msg - call_func_tool = CallFunctionTool() - return call_func_tool.forward(func, context=self.context) else: try: tool: FunctionTool = self.context[func.name] @@ -342,12 +377,15 @@ async def execute_func_async(self, func: Function) -> FunctionOutput: raise ValueError(f"Error {e} executing function: {func}") def execute_func_expr( - self, expr: Union[FunctionExpression, Parameter] + self, + expr: Union[FunctionExpression, Parameter], + map_fn: Callable = lambda x: x.data, ) -> Union[FunctionOutput, Parameter]: r"""Execute the function expression. Support both sync and async functions.""" if isinstance(expr, Parameter): - func: Parameter = self.parse_func_expr(expr.data) + + func: Parameter = self.parse_func_expr(expr, map_fn=map_fn) if not isinstance(func, Parameter): raise ValueError(f"Error parsing function expression: {expr}") diff --git a/adalflow/adalflow/core/types.py b/adalflow/adalflow/core/types.py index db3f27d0..eceac117 100644 --- a/adalflow/adalflow/core/types.py +++ b/adalflow/adalflow/core/types.py @@ -411,12 +411,10 @@ def add(a, b): The benefits are less failed function calls. """ - question: Optional[str] = field( - default=None, metadata={"desc": "The question to ask the LLM"} - ) - thought: Optional[str] = field( - default=None, metadata={"desc": "Why the function is called"} - ) + # question: str = field( + # default=None, metadata={"desc": "The question to ask the LLM"} + # ) + thought: str = field(default=None, metadata={"desc": "Why the function is called"}) action: str = field( default_factory=required_field, metadata={"desc": _action_desc}, diff --git a/adalflow/adalflow/optim/grad_component.py b/adalflow/adalflow/optim/grad_component.py index 0602f411..d306a508 100644 --- a/adalflow/adalflow/optim/grad_component.py +++ b/adalflow/adalflow/optim/grad_component.py @@ -45,12 +45,6 @@ def __init__(self, *args, **kwargs): super().__setattr__("backward_engine", None) super().__setattr__("id", str(uuid.uuid4())) - def __call__(self, *args, **kwargs): - if self.training: - return self.forward(*args, **kwargs) - else: - return self.call(*args, **kwargs) - def set_backward_engine(self, backward_engine: "BackwardEngine", *args, **kwargs): raise NotImplementedError("set_backward_engine method is not implemented") @@ -74,11 +68,6 @@ def forward(self, *args, **kwargs) -> "Parameter": f"Forwarding through {self.name} with args: {args} and kwargs: {kwargs}" ) - # if "id" not in kwargs: - # raise ValueError( - # "id must be provided in the kwargs of a GradComponent for tracing." - # ) - # 1. get all predecessors from all args and kwargs input_args = OrderedDict() diff --git a/adalflow/adalflow/optim/parameter.py b/adalflow/adalflow/optim/parameter.py index 83f2ea1e..6984128e 100644 --- a/adalflow/adalflow/optim/parameter.py +++ b/adalflow/adalflow/optim/parameter.py @@ -265,7 +265,11 @@ def __init__( self.eval_input = eval_input self.successor_map_fn = successor_map_fn or {} - self.data_in_prompt = lambda x: x.data if not data_in_prompt else data_in_prompt + + def default_prompt_map_fn(param: Parameter): + return param.data + + self.data_in_prompt = data_in_prompt or default_prompt_map_fn def map_to_successor(self, successor: object) -> T: """Apply the map function to the successor based on the successor's id.""" @@ -309,6 +313,9 @@ def get_gradients_names(self) -> str: names = ", ".join(names) return names + def get_prompt_data(self) -> str: + return self.data_in_prompt(self) + def get_gradients_str(self) -> str: if not self.gradients: return "" @@ -551,8 +558,10 @@ def get_param_info(self): return { "name": self.name, "role_desc": self.role_desc, - "data": self.data_in_prompt(self), + "prompt_data": self.data_in_prompt(self), # default to use all data "param_type": self.param_type, + "requires_opt": self.requires_opt, + "eval_input": self.eval_input, # for output passing to the eval_fn } def set_peers(self, peers: List["Parameter"] = None): @@ -688,7 +697,8 @@ def get_short_value(self, n_words_offset: int = 10) -> str: :type n_words_offset: int """ # 1. ensure the data is a string - data = self.data + # data = self.data + data = self.get_prompt_data() if not isinstance(self.data, str): data = str(self.data) words = data.split(" ") @@ -775,19 +785,14 @@ def generate_node_html(node: "Parameter", output_dir="node_pages"): filename = f"{output_dir}/{node.name}.html" - # inspect everything about the gradients - # gradients = "" - # for i, g in enumerate(node.gradients): - # # Format each gradient as YAML with proper indentation and replace \n\n\n with actual line breaks - # gradient_yaml = g.to_yaml().replace("\\n", "\n").replace("\n\n\n", "\n") - # gradients += f"{i}:\n{gradient_yaml}\n" - # Gather gradients as JSON objects gradients = [] for i, g in enumerate(node.gradients): - gradients.append( - g.to_json_obj() - ) # Use to_json_obj for proper JSON object structure + gradient = g.to_json_obj() + for k, v in gradient.items(): + if isinstance(v, str): + gradient[k] = v.replace("<", "<").replace(">", ">") + gradients.append(gradient) data_json = None node_data_type = str(type(node.data)).replace("<", "<").replace(">", ">") @@ -863,14 +868,11 @@ def draw_interactive_html_graph( """ from jinja2 import Template - # Define the output file path output_file = "interactive_graph.html" final_file = filepath + "_" + output_file if filepath else output_file - # Create a pyvis Network instance net = Network(height="750px", width="100%", directed=True) - # different color per node type node_colors = { ParameterType.PROMPT: "lightblue", ParameterType.DEMOS: "orange", @@ -919,7 +921,6 @@ def draw_interactive_html_graph( f"Skipping edge from {source.name} to {target.name} as one of the nodes does not exist." ) - # Enable physics for better layout net.toggle_physics(True) net.template = Template( """ @@ -977,8 +978,6 @@ def draw_interactive_html_graph( """ ) - # Save the graph as an HTML file - net.show(final_file) print(f"Interactive graph saved to {final_file}") @@ -1587,6 +1586,7 @@ def __init__( score: Optional[float] = None, eval_input: object = None, successor_map_fn: Optional[Dict[str, Callable]] = None, + data_in_prompt: Optional[Callable] = None, full_response: Optional[Any] = None, ): super().__init__( @@ -1602,6 +1602,7 @@ def __init__( score=score, eval_input=eval_input, successor_map_fn=successor_map_fn, + data_in_prompt=data_in_prompt, ) self.component_trace = ComponentTrace() diff --git a/adalflow/adalflow/optim/text_grad/backend_engine_prompt.py b/adalflow/adalflow/optim/text_grad/backend_engine_prompt.py index bc9582ae..f13f79c2 100644 --- a/adalflow/adalflow/optim/text_grad/backend_engine_prompt.py +++ b/adalflow/adalflow/optim/text_grad/backend_engine_prompt.py @@ -24,14 +24,19 @@ If the same DataID has multiple gradients, it means this component/variable is called multiple times in the compound system(with a cycle) in the same order as it appears in the gradient list. +{% if output_format_str %} +{{output_format_str}} +{% endif %} + + {{conversation_sec}} + {{objective_instruction_sec}} -{% if output_format_str %} -{{output_format_str}} -{% endif %} + + """ ############################################## # Loss Component @@ -48,6 +53,7 @@ # """ # Your only goal is to clearly states how it obtained the "". + OBJECTIVE_INSTRUCTION_BASE = r""" Your task is to provide the response with specific feedback based on the ground truth and the score in the "". Especially when the score is low. @@ -57,13 +63,46 @@ """ +### NOTE: Last node's feedback +OBJECTIVE_INSTRUCTION_CHAIN = r"""This conversation is part of a larger system. The was later used as "{{response_name}}: {{response_desc}}". + +Your only goal is to clearly states how it obtained the "Eval output/score": {{response_gradient}}. +Especially when the score is low. +Be CONCISE. +If you have enough context, add a more specific feedback on how it failed. +""" + +### Loss/Score Information ### +# INPUTS: parameter.get_param_info(): +# the input_output of a GradientContext + +# response_value -> response.get_prompt_data() +LOSS_CONVERSATION_TEMPLATE_STRING = r""" +The variable is passed to the eval function and compared with a target/ground truth value. + +EVAL_FUNC: {{eval_fn_desc}} + +INPUTS: +{% for key, (value, eval_type) in inputs.items() %} +({{ key }}) (role: {{ value.role_desc }}), +data: {{ value.prompt_data }}, +input_to_eval_fn: {{ value.eval_input }}, +data_type: {{ eval_type }} +{% endfor %} + +OUTPUTS/SCORE: {{response_value}} +{% if metadata %} +Note: {{metadata}} +{% endif %}""" + + ### Variable to get feedback on, often it is pred in the loss component # pass parameter.get_param_info() to get the variable info LOSS_CONVERSATION_START_INSTRUCTION_STRING_FN = r""" TARGET VARIABLE: {{variable.name}} {{variable.role_desc}} - {{variable.data}} + {{variable.prompt_data}} {{conversation_str}} """ @@ -76,7 +115,7 @@ INPUTS: {% for key, (value, eval_type) in inputs.items() %} ({{ key }}) (role: {{ value.role_desc }}), -full response: {{ value.data }}, +data: {{ value.prompt_data }}, input_to_eval_fn: {{ value.eval_input }}, data_type: {{ eval_type }} {% endfor %} @@ -108,23 +147,13 @@ {% endif %} """ -### Backward engine: user prompt -# First part to provide context of LLM as gradComponent -# The target variable is used as either input or a task instruction to a language model (LM): -# replace the "The target variable is used as either input or a task instruction to a language model (LM):" with the {{variable_desc}} -# NAME: {{variable_name}} -# Description: {{variable_desc}} -LLM_CONVERSATION_TEMPLATE = r""" -LM_INPUT: {{input_value}} -LM_OUTPUT: {{llm_output}}""" - VARIABLE_AND_PEERS_INFO = r""" {{variable.name}} {{variable.param_type}} {{variable.role_desc}} -{{ variable.data}} +{{ variable.prompt_data}} {% if peers %} @@ -135,8 +164,8 @@ PEER_TYPE: {{peer.param_type}}, PEER_ROLE: {{peer.role_desc}} WILL_BE_OPTIMIZED: {{peer.requires_opt}} -{% if peer.data %} -PEER_VARIABLE: {{peer.data}} +{% if peer.prompt_data %} +PEER_VARIABLE: {{peer.prompt_data}} {% else %} PEER_VARIABLE: EMPTY {% endif %} @@ -145,6 +174,7 @@ {% endif %} """ + # a list of variables ALL_PRED_INFO = r""" @@ -156,12 +186,23 @@ TYPE: {{variable.param_type}}, ROLE: {{variable.role_desc}} WILL_BE_OPTIMIZED: {{variable.requires_opt}} -VARIABLE: {{ variable.data}} +VARIABLE: {{ variable.prompt_data}} {% endfor %} {% endif %} """ + +### Backward engine: user prompt +# First part to provide context of LLM as gradComponent +# The target variable is used as either input or a task instruction to a language model (LM): +# replace the "The target variable is used as either input or a task instruction to a language model (LM):" with the {{variable_desc}} +# NAME: {{variable_name}} +# Description: {{variable_desc}} +LLM_CONVERSATION_TEMPLATE = r""" +LM_INPUT: {{input_value}} +LM_OUTPUT: {{llm_output}}""" + OUTPUT_INSTRUCTION = r""" You will create a feedback for each of the variable in the list above. If a variable will not be optimied, you just output empty string. diff --git a/adalflow/adalflow/optim/text_grad/text_loss_with_eval_fn.py b/adalflow/adalflow/optim/text_grad/text_loss_with_eval_fn.py index 9d7ac36c..6cefdfb6 100644 --- a/adalflow/adalflow/optim/text_grad/text_loss_with_eval_fn.py +++ b/adalflow/adalflow/optim/text_grad/text_loss_with_eval_fn.py @@ -25,21 +25,13 @@ LOSS_CONVERSATION_TEMPLATE_STRING, LOSS_CONVERSATION_START_INSTRUCTION_STRING_FN, OBJECTIVE_INSTRUCTION_BASE, + OBJECTIVE_INSTRUCTION_CHAIN, ) log = logging.getLogger(__name__) -OBJECTIVE_INSTRUCTION_CHAIN = r"""This conversation is part of a larger system. The was later used as "{{response_name}}: {{response_desc}}". - -Your only goal is to clearly states how it obtained the "Eval output/score": {{response_gradient}}. -Especially when the score is low. -Be CONCISE. -If you have enough context, add a more specific feedback on how it failed. -""" - - class EvalFnToTextLoss(LossComponent): __doc__ = """Convert an evaluation function to a text loss. @@ -219,7 +211,7 @@ def _backward_through_one_predecessor( inputs = {} for k, v in kwargs.items(): - inputs[k] = (v, str(type(v.eval_input))) + inputs[k] = (v.get_param_info(), str(type(v.eval_input))) # response information conversation_str = Prompt( @@ -227,7 +219,7 @@ def _backward_through_one_predecessor( prompt_kwargs={ "inputs": inputs, "eval_fn_desc": eval_fn_desc, - "response_value": response.data, + "response_value": response.get_prompt_data(), "metadata": json.dumps(metadata) if metadata else None, }, )() diff --git a/adalflow/adalflow/optim/text_grad/tgd_optimizer.py b/adalflow/adalflow/optim/text_grad/tgd_optimizer.py index 4247c5cf..26bbf38e 100644 --- a/adalflow/adalflow/optim/text_grad/tgd_optimizer.py +++ b/adalflow/adalflow/optim/text_grad/tgd_optimizer.py @@ -369,8 +369,9 @@ def get_gradient_memory_text(self, param: Parameter) -> str: def _get_user_prompt_kwargs(self, param: Parameter) -> Dict[str, str]: + peers_params = [p.get_param_info() for p in self.params if p.id != param.id] variable_and_peer_info = self.variable_and_peers_info.call( - variable=param.get_param_info(), peers=param.peers # param.peers + variable=param.get_param_info(), peers=peers_params ) variable_grad = param.get_gradients_component_schema(skip_correct_sample=True) diff --git a/adalflow/adalflow/optim/trainer/trainer.py b/adalflow/adalflow/optim/trainer/trainer.py index bf35a282..5262d5ad 100644 --- a/adalflow/adalflow/optim/trainer/trainer.py +++ b/adalflow/adalflow/optim/trainer/trainer.py @@ -763,7 +763,7 @@ def _fit_demos_one_step_for_debug( # print(f"Teacher y_preds: {y_preds[0].to_dict()}") - y_preds_outputs = [p.full_response for p in y_preds] + y_preds_outputs = [p.data for p in y_preds] batch_eval: EvaluationResult = self.adaltask.evaluate_samples( batch, y_preds_outputs @@ -824,7 +824,7 @@ def _fit_demos_one_step_for_debug( # for loss in losses_student: # loss.backward() # Check the eval result - y_preds_outputs = [p.full_response for p in y_preds_student] + y_preds_outputs = [p.data for p in y_preds_student] eval_result = self.adaltask.evaluate_samples(batch, y_preds_outputs) print(f"Eval result: {eval_result.avg_score}") # eval_score_per_item = eval_result.per_item_scores @@ -1116,7 +1116,7 @@ def _fit_text_grad_demo_mix_constrained( all_losses.extend(losses) # student losses # extract the non-parameter y_preds all_y_preds.extend( - [y.full_response for y in y_preds if isinstance(y, Parameter)] + [y.data for y in y_preds if isinstance(y, Parameter)] ) # for loss in losses: @@ -1901,6 +1901,7 @@ def _text_grad_constraint_propose_step( raise ValueError("Loss should be a Parameter object") self.adaltask.eval() move_batch_eval = self.adaltask.evaluate_samples(all_samples, all_y_preds) + print(f"Moving batch eval: {move_batch_eval}") move_batch_score = move_batch_eval.avg_score move_batch_acc_score_list = move_batch_eval.per_item_scores diff --git a/adalflow/tests/test_parameter.py b/adalflow/tests/test_parameter.py index 3da290da..a8f64f0e 100644 --- a/adalflow/tests/test_parameter.py +++ b/adalflow/tests/test_parameter.py @@ -46,6 +46,19 @@ def test_update_value(self, data, new_data): param.update_value(new_data) assert param.data == new_data, "Parameter data should be updated correctly" + def test_data_in_prompt_callable(self): + param = Parameter( + data=10, requires_opt=False, data_in_prompt=lambda x: f"Data: {x.data}" + ) + + assert ( + param.data_in_prompt(param) == "Data: 10" + ), "Data should be correctly formatted in the prompt" + + assert ( + param.get_prompt_data() == "Data: 10" + ), "Data should be correctly formatted in the prompt" + # def test_update_value_incorrect_type(self): # """Test updating the parameter with an incorrect type.""" # param = Parameter[int](data=10) diff --git a/benchmarks/hotpot_qa/adal_exp/train_agent_rag.py b/benchmarks/hotpot_qa/adal_exp/train_agent_rag.py index 1ea541f6..93bd1fc5 100644 --- a/benchmarks/hotpot_qa/adal_exp/train_agent_rag.py +++ b/benchmarks/hotpot_qa/adal_exp/train_agent_rag.py @@ -161,7 +161,7 @@ def train( # ) train( - debug=False, + debug=True, max_steps=12, # resume_from_ckpt="/Users/liyin/.adalflow/ckpt/AgenticRAGAdal/constrained_max_steps_4_dca7e_run_1.json", ) @@ -170,3 +170,6 @@ def train( # 0.7, 0.72 /Users/liyin/.adalflow/ckpt/AgenticRAGAdal/constrained_max_steps_2_b7523_run_1.json # 208.085706949234s, 2 steps, maximum 4 steps allow for an agent. # 0.72->0.74, 4 steps, 366s, /Users/liyin/.adalflow/ckpt/AgenticRAGAdal/constrained_max_steps_4_dca7e_run_1.json [Already faster, still lots to optimize] + + # 1246s, 12 steps, 0.8 val, /Users/liyin/.adalflow/ckpt/AgenticRAGAdal/constrained_max_steps_12_defe7_run_1.json + # v2149s, both gradients, 0.68 -> 0.78 /Users/liyin/.adalflow/ckpt/AgenticRAGAdal/constrained_max_steps_12_8a24a_run_1.json diff --git a/use_cases/classification/train.py b/use_cases/classification/train.py index ec110506..1f419140 100644 --- a/use_cases/classification/train.py +++ b/use_cases/classification/train.py @@ -10,7 +10,6 @@ from use_cases.config import ( gpt_3_model, gpt_4o_model, - gpt_4o_mini_model, ) @@ -52,7 +51,7 @@ def prepare_eval( def prepare_loss( self, sample: TRECExtendedData, y_pred: adal.Parameter, *args, **kwargs ) -> Tuple[Callable[..., Any], Dict]: - full_response = y_pred.full_response + full_response = y_pred.data y_label = -1 # default value for failed prediction if ( full_response @@ -87,8 +86,8 @@ def train( adal_component = TrecClassifierAdal( model_client=model_client, model_kwargs=model_kwargs, - text_optimizer_model_config=gpt_4o_mini_model, - backward_engine_model_config=gpt_4o_mini_model, + text_optimizer_model_config=gpt_4o_model, + backward_engine_model_config=gpt_4o_model, teacher_model_config=gpt_4o_model, ) print(adal_component) @@ -157,7 +156,8 @@ def train( # no past history, 83% only. 84 /Users/liyin/.adalflow/ckpt/TrecClassifierAdal/constrained_max_steps_12_ca5ac_run_1.json # past history, both gradients, 88.89% in 12 steps /Users/liyin/.adalflow/ckpt/TrecClassifierAdal/constrained_max_steps_12_b4612_run_1.json 1477s # /Users/liyin/.adalflow/ckpt/TrecClassifierAdal/constrained_max_steps_12_f1e5a_run_1.json 811s 89.58% both positive and negative gradients - # /Users/liyin/.adalflow/ckpt/TrecClassifierAdal/constrained_max_steps_12_05a8e_run_1.json 1518 85.41% only negative gradients + # /Users/liyin/.adalflow/ckpt/TrecClassifierAdal/constrained_max_steps_12_05a8e_run_1.json 1518s 85.41% only negative gradients + # /Users/liyin/.adalflow/ckpt/TrecClassifierAdal/constrained_max_steps_12_e0f86_run_1.json 1247s, 88.88 both gradients # theory: all few-shots demo or instruction, all so that the llm can reason better. Once it reches to its limits, no more shots can help or further instruction can. diff --git a/use_cases/question_answering/bbh/object_count/task.py b/use_cases/question_answering/bbh/object_count/task.py index d3b1e8ed..53fb5057 100644 --- a/use_cases/question_answering/bbh/object_count/task.py +++ b/use_cases/question_answering/bbh/object_count/task.py @@ -40,7 +40,7 @@ def __init__(self, model_client: adal.ModelClient, model_kwargs: Dict): few_shot_demos = adal.Parameter( data=None, role_desc="To provide few shot demos to the language model", - requires_opt=True, + requires_opt=False, param_type=ParameterType.DEMOS, ) @@ -62,7 +62,7 @@ def call( output = self.llm_counter(prompt_kwargs={"input_str": question}, id=id) # print(f"output: {output}, training: {self.training}") if self.training: - if output.full_response.error and "429" in output.full_response.error: + if output.data.error and "429" in output.data.error: raise ValueError("Rate limit exceeded") else: if output.error and "429" in output.error: @@ -85,8 +85,9 @@ def test_object_count_task(): task_pipeline.train() answer: adal.Parameter = task_pipeline(question, id="1") print(answer) - print(f"full_response: {answer.full_response}") + print(f"data: {answer.data}") answer.draw_graph() + print(f"prompt_data: {answer.get_prompt_data()}") if __name__ == "__main__": diff --git a/use_cases/question_answering/bbh/object_count/train_new.py b/use_cases/question_answering/bbh/object_count/train_new.py index b0cb024a..c0400a9c 100644 --- a/use_cases/question_answering/bbh/object_count/train_new.py +++ b/use_cases/question_answering/bbh/object_count/train_new.py @@ -43,6 +43,7 @@ def prepare_eval( self, sample: Example, y_pred: adal.GeneratorOutput ) -> Tuple[float, Dict[str, Any]]: y_label = -1 + print(f"y_pred: {y_pred}") if ( y_pred is not None and y_pred.data is not None ): # if y_pred and y_pred.data: might introduce bug when the data is 0 @@ -58,7 +59,7 @@ def prepare_loss( eval_input=sample.answer, requires_opt=False, ) - pred.eval_input = pred.full_response.data + pred.eval_input = pred.data.data return self.loss_fn, {"kwargs": {"y": pred, "y_gt": y_gt}, "id": sample.id} @@ -159,7 +160,7 @@ def train( ckpt = train( debug=False, - max_steps=1, + max_steps=12, strategy=set_strategy, exclude_input_fields_from_bootstrap_demos=True, ) @@ -180,3 +181,7 @@ def train( # without gradients -> 0.9 on tests # without positive gradients -> /Users/liyin/.adalflow/ckpt/ObjectCountAdalComponent/constrained_max_steps_12_8ac70_run_1.json 0.84->0.94 val, 0.82 -> 0.88 test + + # /Users/liyin/.adalflow/ckpt/ObjectCountAdalComponent/constrained_max_steps_12_1f358_run_1.json 1 val 0.96 val 955s + # 0.94 val, 0.89 test, /Users/liyin/.adalflow/ckpt/ObjectCountAdalComponent/constrained_max_steps_12_e1bb5_run_1.json 907s, with both positive and negatives + # 92, 91 test /Users/liyin/.adalflow/ckpt/ObjectCountAdalComponent/constrained_max_steps_12_18e8d_run_1.json 747s