diff --git a/README.md b/README.md index 6d69ec1..670d959 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,22 @@ ## langchain-crash-course +This repository contains a crash course on Langchain, a framework for developing applications using large language models (LLMs). The course covers various aspects of working with LLMs, including chat models, prompt templates, chains, retrieval augmented generation (RAG), agents, and tools. It also includes examples of using different LLM providers and Ollama models. + +## Course Outline + +1. Setup Environment +2. Chat Models +3. Prompt Templates +4. Chains +5. RAG +6. Agents & Tools + +### Prerequisites + +- Python 3.10 or 3.11 +- Poetry + ### 1_chat_models - 1_chat_model_basic.py @@ -38,10 +54,12 @@ - 8_rag_web_scrape_firecrawl.py - rag_with_metadata.py -### Ollama Models - -- llama3.2:3b Simple, quick tasks - -- gemma3:4b Balanced performance +### 5_agents_tools -- deepseek-r1:14b Complex analysis and best quality +- agent_tools_basic.py +- agent_react_chat.py +- agent_react_rag_context.py +- rag.py +- tools/tool_basetool.py +- tools/tool_constructor.py +- tools/tool_decorator.py diff --git a/langchain-crash-course/5_agents_tools/agent_tools_basic.py b/langchain-crash-course/5_agents_tools/agent_tools_basic.py index 826f06b..a294ae3 100644 --- a/langchain-crash-course/5_agents_tools/agent_tools_basic.py +++ b/langchain-crash-course/5_agents_tools/agent_tools_basic.py @@ -149,7 +149,7 @@ async def main() -> None: while True: try: # User Query - query: str = (await ainput("You: ")).strip() + query: str = (await ainput(prompt="You: ")).strip() if not query: continue diff --git a/langchain-crash-course/5_agents_tools/tools/tool_basetool.py b/langchain-crash-course/5_agents_tools/tools/tool_basetool.py new file mode 100644 index 0000000..ba559dc --- /dev/null +++ b/langchain-crash-course/5_agents_tools/tools/tool_basetool.py @@ -0,0 +1,189 @@ +""" +Demonstrates how to create and use LangChain tools by subclassing BaseTool. + +This script defines two custom tools: +1. SimpleWebSearchTool: A tool that uses the Tavily API to perform a web search. +2. MultiplyNumbersTool: A simple tool to multiply two numbers. + +It then creates a LangChain agent that can use these tools and runs an interactive +chat session where the user can interact with the agent. This approach provides +fine-grained control over the tool's implementation. +""" + +# Import standard libraries +import asyncio +import os +import sys +from logging import Logger +from pathlib import Path +from typing import Any, Dict, Type + +# Add parent directory to path +sys.path.append(str(Path(__file__).parent.parent)) + +# Import necessary libraries +from aioconsole import ainput +from dotenv import load_dotenv + +# Import langchain modules +from langchain import hub +from langchain.agents import AgentExecutor, create_tool_calling_agent +from langchain_core.messages import AIMessage, HumanMessage +from langchain_core.messages.base import BaseMessage +from langchain_core.runnables import Runnable +from langchain_core.tools import BaseTool +from langchain_ollama import ChatOllama +from pydantic import BaseModel, Field +from tavily import TavilyClient + +# Import custom logger +from util.logger import ReActAgentLogger + +# Load environment variables +load_dotenv() + +# Module path +module_path: Path = Path(__file__).resolve() + +# Set logger +logger: Logger = ReActAgentLogger.get_logger(module_name=module_path.name) + +# ==================== Define tools==================== + + +class SimpleWebSearch(BaseModel): + """Input model for the SimpleWebSearchTool, specifying the search query.""" + + query: str = Field(description="Search query") + + +class MultiplyNumbers(BaseModel): + """Input model for the MultiplyNumbersTool, specifying the two numbers to multiply.""" + + num1: float = Field(description="First number") + num2: float = Field(description="Second number") + + +class SimpleWebSearchTool(BaseTool): + """Tool for searching the web.""" + + name: str = "Simple_Web_Search" + description: str = "Useful for searching the web." + args_schema: Type[BaseModel] = SimpleWebSearch + + def _run(self, query: str) -> str: + """Executes the web search synchronously.""" + try: + client = TavilyClient(api_key=os.getenv("TAVILY_API_KEY")) + results: Dict[str, Any] = client.search(query=query) + return f"Search results for: {query}\n\n{results}\n" + except Exception as e: + return f"An error occurred during the Tavily search: {e}" + + async def _arun(self, query: str) -> str: + """Executes the web search asynchronously.""" + return await asyncio.to_thread(self._run, query) + + +class MultiplyNumbersTool(BaseTool): + """Tool for multiplying two numbers.""" + + name: str = "Multiply_Numbers" + description: str = "Useful for multiplying two numbers." + args_schema: Type[BaseModel] = MultiplyNumbers + + def _run(self, num1: float, num2: float) -> str: + """Executes the multiplication synchronously.""" + result: float = num1 * num2 + return f"The product of {num1} and {num2} is {result}\n" + + +tools: list = [ + SimpleWebSearchTool(), # Simple web search tool + MultiplyNumbersTool(), # Multiply numbers tool +] + +# ==================== Create LMM and Agent ==================== +# Create Chat Model +llm = ChatOllama(model="llama3.2:3b") + +# pull prompt template from hub +prompt_template: Any = hub.pull(owner_repo_commit="hwchase17/openai-tools-agent") + +# Create an agent +agent: Runnable[Any, Any] = create_tool_calling_agent( + llm=llm, # llm to use + tools=tools, # tools to use + prompt=prompt_template, # prompt to use +) + +# Create agent executor +agent_executor: AgentExecutor = AgentExecutor.from_agent_and_tools( + agent=agent, # agent to use + tools=tools, # tools to use + verbose=True, # prints out the agent's thought process + handle_parsing_errors=True, # gracefully handles errors in parsing the agent output +) + + +# ==================== Run tools calling agent ==================== +async def main() -> None: + """ + Runs the main asynchronous loop for the chat application. + + This function initializes the agent, handles API key checks, and manages + the interactive chat session with the user, including history management + and graceful exit. + """ + # Check TAVILY_API_KEY + if not os.getenv("TAVILY_API_KEY"): + print( + "\n[ERROR] TAVILY_API_KEY not found in environment variables." + "\nPlease set the key in your .env file to use the web search tool." + ) + sys.exit(1) # Exit the application with a non-zero status code + + logger.info(msg="========= Start BaseTool Calling AI Agent Application ==========") + print("Type 'exit' to end the conversation.") + + # Initialize chat history + chat_history: list[BaseMessage] = [] + + while True: + try: + query: str = (await ainput(prompt="You: ")).strip() + + if not query: + print("Please ask a question!.") + continue + + if query.lower() == "exit": + logger.info(msg="User exited conversation") + print("Exiting...") + break + + # Process user query through agent executor + response: Any = await agent_executor.ainvoke( + input={"input": query, "chat_history": chat_history} + ) + + # Display AI response + if response: + logger.info(msg=f"AI: {response['output']:100}.....") + print(f"AI: {response['output']}") + + # Update chat history + chat_history.append(HumanMessage(content=query)) + chat_history.append(AIMessage(content=response["output"])) + + except (KeyboardInterrupt, EOFError, asyncio.CancelledError): + logger.info(msg="Keyboard interrupt or EOF error") + print("Exiting...") + break + + except Exception as e: + logger.error(msg=f"Unexpected error: {e}") + + +if __name__ == "__main__": + asyncio.run(main=main()) diff --git a/langchain-crash-course/5_agents_tools/tools/tool_constructor.py b/langchain-crash-course/5_agents_tools/tools/tool_constructor.py index 708426b..b009a7f 100644 --- a/langchain-crash-course/5_agents_tools/tools/tool_constructor.py +++ b/langchain-crash-course/5_agents_tools/tools/tool_constructor.py @@ -58,10 +58,10 @@ class ConcatenateStringsArgs(BaseModel): description="Useful for reversing a string.", ), StructuredTool.from_function( - func=concatenate_strings, - name="Concatenate Strings", - description="Useful for concatenating two strings.", - args_schema=ConcatenateStringsArgs, + func=concatenate_strings, # function to call + name="Concatenate Strings", # name of the tool + description="Useful for concatenating two strings.", # description of the tool + args_schema=ConcatenateStringsArgs, # args schema for the tool ), ] @@ -75,21 +75,24 @@ class ConcatenateStringsArgs(BaseModel): # ==================== Create agent==================== agent: Runnable[Any, Any] = create_tool_calling_agent( - llm=llm, - tools=tools, - prompt=prompt_template, + llm=llm, # llm to use + tools=tools, # tools to use + prompt=prompt_template, # prompt to use ) # ==================== Create agent executor==================== agent_executor: AgentExecutor = AgentExecutor.from_agent_and_tools( - agent=agent, tools=tools, verbose=True, handle_parsing_errors=True + agent=agent, # agent to use + tools=tools, # tools to use + verbose=True, # prints out the agent's thought process + handle_parsing_errors=True, # gracefully handles errors in parsing the agent output ) # ==================== Run tools calling agent ==================== async def main() -> None: print( - "\nStart chatting with Tool Calling Agent AI! Type 'exit' to end the conversation." + "\nStart chatting with Constructor Tool Calling Agent AI! Type 'exit' to end the conversation." ) # Initialize chat history diff --git a/langchain-crash-course/5_agents_tools/tools/tool_decorator.py b/langchain-crash-course/5_agents_tools/tools/tool_decorator.py new file mode 100644 index 0000000..4c8d154 --- /dev/null +++ b/langchain-crash-course/5_agents_tools/tools/tool_decorator.py @@ -0,0 +1,123 @@ +# tool_constructor.py + +# Import standard libraries +import asyncio +from typing import Any + +# Import necessary libraries +from aioconsole import ainput +from dotenv import load_dotenv + +# Import langchain modules +from langchain import hub +from langchain.agents import AgentExecutor, create_tool_calling_agent +from langchain_core.messages import AIMessage, HumanMessage +from langchain_core.messages.base import BaseMessage +from langchain_core.runnables import Runnable +from langchain_core.tools import tool +from langchain_ollama import ChatOllama +from pydantic import BaseModel, Field + +# Load environment variables +load_dotenv() # LangSmith API key required for tracing + + +# ==================== Define tools==================== +@tool +def greet_user(name: str) -> str: + """Greet the user.""" + return f"Hello, {name}! Welcome to LangChain." + + +@tool +def reverse_string(text: str) -> str: + """Reverses the given string.""" + return text[::-1] + + +class ConcatenateStringsArgs(BaseModel): + """Input for concatenate_strings.""" + + text1: str = Field(description="First string") + text2: str = Field(description="Second string") + + +# args_schema is for structured tools +@tool(args_schema=ConcatenateStringsArgs) +def concatenate_strings(text1: str, text2: str) -> str: + """Concatenate two strings.""" + return text1 + text2 + + +# ==================== Create tools ==================== +tools: list = [ + greet_user, + reverse_string, + concatenate_strings, +] + +# ==================== Create LLM==================== +# Create Chat Model +llm = ChatOllama(model="llama3.2:3b") + +# pull prompt template from hub +prompt_template: Any = hub.pull(owner_repo_commit="hwchase17/openai-tools-agent") + + +# ==================== Create agent==================== +agent: Runnable[Any, Any] = create_tool_calling_agent( + llm=llm, + tools=tools, + prompt=prompt_template, +) + +# ==================== Create agent executor==================== +agent_executor: AgentExecutor = AgentExecutor.from_agent_and_tools( + agent=agent, tools=tools, verbose=True, handle_parsing_errors=True +) + + +# ==================== Run tools calling agent ==================== +async def main() -> None: + print( + "\nStart chatting with Decorator Tool Calling Agent AI! Type 'exit' to end the conversation." + ) + + # Initialize chat history + chat_history: list[BaseMessage] = [] + + while True: + try: + query: str = (await ainput(prompt="You: ")).strip() + + if not query: + print("Please ask a question!.") + continue + + if query.lower() == "exit": + print("Exiting...") + break + + # Process user query through agent executor + response: Any = await agent_executor.ainvoke( + input={"input": query, "chat_history": chat_history} + ) + + # Display AI response + if response: + print(f"AI: {response['output']}") + + # Update chat history + chat_history.append(HumanMessage(content=query)) + chat_history.append(AIMessage(content=response["output"])) + + except (KeyboardInterrupt, EOFError, asyncio.CancelledError): + print("Exiting...") + break + + except Exception as e: + print(f"Unexpected error: {e}") + + +if __name__ == "__main__": + asyncio.run(main=main()) diff --git a/pyproject.toml b/pyproject.toml index 60dfc7d..24e28d8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,6 +22,7 @@ dependencies = [ "beautifulsoup4>=4.13.5", "aioconsole>=0.8.1", "wikipedia>=1.4.0", + "tavily>=1.1.0", ] [dependency-groups] diff --git a/uv.lock b/uv.lock index bc5c0de..e6969c8 100644 --- a/uv.lock +++ b/uv.lock @@ -1519,6 +1519,7 @@ dependencies = [ { name = "python-dotenv" }, { name = "sentence-transformers" }, { name = "streamlit" }, + { name = "tavily" }, { name = "wikipedia" }, ] @@ -1547,6 +1548,7 @@ requires-dist = [ { name = "python-dotenv", specifier = ">=1.0.1" }, { name = "sentence-transformers", specifier = ">=5.1.0" }, { name = "streamlit", specifier = ">=1.43.2" }, + { name = "tavily", specifier = ">=1.1.0" }, { name = "wikipedia", specifier = ">=1.4.0" }, ] @@ -3399,6 +3401,16 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a2/09/77d55d46fd61b4a135c444fc97158ef34a095e5681d0a6c10b75bf356191/sympy-1.14.0-py3-none-any.whl", hash = "sha256:e091cc3e99d2141a0ba2847328f5479b05d94a6635cb96148ccb3f34671bd8f5", size = 6299353 }, ] +[[package]] +name = "tavily" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohttp" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/63/ba/cd74acdb0537a02fb5657afbd5fd5a27a298c85fc27f544912cc001377bb/tavily-1.1.0.tar.gz", hash = "sha256:7730bf10c925dc0d0d84f27a8979de842ecf88c2882183409addd855e27d8fab", size = 5081 } + [[package]] name = "tenacity" version = "9.1.2"