yuki-zmstr commited on
Commit
2e205d9
·
1 Parent(s): 755ab7d

Modify logic in react_agent

Browse files
Files changed (1) hide show
  1. agent/react_agent.py +77 -101
agent/react_agent.py CHANGED
@@ -5,7 +5,10 @@ ReAct Agent for CapyRead - A reasoning and acting agent that can:
5
  3. Create book reviews in Notion
6
  4. Handle general conversation
7
  """
8
-
 
 
 
9
  from typing import Dict, List, Optional, Any
10
  from langchain_core.messages import BaseMessage, HumanMessage, AIMessage, SystemMessage
11
  from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
@@ -14,17 +17,13 @@ from langchain_openai import ChatOpenAI
14
  from langchain_core.agents import AgentAction, AgentFinish
15
  from langchain_core.output_parsers import PydanticOutputParser
16
  from pydantic import BaseModel, Field
17
- import json
18
- import re
19
- import os
20
- import logging
21
  from services.book_service import BookService
22
  from services.calendar_service import CalendarService
23
  from services.notion_service import NotionService
24
 
25
- # Configure LangSmith tracing
26
- os.environ["LANGCHAIN_TRACING_V2"] = "true"
27
- os.environ["LANGCHAIN_PROJECT"] = "capyread-react-agent"
28
 
29
  # Configure logging
30
  logging.basicConfig(level=logging.INFO)
@@ -35,7 +34,7 @@ class AgentDecision(BaseModel):
35
  """Model for agent's reasoning and action decision"""
36
  thought: str = Field(description="The agent's reasoning about what to do")
37
  action: str = Field(
38
- description="The action to take: 'book_recommendation', 'schedule_reading', 'create_review', 'conversation', or 'FINAL_ANSWER'")
39
  action_input: str = Field(description="The input for the chosen action")
40
 
41
 
@@ -72,7 +71,6 @@ You have access to the following tools:
72
  1. book_recommendation - Get book recommendations from OpenLibrary
73
  2. schedule_reading - Schedule reading time in Google Calendar
74
  3. create_review - Create book review/notes in Notion
75
- 4. conversation - Handle general conversation
76
 
77
  Use the ReAct (Reasoning and Acting) framework:
78
  1. THOUGHT: Think about what the user wants and what action to take
@@ -88,10 +86,10 @@ For each response, provide your reasoning in JSON format:
88
  }
89
 
90
  Guidelines:
91
- - For book recommendations: Ask for genres/preferences if not specified
92
- - For scheduling: Confirm book selection and get time preferences
93
- - For reviews: Ask for book details and user's thoughts
94
- - For general chat: Respond naturally as a helpful reading assistant
95
  - Always be friendly and enthusiastic about reading!
96
 
97
  Available tools:
@@ -110,7 +108,7 @@ Available tools:
110
  ),
111
  Tool(
112
  name="schedule_reading",
113
- description="Schedule reading time in Google Calendar. Input should be a JSON string with 'book_title', 'duration_minutes', 'preferred_time' (optional)",
114
  func=self._schedule_reading
115
  ),
116
  Tool(
@@ -118,11 +116,6 @@ Available tools:
118
  description="Create a book review or reading notes in Notion. Input should be a JSON string with 'book_title', 'author', 'review_text', 'rating' (1-5)",
119
  func=self._create_review
120
  ),
121
- Tool(
122
- name="conversation",
123
- description="Handle general conversation about reading, books, or other topics. Input is the conversation text.",
124
- func=self._handle_conversation
125
- )
126
  ]
127
 
128
  def _recommend_books(self, input_str: str) -> str:
@@ -161,7 +154,6 @@ Available tools:
161
  params = json.loads(input_str)
162
  book_title = params.get("book_title", "")
163
  duration = params.get("duration_minutes", 30)
164
- preferred_time = params.get("preferred_time", "")
165
 
166
  if not book_title:
167
  return "Please specify a book title to schedule reading time for."
@@ -169,7 +161,6 @@ Available tools:
169
  result = self.calendar_service.schedule_reading_session(
170
  book_title=book_title,
171
  duration_minutes=duration,
172
- preferred_time=preferred_time
173
  )
174
 
175
  if result["success"]:
@@ -207,20 +198,6 @@ Available tools:
207
  except Exception as e:
208
  return f"Error creating review: {str(e)}"
209
 
210
- def _handle_conversation(self, input_str: str) -> str:
211
- """Tool for general conversation"""
212
- conversation_prompt = f"""
213
- You are CapyRead, a friendly AI reading assistant. The user said: "{input_str}"
214
-
215
- Respond naturally and helpfully. If they're asking about books, reading, or related topics,
216
- provide enthusiastic and knowledgeable responses. If they need specific actions like
217
- book recommendations, scheduling, or reviews, guide them appropriately.
218
- """
219
-
220
- response = self.llm.invoke(
221
- [SystemMessage(content=conversation_prompt)])
222
- return response.content
223
-
224
  def _parse_agent_decision(self, text: str) -> AgentDecision:
225
  """Parse the agent's decision from LLM output"""
226
  # Try to extract JSON from the response
@@ -229,15 +206,19 @@ Available tools:
229
  try:
230
  decision_data = json.loads(json_match.group())
231
  logger.info(f"Parsed agent decision: {decision_data}")
232
-
233
  # Convert action_input to string if it's a dict/object
234
  if isinstance(decision_data.get('action_input'), (dict, list)):
235
- decision_data['action_input'] = json.dumps(decision_data['action_input'])
236
-
 
 
 
 
237
  return AgentDecision(**decision_data)
238
  except Exception as e:
239
  logger.error(f"Error parsing decision JSON: {e}")
240
- pass
241
 
242
  # Fallback parsing
243
  if "FINAL_ANSWER" in text:
@@ -253,79 +234,74 @@ Available tools:
253
  action_input=text
254
  )
255
 
256
- def run(self, user_input: str, max_iterations: int = 5) -> str:
257
  """Run the ReAct agent"""
258
  self.chat_history.append(HumanMessage(content=user_input))
259
 
260
  agent_scratchpad = ""
261
 
262
- for iteration in range(max_iterations):
263
- logger.info(f"iteration: {iteration}")
264
- # Format the prompt
265
- formatted_prompt = self.react_prompt.format(
266
- tools="\n".join(
267
- [f"- {tool.name}: {tool.description}" for tool in self.tools]),
268
- format_instructions=self.parser.get_format_instructions(),
269
- # All except current input
270
- chat_history=self.chat_history[:-1],
271
- input=user_input,
272
- agent_scratchpad=agent_scratchpad
273
- )
 
274
 
275
- # Get LLM response
276
- response = self.llm.invoke(
277
- [HumanMessage(content=formatted_prompt)])
278
- logger.info(f"LLM response: {response.content}")
 
 
 
 
 
 
279
 
280
- # Parse the decision
 
 
 
 
 
 
 
 
 
 
 
 
281
  try:
282
- decision = self._parse_agent_decision(response.content)
283
- except Exception as e:
284
- # Fallback to conversation
285
- decision = AgentDecision(
286
- thought=f"Error parsing decision: {e}",
287
- action="conversation",
288
- action_input=response.content
289
- )
290
-
291
- agent_scratchpad += f"Thought: {decision.thought}\n"
292
-
293
- # Check if final answer
294
- if decision.action == "FINAL_ANSWER":
295
- final_response = decision.action_input
296
- self.chat_history.append(AIMessage(content=final_response))
297
- return final_response
298
-
299
- # Execute the tool
300
- if decision.action in self.tool_map:
301
- tool = self.tool_map[decision.action]
302
- agent_scratchpad += f"Action: {decision.action}\n"
303
- agent_scratchpad += f"Action Input: {decision.action_input}\n"
304
-
305
- try:
306
- logger.info(f"Executing tool: {decision.action}")
307
- observation = tool.func(decision.action_input)
308
-
309
- # If it's a book recommendation and we got results successfully
310
- if decision.action == "book_recommendation" and "Here are some book recommendations:" in observation:
311
- self.chat_history.append(AIMessage(content=observation))
312
- return observation
313
-
314
- except Exception as e:
315
- observation = f"Error: {str(e)}"
316
-
317
- agent_scratchpad += f"Observation: {observation}\n"
318
- else:
319
- # Unknown action, treat as conversation
320
- observation = self._handle_conversation(decision.action_input)
321
  self.chat_history.append(AIMessage(content=observation))
322
  return observation
323
 
324
- # If we reach max iterations, return the last observation
325
- fallback_response = "I apologize, but I'm having trouble processing your request. Could you please rephrase what you'd like me to help you with?"
326
- self.chat_history.append(AIMessage(content=fallback_response))
327
- return fallback_response
 
 
 
 
328
 
329
  def reset_conversation(self):
330
  """Reset the conversation history"""
331
  self.chat_history = []
 
 
 
 
 
 
5
  3. Create book reviews in Notion
6
  4. Handle general conversation
7
  """
8
+ import json
9
+ import re
10
+ import os
11
+ import logging
12
  from typing import Dict, List, Optional, Any
13
  from langchain_core.messages import BaseMessage, HumanMessage, AIMessage, SystemMessage
14
  from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
 
17
  from langchain_core.agents import AgentAction, AgentFinish
18
  from langchain_core.output_parsers import PydanticOutputParser
19
  from pydantic import BaseModel, Field
20
+ from dotenv import load_dotenv
21
+
 
 
22
  from services.book_service import BookService
23
  from services.calendar_service import CalendarService
24
  from services.notion_service import NotionService
25
 
26
+ load_dotenv()
 
 
27
 
28
  # Configure logging
29
  logging.basicConfig(level=logging.INFO)
 
34
  """Model for agent's reasoning and action decision"""
35
  thought: str = Field(description="The agent's reasoning about what to do")
36
  action: str = Field(
37
+ description="The action to take: 'book_recommendation', 'schedule_reading', 'create_review', or 'FINAL_ANSWER'")
38
  action_input: str = Field(description="The input for the chosen action")
39
 
40
 
 
71
  1. book_recommendation - Get book recommendations from OpenLibrary
72
  2. schedule_reading - Schedule reading time in Google Calendar
73
  3. create_review - Create book review/notes in Notion
 
74
 
75
  Use the ReAct (Reasoning and Acting) framework:
76
  1. THOUGHT: Think about what the user wants and what action to take
 
86
  }
87
 
88
  Guidelines:
89
+ - For book recommendations: You will at least need the genre or search query. If you don't have enough information, ask the user for more information.
90
+ - For scheduling: You will need the book title. The duration is optional.
91
+ - For reviews: You will need the book title, author, rating, and review text.
92
+ - Ask the user for more information if needed. Your action will be FINAL_ANSWER, and action_input will be your response
93
  - Always be friendly and enthusiastic about reading!
94
 
95
  Available tools:
 
108
  ),
109
  Tool(
110
  name="schedule_reading",
111
+ description="Schedule reading time in Google Calendar. Input should be a JSON string with 'book_title', and 'duration_minutes'",
112
  func=self._schedule_reading
113
  ),
114
  Tool(
 
116
  description="Create a book review or reading notes in Notion. Input should be a JSON string with 'book_title', 'author', 'review_text', 'rating' (1-5)",
117
  func=self._create_review
118
  ),
 
 
 
 
 
119
  ]
120
 
121
  def _recommend_books(self, input_str: str) -> str:
 
154
  params = json.loads(input_str)
155
  book_title = params.get("book_title", "")
156
  duration = params.get("duration_minutes", 30)
 
157
 
158
  if not book_title:
159
  return "Please specify a book title to schedule reading time for."
 
161
  result = self.calendar_service.schedule_reading_session(
162
  book_title=book_title,
163
  duration_minutes=duration,
 
164
  )
165
 
166
  if result["success"]:
 
198
  except Exception as e:
199
  return f"Error creating review: {str(e)}"
200
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
201
  def _parse_agent_decision(self, text: str) -> AgentDecision:
202
  """Parse the agent's decision from LLM output"""
203
  # Try to extract JSON from the response
 
206
  try:
207
  decision_data = json.loads(json_match.group())
208
  logger.info(f"Parsed agent decision: {decision_data}")
209
+
210
  # Convert action_input to string if it's a dict/object
211
  if isinstance(decision_data.get('action_input'), (dict, list)):
212
+ decision_data['action_input'] = json.dumps(
213
+ decision_data['action_input'])
214
+ elif not isinstance(decision_data.get('action_input'), str):
215
+ decision_data['action_input'] = str(
216
+ decision_data.get('action_input', ''))
217
+
218
  return AgentDecision(**decision_data)
219
  except Exception as e:
220
  logger.error(f"Error parsing decision JSON: {e}")
221
+ raise
222
 
223
  # Fallback parsing
224
  if "FINAL_ANSWER" in text:
 
234
  action_input=text
235
  )
236
 
237
+ def run(self, user_input: str) -> str:
238
  """Run the ReAct agent"""
239
  self.chat_history.append(HumanMessage(content=user_input))
240
 
241
  agent_scratchpad = ""
242
 
243
+ # Format the prompt
244
+ formatted_prompt = self.react_prompt.format(
245
+ tools="\n".join(
246
+ [f"- {tool.name}: {tool.description}" for tool in self.tools]),
247
+ format_instructions=self.parser.get_format_instructions(),
248
+ chat_history=self.chat_history[:-1],
249
+ input=user_input,
250
+ agent_scratchpad=agent_scratchpad
251
+ )
252
+
253
+ # Get LLM response
254
+ response = self.llm.invoke(
255
+ [HumanMessage(content=formatted_prompt)])
256
 
257
+ # Parse the decision
258
+ try:
259
+ decision = self._parse_agent_decision(response.content)
260
+ except Exception as e:
261
+ # Fallback to conversation
262
+ decision = AgentDecision(
263
+ thought=f"Error parsing decision: {e}",
264
+ action="conversation",
265
+ action_input=response.content
266
+ )
267
 
268
+ # Update scratchpad with thought
269
+ agent_scratchpad += f"\nThought: {decision.thought}"
270
+ agent_scratchpad += f"\nAction: {decision.action}"
271
+ agent_scratchpad += f"\nAction Input: {decision.action_input}"
272
+
273
+ # Check if final answer
274
+ if decision.action == "FINAL_ANSWER":
275
+ final_response = decision.action_input
276
+ self.chat_history.append(AIMessage(content=final_response))
277
+ return final_response
278
+ # Execute the tool
279
+ if decision.action in self.tool_map:
280
+ tool = self.tool_map[decision.action]
281
  try:
282
+ logger.info(f"Executing tool: {decision.action}")
283
+ observation = tool.func(decision.action_input)
284
+
285
+ # Update scratchpad with observation
286
+ agent_scratchpad += f"\nObservation: {observation}\n"
287
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
288
  self.chat_history.append(AIMessage(content=observation))
289
  return observation
290
 
291
+ except Exception as e:
292
+ observation = f"Error: {str(e)}"
293
+ agent_scratchpad += f"\nObservation: {observation}\n"
294
+ else:
295
+ # Unknown action, treat as conversation
296
+ observation = self._handle_conversation(decision.action_input)
297
+ self.chat_history.append(AIMessage(content=observation))
298
+ return observation
299
 
300
  def reset_conversation(self):
301
  """Reset the conversation history"""
302
  self.chat_history = []
303
+
304
+
305
+ if __name__ == "__main__":
306
+ agent = ReActAgent()
307
+ agent.run("Schedule 45 minutes to read The Three Body Problem")