agsagds commited on
Commit
2420c6c
·
1 Parent(s): 422b737

feat: introduce MultiStepWorkflow for enhanced agent functionality and update agent initialization

Browse files
Files changed (3) hide show
  1. agent.py +4 -21
  2. app.py +11 -10
  3. workflow.py +133 -0
agent.py CHANGED
@@ -1,4 +1,5 @@
1
  import asyncio
 
2
  import os
3
  import pprint
4
  from llama_index.core.agent.workflow.workflow_events import AgentInput, AgentOutput, AgentStream, ToolCall, ToolCallResult
@@ -15,9 +16,6 @@ from llama_index.llms.nebius import NebiusLLM
15
  import tools
16
 
17
  """
18
- TODO: 3. Fix submission error 'Agent finished. Submitting 13 answers for user 'agsagds'...
19
- Submitting 13 answers to: https://agents-course-unit4-scoring.hf.space/submit
20
- An unexpected error occurred during submission: Object of type AgentOutput is not JSON serializable'
21
  TODO: 4. Run HF Space and submit answers to the scoring server.
22
  TODO: 5. Get a certificate for the course.
23
  """
@@ -25,20 +23,6 @@ TODO: 5. Get a certificate for the course.
25
  class BasicAgent:
26
  def __init__(self, verbose: bool = False):
27
  self.verbose = verbose
28
- self.system_prompt = """
29
- You are a general AI assistant. I will ask you a question.
30
- Report your thoughts, and finish your answer with the following template:
31
- FINAL ANSWER: [YOUR FINAL ANSWER].
32
- YOUR FINAL ANSWER should be a number OR as few words as possible
33
- OR a comma separated list of numbers and/or strings.
34
- If you are asked for a number, don't use comma to write your number
35
- neither use units such as $ or percent sign unless specified otherwise.
36
- If you are asked for a string, don't use articles, neither abbreviations
37
- (e.g. for cities), and write the digits in plain text unless specified
38
- otherwise. If you are asked for a comma separated list, apply the above
39
- rules depending of whether the element to be put in the list is a number
40
- or a string.
41
- """
42
  # llm = Ollama(
43
  # model="gpt-oss:20b",
44
  # request_timeout=120.0
@@ -63,13 +47,12 @@ class BasicAgent:
63
  *WikipediaToolSpec().to_tool_list(),
64
  ],
65
  llm=llm,
66
- system_prompt=self.system_prompt
67
  )
68
- print("BasicAgent initialized.")
69
 
70
  async def __call__(self, question: str) -> str:
71
  """Async call method that returns the final result without streaming"""
72
- print(f"Agent received question (first 50 chars): {question[:50]}...")
73
 
74
  if self.verbose:
75
  answer = await self.stream_answers(question)
@@ -100,7 +83,7 @@ class BasicAgent:
100
  elif isinstance(event, ToolCall):
101
  print(f"\n\tCalled tool: {event.tool_name} {event.tool_kwargs}")
102
  elif isinstance(event, ToolCallResult):
103
- print(f"\n\t{event.tool_name} {event.tool_kwargs} -> {event.tool_output}")
104
  elif isinstance(event, StopEvent):
105
  return event.result
106
  elif isinstance(event, AgentStream):
 
1
  import asyncio
2
+ import datetime
3
  import os
4
  import pprint
5
  from llama_index.core.agent.workflow.workflow_events import AgentInput, AgentOutput, AgentStream, ToolCall, ToolCallResult
 
16
  import tools
17
 
18
  """
 
 
 
19
  TODO: 4. Run HF Space and submit answers to the scoring server.
20
  TODO: 5. Get a certificate for the course.
21
  """
 
23
  class BasicAgent:
24
  def __init__(self, verbose: bool = False):
25
  self.verbose = verbose
 
 
 
 
 
 
 
 
 
 
 
 
 
 
26
  # llm = Ollama(
27
  # model="gpt-oss:20b",
28
  # request_timeout=120.0
 
47
  *WikipediaToolSpec().to_tool_list(),
48
  ],
49
  llm=llm,
50
+ system_prompt=f"Today's date is {datetime.datetime.now().strftime('%Y-%m-%d')}."
51
  )
 
52
 
53
  async def __call__(self, question: str) -> str:
54
  """Async call method that returns the final result without streaming"""
55
+ print(f"Agent received question : {question}\n")
56
 
57
  if self.verbose:
58
  answer = await self.stream_answers(question)
 
83
  elif isinstance(event, ToolCall):
84
  print(f"\n\tCalled tool: {event.tool_name} {event.tool_kwargs}")
85
  elif isinstance(event, ToolCallResult):
86
+ print(f"\n\t{event.tool_name} {event.tool_kwargs} -> {str(event.tool_output)[200:]}")
87
  elif isinstance(event, StopEvent):
88
  return event.result
89
  elif isinstance(event, AgentStream):
app.py CHANGED
@@ -4,6 +4,7 @@ import requests
4
  import inspect
5
  import pandas as pd
6
  from agent import BasicAgent
 
7
 
8
  # (Keep Constants as is)
9
  # --- Constants ---
@@ -97,10 +98,10 @@ def run_and_submit_all( profile: gr.OAuthProfile | None):
97
 
98
  # 1. Instantiate Agent ( modify this part to create your agent)
99
  try:
100
- agent = BasicAgent(verbose=True)
101
  except Exception as e:
102
- print(f"Error instantiating agent: {e}")
103
- return f"Error initializing agent: {e}", None
104
  # In the case of an app running as a hugging Face space, this link points toward your codebase ( usefull for others so please keep it public)
105
  agent_code = f"https://huggingface.co/spaces/{space_id}/tree/main"
106
  print(agent_code)
@@ -129,7 +130,7 @@ def run_and_submit_all( profile: gr.OAuthProfile | None):
129
  # 3. Run your Agent
130
  results_log = []
131
  answers_payload = []
132
- print(f"Running agent on {len(questions_data)} questions...")
133
  for item in questions_data:
134
  task_id = item.get("task_id")
135
  question_text = item.get("question")
@@ -146,22 +147,22 @@ def run_and_submit_all( profile: gr.OAuthProfile | None):
146
  print(f"Skipping item with missing task_id or question: {item}")
147
  continue
148
  try:
149
- print(f"Running agent on question: {task_id} {question_text}")
150
- submitted_answer = agent.call_sync(question_text)
151
  print(f"Submitted answer: {submitted_answer}")
152
  answers_payload.append({"task_id": task_id, "submitted_answer": submitted_answer})
153
  results_log.append({"Task ID": task_id, "Question": question_text, "Submitted Answer": submitted_answer})
154
  except Exception as e:
155
- print(f"Error running agent on task {task_id}: {e}")
156
  results_log.append({"Task ID": task_id, "Question": question_text, "Submitted Answer": f"AGENT ERROR: {e}"})
157
 
158
  if not answers_payload:
159
- print("Agent did not produce any answers to submit.")
160
- return "Agent did not produce any answers to submit.", pd.DataFrame(results_log)
161
 
162
  # 4. Prepare Submission
163
  submission_data = {"username": username.strip(), "agent_code": agent_code, "answers": answers_payload}
164
- status_update = f"Agent finished. Submitting {len(answers_payload)} answers for user '{username}'..."
165
  print(status_update)
166
 
167
  # 5. Submit
 
4
  import inspect
5
  import pandas as pd
6
  from agent import BasicAgent
7
+ from workflow import MultiStepWorkflow
8
 
9
  # (Keep Constants as is)
10
  # --- Constants ---
 
98
 
99
  # 1. Instantiate Agent ( modify this part to create your agent)
100
  try:
101
+ agent_workflow = MultiStepWorkflow(timeout=300, verbose=False)
102
  except Exception as e:
103
+ print(f"Error instantiating agent workflow: {e}")
104
+ return f"Error initializing agent workflow: {e}", None
105
  # In the case of an app running as a hugging Face space, this link points toward your codebase ( usefull for others so please keep it public)
106
  agent_code = f"https://huggingface.co/spaces/{space_id}/tree/main"
107
  print(agent_code)
 
130
  # 3. Run your Agent
131
  results_log = []
132
  answers_payload = []
133
+ print(f"Running agent workflow on {len(questions_data)} questions...")
134
  for item in questions_data:
135
  task_id = item.get("task_id")
136
  question_text = item.get("question")
 
147
  print(f"Skipping item with missing task_id or question: {item}")
148
  continue
149
  try:
150
+ print(f"Running agent workflow on question: {task_id} {question_text}")
151
+ submitted_answer = agent_workflow.callSync(question_text)
152
  print(f"Submitted answer: {submitted_answer}")
153
  answers_payload.append({"task_id": task_id, "submitted_answer": submitted_answer})
154
  results_log.append({"Task ID": task_id, "Question": question_text, "Submitted Answer": submitted_answer})
155
  except Exception as e:
156
+ print(f"Error running agent workflow on task {task_id}: {e}")
157
  results_log.append({"Task ID": task_id, "Question": question_text, "Submitted Answer": f"AGENT ERROR: {e}"})
158
 
159
  if not answers_payload:
160
+ print("Agent workflow did not produce any answers to submit.")
161
+ return "Agent workflow did not produce any answers to submit.", pd.DataFrame(results_log)
162
 
163
  # 4. Prepare Submission
164
  submission_data = {"username": username.strip(), "agent_code": agent_code, "answers": answers_payload}
165
+ status_update = f"Agent workflow finished. Submitting {len(answers_payload)} answers for user '{username}'..."
166
  print(status_update)
167
 
168
  # 5. Submit
workflow.py ADDED
@@ -0,0 +1,133 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import asyncio
2
+ import os
3
+ import datetime
4
+ from llama_index.core.workflow import Event, StartEvent, StopEvent, Workflow, step, Context
5
+ from llama_index.llms.nebius import NebiusLLM
6
+
7
+ from agent import BasicAgent
8
+
9
+ """
10
+ 1. Decide what to use: context or event payload?
11
+ 2. Investigate alternative to initializate llm at each step|global llm
12
+ 3. Throw relevant data throght steps
13
+ """
14
+ class EvalPlanEvent(Event):
15
+ planStep: str
16
+
17
+ class FinalAnswerEvent(Event):
18
+ finalAnswer: str
19
+
20
+ class UpdatePlanEvent(Event):
21
+ planStep: str
22
+ planStepResult: str
23
+
24
+ class MultiStepWorkflow(Workflow):
25
+
26
+ def __init__(self, **kwargs):
27
+ self.llm = NebiusLLM(
28
+ api_key=os.getenv("NEBIUS_API_KEY"),
29
+ model="Qwen/Qwen3-235B-A22B-Instruct-2507",
30
+ api_base="https://api.tokenfactory.nebius.com/v1",
31
+ system_prompt=f"Today's date is {datetime.datetime.now().strftime('%Y-%m-%d')}."
32
+ )
33
+ self.agent = BasicAgent(verbose=kwargs.get("verbose", False))
34
+ super().__init__(**kwargs)
35
+
36
+ @step
37
+ async def makePlanStep(self, ctx: Context, ev: StartEvent) -> EvalPlanEvent:
38
+ if not hasattr(ev, "question"):
39
+ raise ValueError("question field is required")
40
+ await ctx.store.set("question", ev.question)
41
+ plan = await self.llm.acomplete("""Make a plan to answer the question. Plan should be a list of steps.
42
+ Each step should contain enough context to execute the step.
43
+ Formulate the steps in a way that can be executed by the agent.
44
+ Maximum 7 steps. Return only the plan, no other text.
45
+ The question: """ + ev.question)
46
+ await ctx.store.set("plan", str(plan))
47
+ step = await self.llm.acomplete("""Get the first step of the plan. Return only the step, no other text.
48
+ The plan: """ + str(plan))
49
+ print(f'Plan is {plan}')
50
+ return EvalPlanEvent(planStep=str(step))
51
+
52
+ @step
53
+ async def evalPlanStep(self, ctx: Context, ev: EvalPlanEvent) -> UpdatePlanEvent | FinalAnswerEvent:
54
+ if not hasattr(ev, "planStep"):
55
+ raise ValueError("planStep field is required")
56
+
57
+ result = await self.agent(f"The question is: {await ctx.store.get('question')} \n\n The plan is: {await ctx.store.get('plan')} \n\n Execute only the step: {ev.planStep}")
58
+ return UpdatePlanEvent(planStep=ev.planStep, planStepResult=str(result))
59
+
60
+ @step
61
+ async def updatePlanStep(self, ctx: Context, ev: UpdatePlanEvent)-> EvalPlanEvent | FinalAnswerEvent:
62
+ if not hasattr(ev, "planStep"):
63
+ raise ValueError("planStep field is required")
64
+ if not hasattr(ev, "planStepResult"):
65
+ raise ValueError("planStepResult field is required")
66
+
67
+ plan = await ctx.store.get("plan")
68
+ question = await ctx.store.get("question")
69
+ plan = await self.llm.acomplete("""Update the plan based on the plan step result.
70
+ Note that each plan step should contain enough context to execute the step and formulate the steps in a way that can be executed by the agent.
71
+ Maximum 7 steps.
72
+ Return only the updated plan, no other text.
73
+ The plan step: """ + ev.planStep + """
74
+ The plan step result: """ + ev.planStepResult + """
75
+ The question: """ + question + """
76
+ The plan: """ + plan)
77
+ await ctx.store.set("plan", str(plan))
78
+
79
+ verdict = await self.llm.acomplete("""Check the question and the plan. If there is enough information to answer the question, then return answer with the following template: FINAL ANSWER: [FINAL ANSWER].
80
+ Otherwise return an empty string.
81
+ The question: """ + question + """
82
+ The plan: """ + str(plan))
83
+ print(f'Plan is {plan} \n\nVerdict is {verdict}')
84
+ if "FINAL ANSWER" in str(verdict):
85
+ return FinalAnswerEvent(finalAnswer=str(verdict).split("FINAL ANSWER:")[1])
86
+
87
+ step = await self.llm.acomplete("""Get the next step to evaluate of the plan. Return only the step, no other text.
88
+ The plan: """ + str(plan))
89
+ return EvalPlanEvent(planStep=str(step))
90
+
91
+
92
+ @step
93
+ async def finalAnswerStep(self, ctx: Context, ev: FinalAnswerEvent) -> StopEvent:
94
+ if not hasattr(ev, "finalAnswer"):
95
+ raise ValueError("finalAnswer field is required")
96
+
97
+ question = await ctx.store.get("question")
98
+ formattedAnswer = await self.llm.acomplete("""
99
+ Help me to format the answer to the correct format. Return only the formatted answer, no other text.
100
+ <question>
101
+ """ + question + """
102
+ </question>
103
+ <answer>
104
+ """ + ev.finalAnswer + """
105
+ </answer>
106
+ <formatting rules>
107
+ Answer should be a number OR as few words as possible OR a comma separated list of numbers and/or strings.
108
+ If you are asked for a number, don't use comma to write your number neither use units such as $ or percent sign unless specified otherwise.
109
+ If you are asked for a string, don't use articles, neither abbreviations (e.g. for cities), and write the digits in plain text unless specified otherwise.
110
+ If you are asked for a comma separated list, apply the above rules depending of whether the element to be put in the list is a number or a string.
111
+ </formatting rules>"""
112
+ )
113
+
114
+ return StopEvent(result=str(formattedAnswer))
115
+
116
+ def callSync(self, question: str) -> str:
117
+ return asyncio.run(self.run(question=question))
118
+
119
+ if __name__ == "__main__":
120
+ async def main():
121
+ questionAlbums = "How many studio albums were published by Mercedes Sosa between 2000 and 2009 (included)? You can use the latest 2022 version of english wikipedia."
122
+ questionReverse = ".rewsna eht sa \"tfel\" drow eht fo etisoppo eht etirw ,ecnetnes siht dnatsrednu uoy fI"
123
+ questionDinosaur = "Who nominated the only Featured Article on English Wikipedia about a dinosaur that was promoted in November 2016?"
124
+ questionTable = "Given this table defining * on the set S = {a, b, c, d, e}\n\n|*|a|b|c|d|e|\n|---|---|---|---|---|---|\n|a|a|b|c|b|d|\n|b|b|c|a|e|c|\n|c|c|a|b|b|a|\n|d|b|e|b|e|d|\n|e|d|b|a|d|c|\n\nprovide the subset of S involved in any possible counter-examples that prove * is not commutative. Provide your answer as a comma separated list of the elements in the set in alphabetical order."
125
+ questionSurname = "What is the surname of the equine veterinarian mentioned in 1.E Exercises from the chemistry materials licensed by Marisa Alviar-Agnew & Henry Agnew under the CK-12 license in LibreText's Introductory Chemistry materials as compiled 08/21/2023?"
126
+
127
+ from dotenv import load_dotenv
128
+ load_dotenv()
129
+ workflow = MultiStepWorkflow(timeout=300, verbose=False)
130
+ response = await workflow.run(question=questionSurname)
131
+ print(response)
132
+
133
+ asyncio.run(main())