Spaces:
Sleeping
Sleeping
Commit ·
2e205d9
1
Parent(s): 755ab7d
Modify logic in react_agent
Browse files- 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
|
| 18 |
-
|
| 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 |
-
|
| 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',
|
| 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:
|
| 92 |
-
- For scheduling:
|
| 93 |
-
- For reviews:
|
| 94 |
-
-
|
| 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'
|
| 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(
|
| 236 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 237 |
return AgentDecision(**decision_data)
|
| 238 |
except Exception as e:
|
| 239 |
logger.error(f"Error parsing decision JSON: {e}")
|
| 240 |
-
|
| 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
|
| 257 |
"""Run the ReAct agent"""
|
| 258 |
self.chat_history.append(HumanMessage(content=user_input))
|
| 259 |
|
| 260 |
agent_scratchpad = ""
|
| 261 |
|
| 262 |
-
|
| 263 |
-
|
| 264 |
-
|
| 265 |
-
|
| 266 |
-
|
| 267 |
-
|
| 268 |
-
|
| 269 |
-
|
| 270 |
-
|
| 271 |
-
|
| 272 |
-
|
| 273 |
-
|
|
|
|
| 274 |
|
| 275 |
-
|
| 276 |
-
|
| 277 |
-
|
| 278 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 279 |
|
| 280 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 281 |
try:
|
| 282 |
-
|
| 283 |
-
|
| 284 |
-
|
| 285 |
-
|
| 286 |
-
|
| 287 |
-
|
| 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 |
-
|
| 325 |
-
|
| 326 |
-
|
| 327 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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")
|