From a8dc4aa21fc186d7ee2cb145d7dae02eaab44872 Mon Sep 17 00:00:00 2001 From: SystemSolution21 <156997764+SystemSolution21@users.noreply.github.com> Date: Sat, 8 Nov 2025 23:31:17 +0900 Subject: [PATCH 1/6] new file: langchain-crash-course/5_agents_tools/tools/tool_decorator.py --- .../5_agents_tools/tools/tool_decorator.py | 123 ++++++++++++++++++ 1 file changed, 123 insertions(+) create mode 100644 langchain-crash-course/5_agents_tools/tools/tool_decorator.py 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..74bdb0f --- /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 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()) From fdf802d26c2585f84ea99313612eb8cdd5958df8 Mon Sep 17 00:00:00 2001 From: SystemSolution21 <156997764+SystemSolution21@users.noreply.github.com> Date: Sun, 9 Nov 2025 17:57:15 +0900 Subject: [PATCH 2/6] modified: langchain-crash-course/5_agents_tools/tools/tool_constructor.py --- .../5_agents_tools/tools/tool_constructor.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) 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..ffa762b 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,14 +75,17 @@ 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 ) From ddc8cdc368e53c85135f716b590c88fcf8f64882 Mon Sep 17 00:00:00 2001 From: SystemSolution21 <156997764+SystemSolution21@users.noreply.github.com> Date: Sun, 9 Nov 2025 19:07:05 +0900 Subject: [PATCH 3/6] new file: langchain-crash-course/5_agents_tools/tools/tool_basetool.py modified: langchain-crash-course/5_agents_tools/tools/tool_constructor.py modified: langchain-crash-course/5_agents_tools/tools/tool_decorator.py modified: pyproject.toml modified: uv.lock --- .../5_agents_tools/tools/tool_basetool.py | 144 ++++++++++++++++++ .../5_agents_tools/tools/tool_constructor.py | 2 +- .../5_agents_tools/tools/tool_decorator.py | 2 +- pyproject.toml | 1 + uv.lock | 12 ++ 5 files changed, 159 insertions(+), 2 deletions(-) create mode 100644 langchain-crash-course/5_agents_tools/tools/tool_basetool.py 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..8185d90 --- /dev/null +++ b/langchain-crash-course/5_agents_tools/tools/tool_basetool.py @@ -0,0 +1,144 @@ +# tool_basetool.py + +# Import standard libraries +import asyncio +import os +from typing import Any, Dict, Type + +# 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 + +# Load environment variables +load_dotenv() + + +# ==================== Define tools==================== + + +class SimpleWebSearch(BaseModel): + """Input for simple_web_search.""" + + query: str = Field(description="Search query") + + +class MultiplyNumbers(BaseModel): + """Input for multiply_numbers.""" + + 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: + """Use the tool.""" + api_key: str | None = os.getenv(key="TAVILY_API_KEY") + client = TavilyClient(api_key=api_key) + results: Dict[str, Any] = client.search(query=query) + return f"Search results for: {query}\n\n{results}\n" + + +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: + """Use the tool.""" + result: float = num1 * num2 + return f"The product of {num1} and {num2} is {result}\n" + + +# ==================== Create tools using BaseTool==================== +tools: list = [ + SimpleWebSearchTool(), # Simple web search tool + MultiplyNumbersTool(), # Multiply numbers tool +] + +# ==================== 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, # 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: + print( + "\nStart chatting with BaseTool 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/langchain-crash-course/5_agents_tools/tools/tool_constructor.py b/langchain-crash-course/5_agents_tools/tools/tool_constructor.py index ffa762b..b009a7f 100644 --- a/langchain-crash-course/5_agents_tools/tools/tool_constructor.py +++ b/langchain-crash-course/5_agents_tools/tools/tool_constructor.py @@ -92,7 +92,7 @@ class ConcatenateStringsArgs(BaseModel): # ==================== 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 index 74bdb0f..4c8d154 100644 --- a/langchain-crash-course/5_agents_tools/tools/tool_decorator.py +++ b/langchain-crash-course/5_agents_tools/tools/tool_decorator.py @@ -80,7 +80,7 @@ def concatenate_strings(text1: str, text2: str) -> str: # ==================== 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 Decorator Tool Calling Agent AI! Type 'exit' to end the conversation." ) # Initialize chat history 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" From c90dc4521553bd27869f5b2b11f40c43f263e5c9 Mon Sep 17 00:00:00 2001 From: SystemSolution21 <156997764+SystemSolution21@users.noreply.github.com> Date: Sun, 9 Nov 2025 22:48:41 +0900 Subject: [PATCH 4/6] modified: langchain-crash-course/5_agents_tools/agent_tools_basic.py modified: langchain-crash-course/5_agents_tools/tools/tool_basetool.py --- .../5_agents_tools/agent_tools_basic.py | 2 +- .../5_agents_tools/tools/tool_basetool.py | 78 ++++++++++++++----- 2 files changed, 61 insertions(+), 19 deletions(-) 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 index 8185d90..3588d8c 100644 --- a/langchain-crash-course/5_agents_tools/tools/tool_basetool.py +++ b/langchain-crash-course/5_agents_tools/tools/tool_basetool.py @@ -1,8 +1,21 @@ -# tool_basetool.py +""" +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 # Import necessary libraries @@ -20,21 +33,29 @@ 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 for simple_web_search.""" + """Input model for the SimpleWebSearchTool, specifying the search query.""" query: str = Field(description="Search query") class MultiplyNumbers(BaseModel): - """Input for multiply_numbers.""" + """Input model for the MultiplyNumbersTool, specifying the two numbers to multiply.""" num1: float = Field(description="First number") num2: float = Field(description="Second number") @@ -48,11 +69,17 @@ class SimpleWebSearchTool(BaseTool): args_schema: Type[BaseModel] = SimpleWebSearch def _run(self, query: str) -> str: - """Use the tool.""" - api_key: str | None = os.getenv(key="TAVILY_API_KEY") - client = TavilyClient(api_key=api_key) - results: Dict[str, Any] = client.search(query=query) - return f"Search results for: {query}\n\n{results}\n" + """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): @@ -63,33 +90,31 @@ class MultiplyNumbersTool(BaseTool): args_schema: Type[BaseModel] = MultiplyNumbers def _run(self, num1: float, num2: float) -> str: - """Use the tool.""" + """Executes the multiplication synchronously.""" result: float = num1 * num2 return f"The product of {num1} and {num2} is {result}\n" -# ==================== Create tools using BaseTool==================== tools: list = [ SimpleWebSearchTool(), # Simple web search tool MultiplyNumbersTool(), # Multiply numbers tool ] -# ==================== Create LLM==================== +# ==================== 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 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==================== +# Create agent executor agent_executor: AgentExecutor = AgentExecutor.from_agent_and_tools( agent=agent, # agent to use tools=tools, # tools to use @@ -100,9 +125,23 @@ def _run(self, num1: float, num2: float) -> str: # ==================== Run tools calling agent ==================== async def main() -> None: - print( - "\nStart chatting with BaseTool Calling Agent AI! Type 'exit' to end the conversation." - ) + """ + 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] = [] @@ -116,6 +155,7 @@ async def main() -> None: continue if query.lower() == "exit": + logger.info(msg="User exited conversation") print("Exiting...") break @@ -126,6 +166,7 @@ async def main() -> None: # Display AI response if response: + logger.info(msg=f"AI: {response['output']:100}.....") print(f"AI: {response['output']}") # Update chat history @@ -133,11 +174,12 @@ async def main() -> None: 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: - print(f"Unexpected error: {e}") + logger.error(msg=f"Unexpected error: {e}") if __name__ == "__main__": From edc26b1adb6ccc6dfda062c46cad6e1c5795d57f Mon Sep 17 00:00:00 2001 From: SystemSolution21 <156997764+SystemSolution21@users.noreply.github.com> Date: Sun, 9 Nov 2025 23:55:27 +0900 Subject: [PATCH 5/6] modified: langchain-crash-course/5_agents_tools/tools/tool_basetool.py --- langchain-crash-course/5_agents_tools/tools/tool_basetool.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/langchain-crash-course/5_agents_tools/tools/tool_basetool.py b/langchain-crash-course/5_agents_tools/tools/tool_basetool.py index 3588d8c..ba559dc 100644 --- a/langchain-crash-course/5_agents_tools/tools/tool_basetool.py +++ b/langchain-crash-course/5_agents_tools/tools/tool_basetool.py @@ -18,6 +18,9 @@ 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 From 055fe5168cd31d0c74796123fc4c4f92b5590b40 Mon Sep 17 00:00:00 2001 From: SystemSolution21 <156997764+SystemSolution21@users.noreply.github.com> Date: Tue, 11 Nov 2025 15:42:27 +0900 Subject: [PATCH 6/6] modified: README.md --- README.md | 30 ++++++++++++++++++++++++------ 1 file changed, 24 insertions(+), 6 deletions(-) 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