├── .gitignore ├── .env.example ├── 1_chat_models ├── 1_chat_model_basic.py ├── 4_chat_model_conversation_with_user.py ├── 2_chat_model_basic_conversation.py ├── 3_chat_model_alternatives.py └── 5_chat_model_save_message_history_firebase.py ├── 3_chains ├── 1_chains_basics.py ├── 3_chains_extended.py ├── 2_chains_under_the_hood.py ├── 4_chains_parallel.py └── 5_chains_branching.py ├── 4_rag ├── utils │ └── embedding_cost_calculator.py ├── 1b_rag_basics.py ├── 2b_rag_basics_metadata.py ├── 1a_rag_basics.py ├── 6_rag_one_off_question.py ├── 2a_rag_basics_metadata.py ├── 8_rag_web_scrape_basic.py ├── books │ ├── langchain_demo.txt │ └── us_bill_of_rights.txt ├── 8_rag_web_scrape_firecrawl.py ├── 5_rag_retriever_deep_dive.py ├── 4_rag_embedding_deep_dive.py ├── 7_rag_conversational.py └── 3_rag_text_splitting_deep_dive.py ├── pyproject.toml ├── 2_prompt_templates ├── 2_prompt_template_with_chat_model.py └── 1_prompt_template_basic.py ├── 5_agents_and_tools ├── 1_agent_and_tools_basics.py ├── tools_deep_dive │ ├── 3_tool_base_tool.py │ ├── 1_tool_constructor.py │ └── 2_tool_decorator.py └── agent_deep_dive │ ├── 1_agent_react_chat.py │ └── 2_agent_react_docstore.py └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | db/ -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | OPENAI_API_KEY=ADD_KEY_HERE 2 | ANTHROPIC_API_KEY=ADD_KEY_HERE 3 | GOOGLE_API_KEY=ADD_KEY_HERE 4 | FIRECRAWL_API_KEY=ADD_KEY_HERE 5 | TAVILY_API_KEY=ADD_KEY_HERE -------------------------------------------------------------------------------- /1_chat_models/1_chat_model_basic.py: -------------------------------------------------------------------------------- 1 | # Chat Model Documents: https://python.langchain.com/v0.2/docs/integrations/chat/ 2 | # OpenAI Chat Model Documents: https://python.langchain.com/v0.2/docs/integrations/chat/openai/ 3 | 4 | from dotenv import load_dotenv 5 | from langchain_openai import ChatOpenAI 6 | 7 | # Load environment variables from .env 8 | load_dotenv() 9 | 10 | # Create a ChatOpenAI model 11 | model = ChatOpenAI(model="gpt-4o") 12 | 13 | # Invoke the model with a message 14 | result = model.invoke("What is 81 divided by 9?") 15 | print("Full result:") 16 | print(result) 17 | print("Content only:") 18 | print(result.content) 19 | -------------------------------------------------------------------------------- /3_chains/1_chains_basics.py: -------------------------------------------------------------------------------- 1 | from dotenv import load_dotenv 2 | from langchain.prompts import ChatPromptTemplate 3 | from langchain.schema.output_parser import StrOutputParser 4 | from langchain_openai import ChatOpenAI 5 | 6 | # Load environment variables from .env 7 | load_dotenv() 8 | 9 | # Create a ChatOpenAI model 10 | model = ChatOpenAI(model="gpt-4o") 11 | 12 | # Define prompt templates (no need for separate Runnable chains) 13 | prompt_template = ChatPromptTemplate.from_messages( 14 | [ 15 | ("system", "You are a comedian who tells jokes about {topic}."), 16 | ("human", "Tell me {joke_count} jokes."), 17 | ] 18 | ) 19 | 20 | # Create the combined chain using LangChain Expression Language (LCEL) 21 | chain = prompt_template | model | StrOutputParser() 22 | # chain = prompt_template | model 23 | 24 | # Run the chain 25 | result = chain.invoke({"topic": "lawyers", "joke_count": 3}) 26 | 27 | # Output 28 | print(result) 29 | -------------------------------------------------------------------------------- /4_rag/utils/embedding_cost_calculator.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import tiktoken 4 | 5 | # Define the file path for the document 6 | file_path = os.path.join(os.path.dirname(__file__), "..", "books", "odyssey.txt") 7 | 8 | # Check if the file exists 9 | if not os.path.exists(file_path): 10 | raise FileNotFoundError( 11 | f"The file {file_path} does not exist. Please check the path." 12 | ) 13 | 14 | # Read the content of the file 15 | with open(file_path, "r", encoding="utf-8") as file: 16 | text = file.read() 17 | 18 | tokenizer = tiktoken.get_encoding( 19 | "cl100k_base" 20 | ) # Use the appropriate encoding for the model 21 | 22 | # Tokenize the text and count the tokens 23 | tokens = tokenizer.encode(text) 24 | total_tokens = len(tokens) 25 | 26 | # Calculate the cost based on OpenAI's pricing 27 | cost_per_million_tokens = 0.02 # $0.02 per million tokens 28 | cost = (total_tokens / 1_000_000) * cost_per_million_tokens 29 | 30 | # Print the results 31 | print(f"Total number of tokens: {total_tokens}") 32 | print(f"Estimated cost for processing: ${cost:.6f}") 33 | -------------------------------------------------------------------------------- /1_chat_models/4_chat_model_conversation_with_user.py: -------------------------------------------------------------------------------- 1 | from dotenv import load_dotenv 2 | from langchain_openai import ChatOpenAI 3 | from langchain.schema import AIMessage, HumanMessage, SystemMessage 4 | 5 | # Load environment variables from .env 6 | load_dotenv() 7 | 8 | # Create a ChatOpenAI model 9 | model = ChatOpenAI(model="gpt-4o") 10 | 11 | 12 | chat_history = [] # Use a list to store messages 13 | 14 | # Set an initial system message (optional) 15 | system_message = SystemMessage(content="You are a helpful AI assistant.") 16 | chat_history.append(system_message) # Add system message to chat history 17 | 18 | # Chat loop 19 | while True: 20 | query = input("You: ") 21 | if query.lower() == "exit": 22 | break 23 | chat_history.append(HumanMessage(content=query)) # Add user message 24 | 25 | # Get AI response using history 26 | result = model.invoke(chat_history) 27 | response = result.content 28 | chat_history.append(AIMessage(content=response)) # Add AI message 29 | 30 | print(f"AI: {response}") 31 | 32 | 33 | print("---- Message History ----") 34 | print(chat_history) 35 | -------------------------------------------------------------------------------- /3_chains/3_chains_extended.py: -------------------------------------------------------------------------------- 1 | from dotenv import load_dotenv 2 | from langchain.prompts import ChatPromptTemplate 3 | from langchain.schema.output_parser import StrOutputParser 4 | from langchain.schema.runnable import RunnableLambda 5 | from langchain_openai import ChatOpenAI 6 | 7 | # Load environment variables from .env 8 | load_dotenv() 9 | 10 | # Create a ChatOpenAI model 11 | model = ChatOpenAI(model="gpt-4o") 12 | 13 | # Define prompt templates 14 | prompt_template = ChatPromptTemplate.from_messages( 15 | [ 16 | ("system", "You are a comedian who tells jokes about {topic}."), 17 | ("human", "Tell me {joke_count} jokes."), 18 | ] 19 | ) 20 | 21 | # Define additional processing steps using RunnableLambda 22 | uppercase_output = RunnableLambda(lambda x: x.upper()) 23 | count_words = RunnableLambda(lambda x: f"Word count: {len(x.split())}\n{x}") 24 | 25 | # Create the combined chain using LangChain Expression Language (LCEL) 26 | chain = prompt_template | model | StrOutputParser() | uppercase_output | count_words 27 | 28 | # Run the chain 29 | result = chain.invoke({"topic": "lawyers", "joke_count": 3}) 30 | 31 | # Output 32 | print(result) 33 | -------------------------------------------------------------------------------- /3_chains/2_chains_under_the_hood.py: -------------------------------------------------------------------------------- 1 | from dotenv import load_dotenv 2 | from langchain.prompts import ChatPromptTemplate 3 | from langchain.schema.runnable import RunnableLambda, RunnableSequence 4 | from langchain_openai import ChatOpenAI 5 | 6 | # Load environment variables from .env 7 | load_dotenv() 8 | 9 | # Create a ChatOpenAI model 10 | model = ChatOpenAI(model="gpt-4") 11 | 12 | # Define prompt templates 13 | prompt_template = ChatPromptTemplate.from_messages( 14 | [ 15 | ("system", "You are a comedian who tells jokes about {topic}."), 16 | ("human", "Tell me {joke_count} jokes."), 17 | ] 18 | ) 19 | 20 | # Create individual runnables (steps in the chain) 21 | format_prompt = RunnableLambda(lambda x: prompt_template.format_prompt(**x)) 22 | invoke_model = RunnableLambda(lambda x: model.invoke(x.to_messages())) 23 | parse_output = RunnableLambda(lambda x: x.content) 24 | 25 | # Create the RunnableSequence (equivalent to the LCEL chain) 26 | chain = RunnableSequence(first=format_prompt, middle=[invoke_model], last=parse_output) 27 | 28 | # Run the chain 29 | response = chain.invoke({"topic": "lawyers", "joke_count": 3}) 30 | 31 | # Output 32 | print(response) 33 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "langchain-crash-course" 3 | version = "0.1.0" 4 | description = "Everything you need to know to get started with LangChain" 5 | authors = ["bhancock_ai "] 6 | 7 | [tool.poetry.dependencies] 8 | python = ">=3.10.0,<3.12" 9 | langchain-openai = "^0.1.8" 10 | python-dotenv = "^1.0.1" 11 | langchain = "^0.2.1" 12 | langchain-community = "^0.2.1" 13 | langchain-anthropic = "^0.1.15" 14 | langchain-google-genai = "^1.0.5" 15 | langchain-google-firestore = "^0.3.0" 16 | firestore = "^0.0.8" 17 | chromadb = "^0.5.0" 18 | tiktoken = "^0.7.0" 19 | sentence-transformers = "^3.0.0" 20 | bs4 = "^0.0.2" 21 | firecrawl-py = "^0.0.13" 22 | langchainhub = "^0.1.18" 23 | wikipedia = "^1.4.0" 24 | tavily-python = "^0.3.3" 25 | 26 | [tool.pyright] 27 | # https://github.com/microsoft/pyright/blob/main/docs/configuration.md 28 | useLibraryCodeForTypes = true 29 | exclude = [".cache"] 30 | 31 | [tool.ruff] 32 | # https://beta.ruff.rs/docs/configuration/ 33 | select = ['E', 'W', 'F', 'I', 'B', 'C4', 'ARG', 'SIM'] 34 | ignore = ['W291', 'W292', 'W293'] 35 | 36 | [build-system] 37 | requires = ["poetry-core>=1.0.0"] 38 | build-backend = "poetry.core.masonry.api" 39 | -------------------------------------------------------------------------------- /4_rag/1b_rag_basics.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from langchain_community.vectorstores import Chroma 4 | from langchain_openai import OpenAIEmbeddings 5 | 6 | # Define the persistent directory 7 | current_dir = os.path.dirname(os.path.abspath(__file__)) 8 | persistent_directory = os.path.join(current_dir, "db", "chroma_db") 9 | 10 | # Define the embedding model 11 | embeddings = OpenAIEmbeddings(model="text-embedding-3-small") 12 | 13 | # Load the existing vector store with the embedding function 14 | db = Chroma(persist_directory=persistent_directory, 15 | embedding_function=embeddings) 16 | 17 | # Define the user's question 18 | query = "Who is Odysseus' wife?" 19 | 20 | # Retrieve relevant documents based on the query 21 | retriever = db.as_retriever( 22 | search_type="similarity_score_threshold", 23 | search_kwargs={"k": 3, "score_threshold": 0.9}, 24 | ) 25 | relevant_docs = retriever.invoke(query) 26 | 27 | # Display the relevant results with metadata 28 | print("\n--- Relevant Documents ---") 29 | for i, doc in enumerate(relevant_docs, 1): 30 | print(f"Document {i}:\n{doc.page_content}\n") 31 | if doc.metadata: 32 | print(f"Source: {doc.metadata.get('source', 'Unknown')}\n") 33 | -------------------------------------------------------------------------------- /4_rag/2b_rag_basics_metadata.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from langchain_community.vectorstores import Chroma 4 | from langchain_openai import OpenAIEmbeddings 5 | 6 | # Define the persistent directory 7 | current_dir = os.path.dirname(os.path.abspath(__file__)) 8 | db_dir = os.path.join(current_dir, "db") 9 | persistent_directory = os.path.join(db_dir, "chroma_db_with_metadata") 10 | 11 | # Define the embedding model 12 | embeddings = OpenAIEmbeddings(model="text-embedding-3-small") 13 | 14 | # Load the existing vector store with the embedding function 15 | db = Chroma(persist_directory=persistent_directory, 16 | embedding_function=embeddings) 17 | 18 | # Define the user's question 19 | query = "How did Juliet die?" 20 | 21 | # Retrieve relevant documents based on the query 22 | retriever = db.as_retriever( 23 | search_type="similarity_score_threshold", 24 | search_kwargs={"k": 3, "score_threshold": 0.1}, 25 | ) 26 | relevant_docs = retriever.invoke(query) 27 | 28 | # Display the relevant results with metadata 29 | print("\n--- Relevant Documents ---") 30 | for i, doc in enumerate(relevant_docs, 1): 31 | print(f"Document {i}:\n{doc.page_content}\n") 32 | print(f"Source: {doc.metadata['source']}\n") 33 | -------------------------------------------------------------------------------- /1_chat_models/2_chat_model_basic_conversation.py: -------------------------------------------------------------------------------- 1 | from dotenv import load_dotenv 2 | from langchain_core.messages import AIMessage, HumanMessage, SystemMessage 3 | from langchain_openai import ChatOpenAI 4 | 5 | # Load environment variables from .env 6 | load_dotenv() 7 | 8 | # Create a ChatOpenAI model 9 | model = ChatOpenAI(model="gpt-4o") 10 | 11 | # SystemMessage: 12 | # Message for priming AI behavior, usually passed in as the first of a sequenc of input messages. 13 | # HumanMessagse: 14 | # Message from a human to the AI model. 15 | messages = [ 16 | SystemMessage(content="Solve the following math problems"), 17 | HumanMessage(content="What is 81 divided by 9?"), 18 | ] 19 | 20 | # Invoke the model with messages 21 | result = model.invoke(messages) 22 | print(f"Answer from AI: {result.content}") 23 | 24 | 25 | # AIMessage: 26 | # Message from an AI. 27 | messages = [ 28 | SystemMessage(content="Solve the following math problems"), 29 | HumanMessage(content="What is 81 divided by 9?"), 30 | AIMessage(content="81 divided by 9 is 9."), 31 | HumanMessage(content="What is 10 times 5?"), 32 | ] 33 | 34 | # Invoke the model with messages 35 | result = model.invoke(messages) 36 | print(f"Answer from AI: {result.content}") 37 | -------------------------------------------------------------------------------- /1_chat_models/3_chat_model_alternatives.py: -------------------------------------------------------------------------------- 1 | from langchain_google_genai import ChatGoogleGenerativeAI 2 | from langchain_anthropic import ChatAnthropic 3 | from langchain_openai import ChatOpenAI 4 | from dotenv import load_dotenv 5 | from langchain_core.messages import HumanMessage, SystemMessage 6 | 7 | # Setup environment variables and messages 8 | load_dotenv() 9 | 10 | messages = [ 11 | SystemMessage(content="Solve the following math problems"), 12 | HumanMessage(content="What is 81 divided by 9?"), 13 | ] 14 | 15 | 16 | # ---- LangChain OpenAI Chat Model Example ---- 17 | 18 | # Create a ChatOpenAI model 19 | model = ChatOpenAI(model="gpt-4o") 20 | 21 | # Invoke the model with messages 22 | result = model.invoke(messages) 23 | print(f"Answer from OpenAI: {result.content}") 24 | 25 | 26 | # ---- Anthropic Chat Model Example ---- 27 | 28 | # Create a Anthropic model 29 | # Anthropic models: https://docs.anthropic.com/en/docs/models-overview 30 | model = ChatAnthropic(model="claude-3-opus-20240229") 31 | 32 | result = model.invoke(messages) 33 | print(f"Answer from Anthropic: {result.content}") 34 | 35 | 36 | # ---- Google Chat Model Example ---- 37 | 38 | # https://console.cloud.google.com/gen-app-builder/engines 39 | # https://ai.google.dev/gemini-api/docs/models/gemini 40 | model = ChatGoogleGenerativeAI(model="gemini-1.5-flash") 41 | 42 | result = model.invoke(messages) 43 | print(f"Answer from Google: {result.content}") 44 | -------------------------------------------------------------------------------- /2_prompt_templates/2_prompt_template_with_chat_model.py: -------------------------------------------------------------------------------- 1 | from dotenv import load_dotenv 2 | from langchain.prompts import ChatPromptTemplate 3 | from langchain_openai import ChatOpenAI 4 | 5 | # Load environment variables from .env 6 | load_dotenv() 7 | 8 | # Create a ChatOpenAI model 9 | model = ChatOpenAI(model="gpt-4o") 10 | 11 | # PART 1: Create a ChatPromptTemplate using a template string 12 | print("-----Prompt from Template-----") 13 | template = "Tell me a joke about {topic}." 14 | prompt_template = ChatPromptTemplate.from_template(template) 15 | 16 | prompt = prompt_template.invoke({"topic": "cats"}) 17 | result = model.invoke(prompt) 18 | print(result.content) 19 | 20 | # PART 2: Prompt with Multiple Placeholders 21 | print("\n----- Prompt with Multiple Placeholders -----\n") 22 | template_multiple = """You are a helpful assistant. 23 | Human: Tell me a {adjective} short story about a {animal}. 24 | Assistant:""" 25 | prompt_multiple = ChatPromptTemplate.from_template(template_multiple) 26 | prompt = prompt_multiple.invoke({"adjective": "funny", "animal": "panda"}) 27 | 28 | result = model.invoke(prompt) 29 | print(result.content) 30 | 31 | # PART 3: Prompt with System and Human Messages (Using Tuples) 32 | print("\n----- Prompt with System and Human Messages (Tuple) -----\n") 33 | messages = [ 34 | ("system", "You are a comedian who tells jokes about {topic}."), 35 | ("human", "Tell me {joke_count} jokes."), 36 | ] 37 | prompt_template = ChatPromptTemplate.from_messages(messages) 38 | prompt = prompt_template.invoke({"topic": "lawyers", "joke_count": 3}) 39 | result = model.invoke(prompt) 40 | print(result.content) 41 | -------------------------------------------------------------------------------- /5_agents_and_tools/1_agent_and_tools_basics.py: -------------------------------------------------------------------------------- 1 | from dotenv import load_dotenv 2 | from langchain import hub 3 | from langchain.agents import ( 4 | AgentExecutor, 5 | create_react_agent, 6 | ) 7 | from langchain_core.tools import Tool 8 | from langchain_openai import ChatOpenAI 9 | 10 | # Load environment variables from .env file 11 | load_dotenv() 12 | 13 | 14 | # Define a very simple tool function that returns the current time 15 | def get_current_time(*args, **kwargs): 16 | """Returns the current time in H:MM AM/PM format.""" 17 | import datetime # Import datetime module to get current time 18 | 19 | now = datetime.datetime.now() # Get current time 20 | return now.strftime("%I:%M %p") # Format time in H:MM AM/PM format 21 | 22 | 23 | # List of tools available to the agent 24 | tools = [ 25 | Tool( 26 | name="Time", # Name of the tool 27 | func=get_current_time, # Function that the tool will execute 28 | # Description of the tool 29 | description="Useful for when you need to know the current time", 30 | ), 31 | ] 32 | 33 | # Pull the prompt template from the hub 34 | # ReAct = Reason and Action 35 | # https://smith.langchain.com/hub/hwchase17/react 36 | prompt = hub.pull("hwchase17/react") 37 | 38 | # Initialize a ChatOpenAI model 39 | llm = ChatOpenAI( 40 | model="gpt-4o", temperature=0 41 | ) 42 | 43 | # Create the ReAct agent using the create_react_agent function 44 | agent = create_react_agent( 45 | llm=llm, 46 | tools=tools, 47 | prompt=prompt, 48 | stop_sequence=True, 49 | ) 50 | 51 | # Create an agent executor from the agent and tools 52 | agent_executor = AgentExecutor.from_agent_and_tools( 53 | agent=agent, 54 | tools=tools, 55 | verbose=True, 56 | ) 57 | 58 | # Run the agent with a test query 59 | response = agent_executor.invoke({"input": "What time is it?"}) 60 | 61 | # Print the response from the agent 62 | print("response:", response) 63 | -------------------------------------------------------------------------------- /4_rag/1a_rag_basics.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from langchain.text_splitter import CharacterTextSplitter 4 | from langchain_community.document_loaders import TextLoader 5 | from langchain_community.vectorstores import Chroma 6 | from langchain_openai import OpenAIEmbeddings 7 | 8 | # Define the directory containing the text file and the persistent directory 9 | current_dir = os.path.dirname(os.path.abspath(__file__)) 10 | file_path = os.path.join(current_dir, "books", "odyssey.txt") 11 | persistent_directory = os.path.join(current_dir, "db", "chroma_db") 12 | 13 | # Check if the Chroma vector store already exists 14 | if not os.path.exists(persistent_directory): 15 | print("Persistent directory does not exist. Initializing vector store...") 16 | 17 | # Ensure the text file exists 18 | if not os.path.exists(file_path): 19 | raise FileNotFoundError( 20 | f"The file {file_path} does not exist. Please check the path." 21 | ) 22 | 23 | # Read the text content from the file 24 | loader = TextLoader(file_path) 25 | documents = loader.load() 26 | 27 | # Split the document into chunks 28 | text_splitter = CharacterTextSplitter(chunk_size=1000, chunk_overlap=0) 29 | docs = text_splitter.split_documents(documents) 30 | 31 | # Display information about the split documents 32 | print("\n--- Document Chunks Information ---") 33 | print(f"Number of document chunks: {len(docs)}") 34 | print(f"Sample chunk:\n{docs[0].page_content}\n") 35 | 36 | # Create embeddings 37 | print("\n--- Creating embeddings ---") 38 | embeddings = OpenAIEmbeddings( 39 | model="text-embedding-3-small" 40 | ) # Update to a valid embedding model if needed 41 | print("\n--- Finished creating embeddings ---") 42 | 43 | # Create the vector store and persist it automatically 44 | print("\n--- Creating vector store ---") 45 | db = Chroma.from_documents( 46 | docs, embeddings, persist_directory=persistent_directory) 47 | print("\n--- Finished creating vector store ---") 48 | 49 | else: 50 | print("Vector store already exists. No need to initialize.") 51 | -------------------------------------------------------------------------------- /1_chat_models/5_chat_model_save_message_history_firebase.py: -------------------------------------------------------------------------------- 1 | # Example Source: https://python.langchain.com/v0.2/docs/integrations/memory/google_firestore/ 2 | 3 | from dotenv import load_dotenv 4 | from google.cloud import firestore 5 | from langchain_google_firestore import FirestoreChatMessageHistory 6 | from langchain_openai import ChatOpenAI 7 | 8 | """ 9 | Steps to replicate this example: 10 | 1. Create a Firebase account 11 | 2. Create a new Firebase project 12 | - Copy the project ID 13 | 3. Create a Firestore database in the Firebase project 14 | 4. Install the Google Cloud CLI on your computer 15 | - https://cloud.google.com/sdk/docs/install 16 | - Authenticate the Google Cloud CLI with your Google account 17 | - https://cloud.google.com/docs/authentication/provide-credentials-adc#local-dev 18 | - Set your default project to the new Firebase project you created 19 | 5. Enable the Firestore API in the Google Cloud Console: 20 | - https://console.cloud.google.com/apis/enableflow?apiid=firestore.googleapis.com&project=crewai-automation 21 | """ 22 | 23 | load_dotenv() 24 | 25 | # Setup Firebase Firestore 26 | PROJECT_ID = "langchain-demo-abf48" 27 | SESSION_ID = "user_session_new" # This could be a username or a unique ID 28 | COLLECTION_NAME = "chat_history" 29 | 30 | # Initialize Firestore Client 31 | print("Initializing Firestore Client...") 32 | client = firestore.Client(project=PROJECT_ID) 33 | 34 | # Initialize Firestore Chat Message History 35 | print("Initializing Firestore Chat Message History...") 36 | chat_history = FirestoreChatMessageHistory( 37 | session_id=SESSION_ID, 38 | collection=COLLECTION_NAME, 39 | client=client, 40 | ) 41 | print("Chat History Initialized.") 42 | print("Current Chat History:", chat_history.messages) 43 | 44 | # Initialize Chat Model 45 | model = ChatOpenAI() 46 | 47 | print("Start chatting with the AI. Type 'exit' to quit.") 48 | 49 | while True: 50 | human_input = input("User: ") 51 | if human_input.lower() == "exit": 52 | break 53 | 54 | chat_history.add_user_message(human_input) 55 | 56 | ai_response = model.invoke(chat_history.messages) 57 | chat_history.add_ai_message(ai_response.content) 58 | 59 | print(f"AI: {ai_response.content}") 60 | -------------------------------------------------------------------------------- /4_rag/6_rag_one_off_question.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from dotenv import load_dotenv 4 | from langchain_community.vectorstores import Chroma 5 | from langchain_core.messages import HumanMessage, SystemMessage 6 | from langchain_openai import ChatOpenAI, OpenAIEmbeddings 7 | 8 | # Load environment variables from .env 9 | load_dotenv() 10 | 11 | # Define the persistent directory 12 | current_dir = os.path.dirname(os.path.abspath(__file__)) 13 | persistent_directory = os.path.join( 14 | current_dir, "db", "chroma_db_with_metadata") 15 | 16 | # Define the embedding model 17 | embeddings = OpenAIEmbeddings(model="text-embedding-3-small") 18 | 19 | # Load the existing vector store with the embedding function 20 | db = Chroma(persist_directory=persistent_directory, 21 | embedding_function=embeddings) 22 | 23 | # Define the user's question 24 | query = "How can I learn more about LangChain?" 25 | 26 | # Retrieve relevant documents based on the query 27 | retriever = db.as_retriever( 28 | search_type="similarity", 29 | search_kwargs={"k": 1}, 30 | ) 31 | relevant_docs = retriever.invoke(query) 32 | 33 | # Display the relevant results with metadata 34 | print("\n--- Relevant Documents ---") 35 | for i, doc in enumerate(relevant_docs, 1): 36 | print(f"Document {i}:\n{doc.page_content}\n") 37 | 38 | # Combine the query and the relevant document contents 39 | combined_input = ( 40 | "Here are some documents that might help answer the question: " 41 | + query 42 | + "\n\nRelevant Documents:\n" 43 | + "\n\n".join([doc.page_content for doc in relevant_docs]) 44 | + "\n\nPlease provide an answer based only on the provided documents. If the answer is not found in the documents, respond with 'I'm not sure'." 45 | ) 46 | 47 | # Create a ChatOpenAI model 48 | model = ChatOpenAI(model="gpt-4o") 49 | 50 | # Define the messages for the model 51 | messages = [ 52 | SystemMessage(content="You are a helpful assistant."), 53 | HumanMessage(content=combined_input), 54 | ] 55 | 56 | # Invoke the model with the combined input 57 | result = model.invoke(messages) 58 | 59 | # Display the full result and content only 60 | print("\n--- Generated Response ---") 61 | # print("Full result:") 62 | # print(result) 63 | print("Content only:") 64 | print(result.content) 65 | -------------------------------------------------------------------------------- /2_prompt_templates/1_prompt_template_basic.py: -------------------------------------------------------------------------------- 1 | # Prompt Template Docs: 2 | # https://python.langchain.com/v0.2/docs/concepts/#prompt-templateshttps://python.langchain.com/v0.2/docs/concepts/#prompt-templates 3 | 4 | from langchain.prompts import ChatPromptTemplate 5 | from langchain_core.messages import HumanMessage 6 | 7 | # # PART 1: Create a ChatPromptTemplate using a template string 8 | # template = "Tell me a joke about {topic}." 9 | # prompt_template = ChatPromptTemplate.from_template(template) 10 | 11 | # print("-----Prompt from Template-----") 12 | # prompt = prompt_template.invoke({"topic": "cats"}) 13 | # print(prompt) 14 | 15 | # # PART 2: Prompt with Multiple Placeholders 16 | # template_multiple = """You are a helpful assistant. 17 | # Human: Tell me a {adjective} story about a {animal}. 18 | # Assistant:""" 19 | # prompt_multiple = ChatPromptTemplate.from_template(template_multiple) 20 | # prompt = prompt_multiple.invoke({"adjective": "funny", "animal": "panda"}) 21 | # print("\n----- Prompt with Multiple Placeholders -----\n") 22 | # print(prompt) 23 | 24 | 25 | # PART 3: Prompt with System and Human Messages (Using Tuples) 26 | # messages = [ 27 | # ("system", "You are a comedian who tells jokes about {topic}."), 28 | # ("human", "Tell me {joke_count} jokes."), 29 | # ] 30 | # prompt_template = ChatPromptTemplate.from_messages(messages) 31 | # prompt = prompt_template.invoke({"topic": "lawyers", "joke_count": 3}) 32 | # print("\n----- Prompt with System and Human Messages (Tuple) -----\n") 33 | # print(prompt) 34 | 35 | # # Extra Informoation about Part 3. 36 | # # This does work: 37 | # messages = [ 38 | # ("system", "You are a comedian who tells jokes about {topic}."), 39 | # HumanMessage(content="Tell me 3 jokes."), 40 | # ] 41 | # prompt_template = ChatPromptTemplate.from_messages(messages) 42 | # prompt = prompt_template.invoke({"topic": "lawyers"}) 43 | # print("\n----- Prompt with System and Human Messages (Tuple) -----\n") 44 | # print(prompt) 45 | 46 | 47 | # This does NOT work: 48 | messages = [ 49 | ("system", "You are a comedian who tells jokes about {topic}."), 50 | HumanMessage(content="Tell me {joke_count} jokes."), 51 | ] 52 | prompt_template = ChatPromptTemplate.from_messages(messages) 53 | prompt = prompt_template.invoke({"topic": "lawyers", "joke_count": 3}) 54 | print("\n----- Prompt with System and Human Messages (Tuple) -----\n") 55 | print(prompt) 56 | -------------------------------------------------------------------------------- /3_chains/4_chains_parallel.py: -------------------------------------------------------------------------------- 1 | from dotenv import load_dotenv 2 | from langchain.prompts import ChatPromptTemplate 3 | from langchain.schema.output_parser import StrOutputParser 4 | from langchain.schema.runnable import RunnableParallel, RunnableLambda 5 | from langchain_openai import ChatOpenAI 6 | 7 | # Load environment variables from .env 8 | load_dotenv() 9 | 10 | # Create a ChatOpenAI model 11 | model = ChatOpenAI(model="gpt-4o") 12 | 13 | # Define prompt template 14 | prompt_template = ChatPromptTemplate.from_messages( 15 | [ 16 | ("system", "You are an expert product reviewer."), 17 | ("human", "List the main features of the product {product_name}."), 18 | ] 19 | ) 20 | 21 | 22 | # Define pros analysis step 23 | def analyze_pros(features): 24 | pros_template = ChatPromptTemplate.from_messages( 25 | [ 26 | ("system", "You are an expert product reviewer."), 27 | ( 28 | "human", 29 | "Given these features: {features}, list the pros of these features.", 30 | ), 31 | ] 32 | ) 33 | return pros_template.format_prompt(features=features) 34 | 35 | 36 | # Define cons analysis step 37 | def analyze_cons(features): 38 | cons_template = ChatPromptTemplate.from_messages( 39 | [ 40 | ("system", "You are an expert product reviewer."), 41 | ( 42 | "human", 43 | "Given these features: {features}, list the cons of these features.", 44 | ), 45 | ] 46 | ) 47 | return cons_template.format_prompt(features=features) 48 | 49 | 50 | # Combine pros and cons into a final review 51 | def combine_pros_cons(pros, cons): 52 | return f"Pros:\n{pros}\n\nCons:\n{cons}" 53 | 54 | 55 | # Simplify branches with LCEL 56 | pros_branch_chain = ( 57 | RunnableLambda(lambda x: analyze_pros(x)) | model | StrOutputParser() 58 | ) 59 | 60 | cons_branch_chain = ( 61 | RunnableLambda(lambda x: analyze_cons(x)) | model | StrOutputParser() 62 | ) 63 | 64 | # Create the combined chain using LangChain Expression Language (LCEL) 65 | chain = ( 66 | prompt_template 67 | | model 68 | | StrOutputParser() 69 | | RunnableParallel(branches={"pros": pros_branch_chain, "cons": cons_branch_chain}) 70 | | RunnableLambda(lambda x: combine_pros_cons(x["branches"]["pros"], x["branches"]["cons"])) 71 | ) 72 | 73 | # Run the chain 74 | result = chain.invoke({"product_name": "MacBook Pro"}) 75 | 76 | # Output 77 | print(result) 78 | -------------------------------------------------------------------------------- /4_rag/2a_rag_basics_metadata.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from langchain.text_splitter import CharacterTextSplitter 4 | from langchain_community.document_loaders import TextLoader 5 | from langchain_community.vectorstores import Chroma 6 | from langchain_openai import OpenAIEmbeddings 7 | 8 | # Define the directory containing the text files and the persistent directory 9 | current_dir = os.path.dirname(os.path.abspath(__file__)) 10 | books_dir = os.path.join(current_dir, "books") 11 | db_dir = os.path.join(current_dir, "db") 12 | persistent_directory = os.path.join(db_dir, "chroma_db_with_metadata") 13 | 14 | print(f"Books directory: {books_dir}") 15 | print(f"Persistent directory: {persistent_directory}") 16 | 17 | # Check if the Chroma vector store already exists 18 | if not os.path.exists(persistent_directory): 19 | print("Persistent directory does not exist. Initializing vector store...") 20 | 21 | # Ensure the books directory exists 22 | if not os.path.exists(books_dir): 23 | raise FileNotFoundError( 24 | f"The directory {books_dir} does not exist. Please check the path." 25 | ) 26 | 27 | # List all text files in the directory 28 | book_files = [f for f in os.listdir(books_dir) if f.endswith(".txt")] 29 | 30 | # Read the text content from each file and store it with metadata 31 | documents = [] 32 | for book_file in book_files: 33 | file_path = os.path.join(books_dir, book_file) 34 | loader = TextLoader(file_path) 35 | book_docs = loader.load() 36 | for doc in book_docs: 37 | # Add metadata to each document indicating its source 38 | doc.metadata = {"source": book_file} 39 | documents.append(doc) 40 | 41 | # Split the documents into chunks 42 | text_splitter = CharacterTextSplitter(chunk_size=1000, chunk_overlap=0) 43 | docs = text_splitter.split_documents(documents) 44 | 45 | # Display information about the split documents 46 | print("\n--- Document Chunks Information ---") 47 | print(f"Number of document chunks: {len(docs)}") 48 | 49 | # Create embeddings 50 | print("\n--- Creating embeddings ---") 51 | embeddings = OpenAIEmbeddings( 52 | model="text-embedding-3-small" 53 | ) # Update to a valid embedding model if needed 54 | print("\n--- Finished creating embeddings ---") 55 | 56 | # Create the vector store and persist it 57 | print("\n--- Creating and persisting vector store ---") 58 | db = Chroma.from_documents( 59 | docs, embeddings, persist_directory=persistent_directory) 60 | print("\n--- Finished creating and persisting vector store ---") 61 | 62 | else: 63 | print("Vector store already exists. No need to initialize.") 64 | -------------------------------------------------------------------------------- /4_rag/8_rag_web_scrape_basic.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from dotenv import load_dotenv 4 | from langchain.text_splitter import CharacterTextSplitter 5 | from langchain_community.document_loaders import WebBaseLoader 6 | from langchain_community.vectorstores import Chroma 7 | from langchain_openai import OpenAIEmbeddings 8 | 9 | # Load environment variables from .env 10 | load_dotenv() 11 | 12 | # Define the persistent directory 13 | current_dir = os.path.dirname(os.path.abspath(__file__)) 14 | db_dir = os.path.join(current_dir, "db") 15 | persistent_directory = os.path.join(db_dir, "chroma_db_apple") 16 | 17 | # Step 1: Scrape the content from apple.com using WebBaseLoader 18 | # WebBaseLoader loads web pages and extracts their content 19 | urls = ["https://www.apple.com/"] 20 | 21 | # Create a loader for web content 22 | loader = WebBaseLoader(urls) 23 | documents = loader.load() 24 | 25 | # Step 2: Split the scraped content into chunks 26 | # CharacterTextSplitter splits the text into smaller chunks 27 | text_splitter = CharacterTextSplitter(chunk_size=1000, chunk_overlap=0) 28 | docs = text_splitter.split_documents(documents) 29 | 30 | # Display information about the split documents 31 | print("\n--- Document Chunks Information ---") 32 | print(f"Number of document chunks: {len(docs)}") 33 | print(f"Sample chunk:\n{docs[0].page_content}\n") 34 | 35 | # Step 3: Create embeddings for the document chunks 36 | # OpenAIEmbeddings turns text into numerical vectors that capture semantic meaning 37 | embeddings = OpenAIEmbeddings(model="text-embedding-3-small") 38 | 39 | # Step 4: Create and persist the vector store with the embeddings 40 | # Chroma stores the embeddings for efficient searching 41 | if not os.path.exists(persistent_directory): 42 | print(f"\n--- Creating vector store in {persistent_directory} ---") 43 | db = Chroma.from_documents(docs, embeddings, persist_directory=persistent_directory) 44 | print(f"--- Finished creating vector store in {persistent_directory} ---") 45 | else: 46 | print(f"Vector store {persistent_directory} already exists. No need to initialize.") 47 | db = Chroma(persist_directory=persistent_directory, embedding_function=embeddings) 48 | 49 | # Step 5: Query the vector store 50 | # Create a retriever for querying the vector store 51 | retriever = db.as_retriever( 52 | search_type="similarity", 53 | search_kwargs={"k": 3}, 54 | ) 55 | 56 | # Define the user's question 57 | query = "What new products are announced on Apple.com?" 58 | 59 | # Retrieve relevant documents based on the query 60 | relevant_docs = retriever.invoke(query) 61 | 62 | # Display the relevant results with metadata 63 | print("\n--- Relevant Documents ---") 64 | for i, doc in enumerate(relevant_docs, 1): 65 | print(f"Document {i}:\n{doc.page_content}\n") 66 | if doc.metadata: 67 | print(f"Source: {doc.metadata.get('source', 'Unknown')}\n") 68 | -------------------------------------------------------------------------------- /5_agents_and_tools/tools_deep_dive/3_tool_base_tool.py: -------------------------------------------------------------------------------- 1 | # Docs: https://python.langchain.com/v0.1/docs/modules/tools/custom_tools/ 2 | 3 | # Import necessary libraries 4 | import os 5 | from typing import Type 6 | 7 | from dotenv import load_dotenv 8 | from langchain import hub 9 | from langchain.agents import AgentExecutor, create_tool_calling_agent 10 | from langchain.pydantic_v1 import BaseModel, Field 11 | from langchain_core.tools import BaseTool 12 | from langchain_openai import ChatOpenAI 13 | 14 | 15 | load_dotenv() 16 | 17 | # Pydantic models for tool arguments 18 | 19 | 20 | class SimpleSearchInput(BaseModel): 21 | query: str = Field(description="should be a search query") 22 | 23 | 24 | class MultiplyNumbersArgs(BaseModel): 25 | x: float = Field(description="First number to multiply") 26 | y: float = Field(description="Second number to multiply") 27 | 28 | 29 | # Custom tool with only custom input 30 | 31 | 32 | class SimpleSearchTool(BaseTool): 33 | name = "simple_search" 34 | description = "useful for when you need to answer questions about current events" 35 | args_schema: Type[BaseModel] = SimpleSearchInput 36 | 37 | def _run( 38 | self, 39 | query: str, 40 | ) -> str: 41 | """Use the tool.""" 42 | from tavily import TavilyClient 43 | 44 | api_key = os.getenv("TAVILY_API_KEY") 45 | client = TavilyClient(api_key=api_key) 46 | results = client.search(query=query) 47 | return f"Search results for: {query}\n\n\n{results}\n" 48 | 49 | 50 | # Custom tool with custom input and output 51 | class MultiplyNumbersTool(BaseTool): 52 | name = "multiply_numbers" 53 | description = "useful for multiplying two numbers" 54 | args_schema: Type[BaseModel] = MultiplyNumbersArgs 55 | 56 | def _run( 57 | self, 58 | x: float, 59 | y: float, 60 | ) -> str: 61 | """Use the tool.""" 62 | result = x * y 63 | return f"The product of {x} and {y} is {result}" 64 | 65 | 66 | # Create tools using the Pydantic subclass approach 67 | tools = [ 68 | SimpleSearchTool(), 69 | MultiplyNumbersTool(), 70 | ] 71 | 72 | # Initialize a ChatOpenAI model 73 | llm = ChatOpenAI(model="gpt-4o") 74 | 75 | # Pull the prompt template from the hub 76 | prompt = hub.pull("hwchase17/openai-tools-agent") 77 | 78 | # Create the ReAct agent using the create_tool_calling_agent function 79 | agent = create_tool_calling_agent( 80 | llm=llm, 81 | tools=tools, 82 | prompt=prompt, 83 | ) 84 | 85 | # Create the agent executor 86 | agent_executor = AgentExecutor.from_agent_and_tools( 87 | agent=agent, 88 | tools=tools, 89 | verbose=True, 90 | handle_parsing_errors=True, 91 | ) 92 | 93 | # Test the agent with sample queries 94 | response = agent_executor.invoke({"input": "Search for Apple Intelligence"}) 95 | print("Response for 'Search for LangChain updates':", response) 96 | 97 | response = agent_executor.invoke({"input": "Multiply 10 and 20"}) 98 | print("Response for 'Multiply 10 and 20':", response) 99 | -------------------------------------------------------------------------------- /4_rag/books/langchain_demo.txt: -------------------------------------------------------------------------------- 1 | LangChain: A Framework for LLM-Powered Applications 2 | LangChain is a powerful and flexible framework designed to simplify the development of applications that harness the capabilities of large language models (LLMs). It provides a wide range of tools, abstractions, and integrations that help developers build, customize, and optimize applications that leverage LLMs for tasks like text generation, question answering, summarization, chatbots, and more. 3 | 4 | Key Features and Benefits 5 | Modular Components: LangChain offers a variety of modular components (chains, agents, tools, prompts, memory, etc.) that can be easily combined and customized to build complex LLM-powered workflows. 6 | Data Integration: It seamlessly integrates with various data sources, enabling applications to access and process external information, enhancing the context and relevance of LLM responses. 7 | Agent Frameworks: LangChain provides agent frameworks that allow LLMs to interact with their environment, make decisions, and take actions based on user input or specific goals. 8 | Memory Management: It includes memory components that enable applications to maintain context and track conversations, leading to more coherent and personalized interactions. 9 | Prompt Engineering: LangChain facilitates prompt engineering, the process of crafting effective prompts to elicit desired responses from LLMs, by offering templates and tools for experimentation. 10 | Chain Optimization: It provides mechanisms to evaluate and optimize chain performance, ensuring that applications deliver the best possible results. 11 | Use Cases 12 | LangChain empowers developers to create a wide array of applications, including: 13 | 14 | Chatbots and Conversational Agents: Build intelligent chatbots capable of understanding natural language and providing informative responses. 15 | Question Answering Systems: Develop systems that can accurately answer questions posed in natural language, leveraging information from various sources. 16 | Summarization Tools: Create tools that can condense lengthy documents or articles into concise summaries. 17 | Text Generation Applications: Build applications that can generate creative content like stories, poems, or code snippets. 18 | Autonomous Agents: Develop agents that can perform tasks autonomously based on user instructions or predefined goals. 19 | A Wealth of Resources 20 | LangChain boasts a thriving community and comprehensive documentation, making it easy for developers to get started and explore its capabilities. It offers extensive tutorials, examples, and guides to help users build powerful LLM-powered applications. 21 | 22 | For Further Exploration 23 | To learn more about LangChain and its wide range of applications, be sure to check out the comprehensive documentation and tutorials available on the official website: 24 | 25 | LangChain Documentation: https://python.langchain.com/ 26 | Don't Forget to Like and Subscribe! 27 | If you're looking for in-depth tutorials and insights into LangChain, CrewAI, and other AI technologies, be sure to check out the fantastic YouTube channel by Brandon Hancock: 28 | 29 | YouTube Channel: https://www.youtube.com/@bhancock_ai 30 | Don't forget to like and subscribe to his channel!! -------------------------------------------------------------------------------- /3_chains/5_chains_branching.py: -------------------------------------------------------------------------------- 1 | from dotenv import load_dotenv 2 | from langchain.prompts import ChatPromptTemplate 3 | from langchain.schema.output_parser import StrOutputParser 4 | from langchain.schema.runnable import RunnableBranch 5 | from langchain_openai import ChatOpenAI 6 | 7 | # Load environment variables from .env 8 | load_dotenv() 9 | 10 | # Create a ChatOpenAI model 11 | model = ChatOpenAI(model="gpt-4o") 12 | 13 | # Define prompt templates for different feedback types 14 | positive_feedback_template = ChatPromptTemplate.from_messages( 15 | [ 16 | ("system", "You are a helpful assistant."), 17 | ("human", 18 | "Generate a thank you note for this positive feedback: {feedback}."), 19 | ] 20 | ) 21 | 22 | negative_feedback_template = ChatPromptTemplate.from_messages( 23 | [ 24 | ("system", "You are a helpful assistant."), 25 | ("human", 26 | "Generate a response addressing this negative feedback: {feedback}."), 27 | ] 28 | ) 29 | 30 | neutral_feedback_template = ChatPromptTemplate.from_messages( 31 | [ 32 | ("system", "You are a helpful assistant."), 33 | ( 34 | "human", 35 | "Generate a request for more details for this neutral feedback: {feedback}.", 36 | ), 37 | ] 38 | ) 39 | 40 | escalate_feedback_template = ChatPromptTemplate.from_messages( 41 | [ 42 | ("system", "You are a helpful assistant."), 43 | ( 44 | "human", 45 | "Generate a message to escalate this feedback to a human agent: {feedback}.", 46 | ), 47 | ] 48 | ) 49 | 50 | # Define the feedback classification template 51 | classification_template = ChatPromptTemplate.from_messages( 52 | [ 53 | ("system", "You are a helpful assistant."), 54 | ("human", 55 | "Classify the sentiment of this feedback as positive, negative, neutral, or escalate: {feedback}."), 56 | ] 57 | ) 58 | 59 | # Define the runnable branches for handling feedback 60 | branches = RunnableBranch( 61 | ( 62 | lambda x: "positive" in x, 63 | positive_feedback_template | model | StrOutputParser() # Positive feedback chain 64 | ), 65 | ( 66 | lambda x: "negative" in x, 67 | negative_feedback_template | model | StrOutputParser() # Negative feedback chain 68 | ), 69 | ( 70 | lambda x: "neutral" in x, 71 | neutral_feedback_template | model | StrOutputParser() # Neutral feedback chain 72 | ), 73 | escalate_feedback_template | model | StrOutputParser() 74 | ) 75 | 76 | # Create the classification chain 77 | classification_chain = classification_template | model | StrOutputParser() 78 | 79 | # Combine classification and response generation into one chain 80 | chain = classification_chain | branches 81 | 82 | # Run the chain with an example review 83 | # Good review - "The product is excellent. I really enjoyed using it and found it very helpful." 84 | # Bad review - "The product is terrible. It broke after just one use and the quality is very poor." 85 | # Neutral review - "The product is okay. It works as expected but nothing exceptional." 86 | # Default - "I'm not sure about the product yet. Can you tell me more about its features and benefits?" 87 | 88 | review = "The product is terrible. It broke after just one use and the quality is very poor." 89 | result = chain.invoke({"feedback": review}) 90 | 91 | # Output the result 92 | print(result) 93 | -------------------------------------------------------------------------------- /5_agents_and_tools/tools_deep_dive/1_tool_constructor.py: -------------------------------------------------------------------------------- 1 | # Docs: https://python.langchain.com/v0.1/docs/modules/tools/custom_tools/ 2 | 3 | # Import necessary libraries 4 | from langchain import hub 5 | from langchain.agents import AgentExecutor, create_tool_calling_agent 6 | from langchain.pydantic_v1 import BaseModel, Field 7 | from langchain_core.tools import StructuredTool, Tool 8 | from langchain_openai import ChatOpenAI 9 | 10 | 11 | # Functions for the tools 12 | def greet_user(name: str) -> str: 13 | """Greets the user by name.""" 14 | return f"Hello, {name}!" 15 | 16 | 17 | def reverse_string(text: str) -> str: 18 | """Reverses the given string.""" 19 | return text[::-1] 20 | 21 | 22 | def concatenate_strings(a: str, b: str) -> str: 23 | """Concatenates two strings.""" 24 | return a + b 25 | 26 | 27 | # Pydantic model for tool arguments 28 | class ConcatenateStringsArgs(BaseModel): 29 | a: str = Field(description="First string") 30 | b: str = Field(description="Second string") 31 | 32 | 33 | # Create tools using the Tool and StructuredTool constructor approach 34 | tools = [ 35 | # Use Tool for simpler functions with a single input parameter. 36 | # This is straightforward and doesn't require an input schema. 37 | Tool( 38 | name="GreetUser", # Name of the tool 39 | func=greet_user, # Function to execute 40 | description="Greets the user by name.", # Description of the tool 41 | ), 42 | # Use Tool for another simple function with a single input parameter. 43 | Tool( 44 | name="ReverseString", # Name of the tool 45 | func=reverse_string, # Function to execute 46 | description="Reverses the given string.", # Description of the tool 47 | ), 48 | # Use StructuredTool for more complex functions that require multiple input parameters. 49 | # StructuredTool allows us to define an input schema using Pydantic, ensuring proper validation and description. 50 | StructuredTool.from_function( 51 | func=concatenate_strings, # Function to execute 52 | name="ConcatenateStrings", # Name of the tool 53 | description="Concatenates two strings.", # Description of the tool 54 | args_schema=ConcatenateStringsArgs, # Schema defining the tool's input arguments 55 | ), 56 | ] 57 | 58 | # Initialize a ChatOpenAI model 59 | llm = ChatOpenAI(model="gpt-4o") 60 | 61 | # Pull the prompt template from the hub 62 | prompt = hub.pull("hwchase17/openai-tools-agent") 63 | 64 | # Create the ReAct agent using the create_tool_calling_agent function 65 | agent = create_tool_calling_agent( 66 | llm=llm, # Language model to use 67 | tools=tools, # List of tools available to the agent 68 | prompt=prompt, # Prompt template to guide the agent's responses 69 | ) 70 | 71 | # Create the agent executor 72 | agent_executor = AgentExecutor.from_agent_and_tools( 73 | agent=agent, # The agent to execute 74 | tools=tools, # List of tools available to the agent 75 | verbose=True, # Enable verbose logging 76 | handle_parsing_errors=True, # Handle parsing errors gracefully 77 | ) 78 | 79 | # Test the agent with sample queries 80 | response = agent_executor.invoke({"input": "Greet Alice"}) 81 | print("Response for 'Greet Alice':", response) 82 | 83 | response = agent_executor.invoke({"input": "Reverse the string 'hello'"}) 84 | print("Response for 'Reverse the string hello':", response) 85 | 86 | response = agent_executor.invoke({"input": "Concatenate 'hello' and 'world'"}) 87 | print("Response for 'Concatenate hello and world':", response) 88 | -------------------------------------------------------------------------------- /5_agents_and_tools/tools_deep_dive/2_tool_decorator.py: -------------------------------------------------------------------------------- 1 | # Documentation: https://python.langchain.com/v0.1/docs/modules/tools/custom_tools/ 2 | 3 | # Import necessary libraries 4 | from langchain import hub 5 | from langchain.agents import AgentExecutor, create_tool_calling_agent 6 | from langchain.pydantic_v1 import BaseModel, Field 7 | from langchain.tools import tool 8 | from langchain_openai import ChatOpenAI 9 | 10 | 11 | # Simple Tool with one parameter without args_schema 12 | # This is a basic tool that does not require an input schema. 13 | # Use this approach for simple functions that need only one parameter. 14 | @tool() 15 | def greet_user(name: str) -> str: 16 | """Greets the user by name.""" 17 | return f"Hello, {name}!" 18 | 19 | 20 | # Pydantic models for tool arguments 21 | # Define a Pydantic model to specify the input schema for tools that need more structured input. 22 | class ReverseStringArgs(BaseModel): 23 | text: str = Field(description="Text to be reversed") 24 | 25 | 26 | # Tool with One Parameter using args_schema 27 | # Use the args_schema parameter to specify the input schema using a Pydantic model. 28 | @tool(args_schema=ReverseStringArgs) 29 | def reverse_string(text: str) -> str: 30 | """Reverses the given string.""" 31 | return text[::-1] 32 | 33 | 34 | # Another Pydantic model for tool arguments 35 | class ConcatenateStringsArgs(BaseModel): 36 | a: str = Field(description="First string") 37 | b: str = Field(description="Second string") 38 | 39 | 40 | # Tool with Two Parameters using args_schema 41 | # This tool requires multiple input parameters, so we use the args_schema to define the schema. 42 | @tool(args_schema=ConcatenateStringsArgs) 43 | def concatenate_strings(a: str, b: str) -> str: 44 | """Concatenates two strings.""" 45 | print("a", a) 46 | print("b", b) 47 | return a + b 48 | 49 | 50 | # Create tools using the @tool decorator 51 | # The @tool decorator simplifies the process of defining tools by handling the setup automatically. 52 | tools = [ 53 | greet_user, # Simple tool without args_schema 54 | reverse_string, # Tool with one parameter using args_schema 55 | concatenate_strings, # Tool with two parameters using args_schema 56 | ] 57 | 58 | # Initialize a ChatOpenAI model 59 | llm = ChatOpenAI(model="gpt-4o") 60 | 61 | # Pull the prompt template from the hub 62 | prompt = hub.pull("hwchase17/openai-tools-agent") 63 | 64 | # Create the ReAct agent using the create_tool_calling_agent function 65 | # This function sets up an agent capable of calling tools based on the provided prompt. 66 | agent = create_tool_calling_agent( 67 | llm=llm, # Language model to use 68 | tools=tools, # List of tools available to the agent 69 | prompt=prompt, # Prompt template to guide the agent's responses 70 | ) 71 | 72 | # Create the agent executor 73 | agent_executor = AgentExecutor.from_agent_and_tools( 74 | agent=agent, # The agent to execute 75 | tools=tools, # List of tools available to the agent 76 | verbose=True, # Enable verbose logging 77 | handle_parsing_errors=True, # Handle parsing errors gracefully 78 | ) 79 | 80 | # Test the agent with sample queries 81 | response = agent_executor.invoke({"input": "Greet Alice"}) 82 | print("Response for 'Greet Alice':", response) 83 | 84 | response = agent_executor.invoke({"input": "Reverse the string 'hello'"}) 85 | print("Response for 'Reverse the string hello':", response) 86 | 87 | response = agent_executor.invoke({"input": "Concatenate 'hello' and 'world'"}) 88 | print("Response for 'Concatenate hello and world':", response) 89 | -------------------------------------------------------------------------------- /4_rag/8_rag_web_scrape_firecrawl.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from dotenv import load_dotenv 4 | from langchain.text_splitter import CharacterTextSplitter 5 | from langchain_community.document_loaders import FireCrawlLoader 6 | from langchain_community.vectorstores import Chroma 7 | from langchain_openai import OpenAIEmbeddings 8 | 9 | # Load environment variables from .env 10 | load_dotenv() 11 | 12 | # Define the persistent directory 13 | current_dir = os.path.dirname(os.path.abspath(__file__)) 14 | db_dir = os.path.join(current_dir, "db") 15 | persistent_directory = os.path.join(db_dir, "chroma_db_firecrawl") 16 | 17 | 18 | def create_vector_store(): 19 | """Crawl the website, split the content, create embeddings, and persist the vector store.""" 20 | # Define the Firecrawl API key 21 | api_key = os.getenv("FIRECRAWL_API_KEY") 22 | if not api_key: 23 | raise ValueError("FIRECRAWL_API_KEY environment variable not set") 24 | 25 | # Step 1: Crawl the website using FireCrawlLoader 26 | print("Begin crawling the website...") 27 | loader = FireCrawlLoader( 28 | api_key=api_key, url="https://apple.com", mode="scrape") 29 | docs = loader.load() 30 | print("Finished crawling the website.") 31 | 32 | # Convert metadata values to strings if they are lists 33 | for doc in docs: 34 | for key, value in doc.metadata.items(): 35 | if isinstance(value, list): 36 | doc.metadata[key] = ", ".join(map(str, value)) 37 | 38 | # Step 2: Split the crawled content into chunks 39 | text_splitter = CharacterTextSplitter(chunk_size=1000, chunk_overlap=0) 40 | split_docs = text_splitter.split_documents(docs) 41 | 42 | # Display information about the split documents 43 | print("\n--- Document Chunks Information ---") 44 | print(f"Number of document chunks: {len(split_docs)}") 45 | print(f"Sample chunk:\n{split_docs[0].page_content}\n") 46 | 47 | # Step 3: Create embeddings for the document chunks 48 | embeddings = OpenAIEmbeddings(model="text-embedding-3-small") 49 | 50 | # Step 4: Create and persist the vector store with the embeddings 51 | print(f"\n--- Creating vector store in {persistent_directory} ---") 52 | db = Chroma.from_documents( 53 | split_docs, embeddings, persist_directory=persistent_directory 54 | ) 55 | print(f"--- Finished creating vector store in {persistent_directory} ---") 56 | 57 | 58 | # Check if the Chroma vector store already exists 59 | if not os.path.exists(persistent_directory): 60 | create_vector_store() 61 | else: 62 | print( 63 | f"Vector store {persistent_directory} already exists. No need to initialize.") 64 | 65 | # Load the vector store with the embeddings 66 | embeddings = OpenAIEmbeddings(model="text-embedding-3-small") 67 | db = Chroma(persist_directory=persistent_directory, 68 | embedding_function=embeddings) 69 | 70 | 71 | # Step 5: Query the vector store 72 | def query_vector_store(query): 73 | """Query the vector store with the specified question.""" 74 | # Create a retriever for querying the vector store 75 | retriever = db.as_retriever( 76 | search_type="similarity", 77 | search_kwargs={"k": 3}, 78 | ) 79 | 80 | # Retrieve relevant documents based on the query 81 | relevant_docs = retriever.invoke(query) 82 | 83 | # Display the relevant results with metadata 84 | print("\n--- Relevant Documents ---") 85 | for i, doc in enumerate(relevant_docs, 1): 86 | print(f"Document {i}:\n{doc.page_content}\n") 87 | if doc.metadata: 88 | print(f"Source: {doc.metadata.get('source', 'Unknown')}\n") 89 | 90 | 91 | # Define the user's question 92 | query = "Apple Intelligence?" 93 | 94 | # Query the vector store with the user's question 95 | query_vector_store(query) 96 | -------------------------------------------------------------------------------- /5_agents_and_tools/agent_deep_dive/1_agent_react_chat.py: -------------------------------------------------------------------------------- 1 | from dotenv import load_dotenv 2 | from langchain import hub 3 | from langchain.agents import AgentExecutor, create_structured_chat_agent 4 | from langchain.memory import ConversationBufferMemory 5 | from langchain_core.messages import AIMessage, HumanMessage, SystemMessage 6 | from langchain_core.tools import Tool 7 | from langchain_openai import ChatOpenAI 8 | 9 | # Load environment variables from .env file 10 | load_dotenv() 11 | 12 | 13 | # Define Tools 14 | def get_current_time(*args, **kwargs): 15 | """Returns the current time in H:MM AM/PM format.""" 16 | import datetime 17 | 18 | now = datetime.datetime.now() 19 | return now.strftime("%I:%M %p") 20 | 21 | 22 | def search_wikipedia(query): 23 | """Searches Wikipedia and returns the summary of the first result.""" 24 | from wikipedia import summary 25 | 26 | try: 27 | # Limit to two sentences for brevity 28 | return summary(query, sentences=2) 29 | except: 30 | return "I couldn't find any information on that." 31 | 32 | 33 | # Define the tools that the agent can use 34 | tools = [ 35 | Tool( 36 | name="Time", 37 | func=get_current_time, 38 | description="Useful for when you need to know the current time.", 39 | ), 40 | Tool( 41 | name="Wikipedia", 42 | func=search_wikipedia, 43 | description="Useful for when you need to know information about a topic.", 44 | ), 45 | ] 46 | 47 | # Load the correct JSON Chat Prompt from the hub 48 | prompt = hub.pull("hwchase17/structured-chat-agent") 49 | 50 | # Initialize a ChatOpenAI model 51 | llm = ChatOpenAI(model="gpt-4o") 52 | 53 | # Create a structured Chat Agent with Conversation Buffer Memory 54 | # ConversationBufferMemory stores the conversation history, allowing the agent to maintain context across interactions 55 | memory = ConversationBufferMemory( 56 | memory_key="chat_history", return_messages=True) 57 | 58 | # create_structured_chat_agent initializes a chat agent designed to interact using a structured prompt and tools 59 | # It combines the language model (llm), tools, and prompt to create an interactive agent 60 | agent = create_structured_chat_agent(llm=llm, tools=tools, prompt=prompt) 61 | 62 | # AgentExecutor is responsible for managing the interaction between the user input, the agent, and the tools 63 | # It also handles memory to ensure context is maintained throughout the conversation 64 | agent_executor = AgentExecutor.from_agent_and_tools( 65 | agent=agent, 66 | tools=tools, 67 | verbose=True, 68 | memory=memory, # Use the conversation memory to maintain context 69 | handle_parsing_errors=True, # Handle any parsing errors gracefully 70 | ) 71 | 72 | # Initial system message to set the context for the chat 73 | # SystemMessage is used to define a message from the system to the agent, setting initial instructions or context 74 | initial_message = "You are an AI assistant that can provide helpful answers using available tools.\nIf you are unable to answer, you can use the following tools: Time and Wikipedia." 75 | memory.chat_memory.add_message(SystemMessage(content=initial_message)) 76 | 77 | # Chat Loop to interact with the user 78 | while True: 79 | user_input = input("User: ") 80 | if user_input.lower() == "exit": 81 | break 82 | 83 | # Add the user's message to the conversation memory 84 | memory.chat_memory.add_message(HumanMessage(content=user_input)) 85 | 86 | # Invoke the agent with the user input and the current chat history 87 | response = agent_executor.invoke({"input": user_input}) 88 | print("Bot:", response["output"]) 89 | 90 | # Add the agent's response to the conversation memory 91 | memory.chat_memory.add_message(AIMessage(content=response["output"])) 92 | -------------------------------------------------------------------------------- /4_rag/5_rag_retriever_deep_dive.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from dotenv import load_dotenv 4 | from langchain_community.vectorstores import Chroma 5 | from langchain_openai import OpenAIEmbeddings 6 | 7 | load_dotenv() 8 | 9 | # Define the persistent directory 10 | current_dir = os.path.dirname(os.path.abspath(__file__)) 11 | db_dir = os.path.join(current_dir, "db") 12 | persistent_directory = os.path.join(db_dir, "chroma_db_with_metadata") 13 | 14 | # Define the embedding model 15 | embeddings = OpenAIEmbeddings(model="text-embedding-3-small") 16 | 17 | # Load the existing vector store with the embedding function 18 | db = Chroma(persist_directory=persistent_directory, 19 | embedding_function=embeddings) 20 | 21 | 22 | # Function to query a vector store with different search types and parameters 23 | def query_vector_store( 24 | store_name, query, embedding_function, search_type, search_kwargs 25 | ): 26 | if os.path.exists(persistent_directory): 27 | print(f"\n--- Querying the Vector Store {store_name} ---") 28 | db = Chroma( 29 | persist_directory=persistent_directory, 30 | embedding_function=embedding_function, 31 | ) 32 | retriever = db.as_retriever( 33 | search_type=search_type, 34 | search_kwargs=search_kwargs, 35 | ) 36 | relevant_docs = retriever.invoke(query) 37 | # Display the relevant results with metadata 38 | print(f"\n--- Relevant Documents for {store_name} ---") 39 | for i, doc in enumerate(relevant_docs, 1): 40 | print(f"Document {i}:\n{doc.page_content}\n") 41 | if doc.metadata: 42 | print(f"Source: {doc.metadata.get('source', 'Unknown')}\n") 43 | else: 44 | print(f"Vector store {store_name} does not exist.") 45 | 46 | 47 | # Define the user's question 48 | query = "How did Juliet die?" 49 | 50 | # Showcase different retrieval methods 51 | 52 | # 1. Similarity Search 53 | # This method retrieves documents based on vector similarity. 54 | # It finds the most similar documents to the query vector based on cosine similarity. 55 | # Use this when you want to retrieve the top k most similar documents. 56 | print("\n--- Using Similarity Search ---") 57 | query_vector_store("chroma_db_with_metadata", query, 58 | embeddings, "similarity", {"k": 3}) 59 | 60 | # 2. Max Marginal Relevance (MMR) 61 | # This method balances between selecting documents that are relevant to the query and diverse among themselves. 62 | # 'fetch_k' specifies the number of documents to initially fetch based on similarity. 63 | # 'lambda_mult' controls the diversity of the results: 1 for minimum diversity, 0 for maximum. 64 | # Use this when you want to avoid redundancy and retrieve diverse yet relevant documents. 65 | # Note: Relevance measures how closely documents match the query. 66 | # Note: Diversity ensures that the retrieved documents are not too similar to each other, 67 | # providing a broader range of information. 68 | print("\n--- Using Max Marginal Relevance (MMR) ---") 69 | query_vector_store( 70 | "chroma_db_with_metadata", 71 | query, 72 | embeddings, 73 | "mmr", 74 | {"k": 3, "fetch_k": 20, "lambda_mult": 0.5}, 75 | ) 76 | 77 | # 3. Similarity Score Threshold 78 | # This method retrieves documents that exceed a certain similarity score threshold. 79 | # 'score_threshold' sets the minimum similarity score a document must have to be considered relevant. 80 | # Use this when you want to ensure that only highly relevant documents are retrieved, filtering out less relevant ones. 81 | print("\n--- Using Similarity Score Threshold ---") 82 | query_vector_store( 83 | "chroma_db_with_metadata", 84 | query, 85 | embeddings, 86 | "similarity_score_threshold", 87 | {"k": 3, "score_threshold": 0.1}, 88 | ) 89 | 90 | print("Querying demonstrations with different search types completed.") 91 | -------------------------------------------------------------------------------- /4_rag/4_rag_embedding_deep_dive.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from langchain.embeddings import HuggingFaceEmbeddings 4 | from langchain.text_splitter import CharacterTextSplitter 5 | from langchain_community.document_loaders import TextLoader 6 | from langchain_community.vectorstores import Chroma 7 | from langchain_openai import OpenAIEmbeddings 8 | 9 | # Define the directory containing the text file and the persistent directory 10 | current_dir = os.path.dirname(os.path.abspath(__file__)) 11 | file_path = os.path.join(current_dir, "books", "odyssey.txt") 12 | db_dir = os.path.join(current_dir, "db") 13 | 14 | # Check if the text file exists 15 | if not os.path.exists(file_path): 16 | raise FileNotFoundError( 17 | f"The file {file_path} does not exist. Please check the path." 18 | ) 19 | 20 | # Read the text content from the file 21 | loader = TextLoader(file_path) 22 | documents = loader.load() 23 | 24 | # Split the document into chunks 25 | text_splitter = CharacterTextSplitter(chunk_size=1000, chunk_overlap=0) 26 | docs = text_splitter.split_documents(documents) 27 | 28 | # Display information about the split documents 29 | print("\n--- Document Chunks Information ---") 30 | print(f"Number of document chunks: {len(docs)}") 31 | print(f"Sample chunk:\n{docs[0].page_content}\n") 32 | 33 | 34 | # Function to create and persist vector store 35 | def create_vector_store(docs, embeddings, store_name): 36 | persistent_directory = os.path.join(db_dir, store_name) 37 | if not os.path.exists(persistent_directory): 38 | print(f"\n--- Creating vector store {store_name} ---") 39 | Chroma.from_documents( 40 | docs, embeddings, persist_directory=persistent_directory) 41 | print(f"--- Finished creating vector store {store_name} ---") 42 | else: 43 | print( 44 | f"Vector store {store_name} already exists. No need to initialize.") 45 | 46 | 47 | # 1. OpenAI Embeddings 48 | # Uses OpenAI's embedding models. 49 | # Useful for general-purpose embeddings with high accuracy. 50 | # Note: The cost of using OpenAI embeddings will depend on your OpenAI API usage and pricing plan. 51 | # Pricing: https://openai.com/api/pricing/ 52 | print("\n--- Using OpenAI Embeddings ---") 53 | openai_embeddings = OpenAIEmbeddings(model="text-embedding-ada-002") 54 | create_vector_store(docs, openai_embeddings, "chroma_db_openai") 55 | 56 | # 2. Hugging Face Transformers 57 | # Uses models from the Hugging Face library. 58 | # Ideal for leveraging a wide variety of models for different tasks. 59 | # Note: Running Hugging Face models locally on your machine incurs no direct cost other than using your computational resources. 60 | # Note: Find other models at https://huggingface.co/models?other=embeddings 61 | print("\n--- Using Hugging Face Transformers ---") 62 | huggingface_embeddings = HuggingFaceEmbeddings( 63 | model_name="sentence-transformers/all-mpnet-base-v2" 64 | ) 65 | create_vector_store(docs, huggingface_embeddings, "chroma_db_huggingface") 66 | 67 | print("Embedding demonstrations for OpenAI and Hugging Face completed.") 68 | 69 | 70 | # Function to query a vector store 71 | def query_vector_store(store_name, query, embedding_function): 72 | persistent_directory = os.path.join(db_dir, store_name) 73 | if os.path.exists(persistent_directory): 74 | print(f"\n--- Querying the Vector Store {store_name} ---") 75 | db = Chroma( 76 | persist_directory=persistent_directory, 77 | embedding_function=embedding_function, 78 | ) 79 | retriever = db.as_retriever( 80 | search_type="similarity_score_threshold", 81 | search_kwargs={"k": 3, "score_threshold": 0.1}, 82 | ) 83 | relevant_docs = retriever.invoke(query) 84 | # Display the relevant results with metadata 85 | print(f"\n--- Relevant Documents for {store_name} ---") 86 | for i, doc in enumerate(relevant_docs, 1): 87 | print(f"Document {i}:\n{doc.page_content}\n") 88 | if doc.metadata: 89 | print(f"Source: {doc.metadata.get('source', 'Unknown')}\n") 90 | else: 91 | print(f"Vector store {store_name} does not exist.") 92 | 93 | 94 | # Define the user's question 95 | query = "Who is Odysseus' wife?" 96 | 97 | # Query each vector store 98 | query_vector_store("chroma_db_openai", query, openai_embeddings) 99 | query_vector_store("chroma_db_huggingface", query, huggingface_embeddings) 100 | 101 | print("Querying demonstrations completed.") 102 | -------------------------------------------------------------------------------- /4_rag/7_rag_conversational.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from dotenv import load_dotenv 4 | from langchain.chains import create_history_aware_retriever, create_retrieval_chain 5 | from langchain.chains.combine_documents import create_stuff_documents_chain 6 | from langchain_community.vectorstores import Chroma 7 | from langchain_core.messages import HumanMessage, SystemMessage 8 | from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder 9 | from langchain_openai import ChatOpenAI, OpenAIEmbeddings 10 | 11 | # Load environment variables from .env 12 | load_dotenv() 13 | 14 | # Define the persistent directory 15 | current_dir = os.path.dirname(os.path.abspath(__file__)) 16 | persistent_directory = os.path.join(current_dir, "db", "chroma_db_with_metadata") 17 | 18 | # Define the embedding model 19 | embeddings = OpenAIEmbeddings(model="text-embedding-3-small") 20 | 21 | # Load the existing vector store with the embedding function 22 | db = Chroma(persist_directory=persistent_directory, embedding_function=embeddings) 23 | 24 | # Create a retriever for querying the vector store 25 | # `search_type` specifies the type of search (e.g., similarity) 26 | # `search_kwargs` contains additional arguments for the search (e.g., number of results to return) 27 | retriever = db.as_retriever( 28 | search_type="similarity", 29 | search_kwargs={"k": 3}, 30 | ) 31 | 32 | # Create a ChatOpenAI model 33 | llm = ChatOpenAI(model="gpt-4o") 34 | 35 | # Contextualize question prompt 36 | # This system prompt helps the AI understand that it should reformulate the question 37 | # based on the chat history to make it a standalone question 38 | contextualize_q_system_prompt = ( 39 | "Given a chat history and the latest user question " 40 | "which might reference context in the chat history, " 41 | "formulate a standalone question which can be understood " 42 | "without the chat history. Do NOT answer the question, just " 43 | "reformulate it if needed and otherwise return it as is." 44 | ) 45 | 46 | # Create a prompt template for contextualizing questions 47 | contextualize_q_prompt = ChatPromptTemplate.from_messages( 48 | [ 49 | ("system", contextualize_q_system_prompt), 50 | MessagesPlaceholder("chat_history"), 51 | ("human", "{input}"), 52 | ] 53 | ) 54 | 55 | # Create a history-aware retriever 56 | # This uses the LLM to help reformulate the question based on chat history 57 | history_aware_retriever = create_history_aware_retriever( 58 | llm, retriever, contextualize_q_prompt 59 | ) 60 | 61 | # Answer question prompt 62 | # This system prompt helps the AI understand that it should provide concise answers 63 | # based on the retrieved context and indicates what to do if the answer is unknown 64 | qa_system_prompt = ( 65 | "You are an assistant for question-answering tasks. Use " 66 | "the following pieces of retrieved context to answer the " 67 | "question. If you don't know the answer, just say that you " 68 | "don't know. Use three sentences maximum and keep the answer " 69 | "concise." 70 | "\n\n" 71 | "{context}" 72 | ) 73 | 74 | # Create a prompt template for answering questions 75 | qa_prompt = ChatPromptTemplate.from_messages( 76 | [ 77 | ("system", qa_system_prompt), 78 | MessagesPlaceholder("chat_history"), 79 | ("human", "{input}"), 80 | ] 81 | ) 82 | 83 | # Create a chain to combine documents for question answering 84 | # `create_stuff_documents_chain` feeds all retrieved context into the LLM 85 | question_answer_chain = create_stuff_documents_chain(llm, qa_prompt) 86 | 87 | # Create a retrieval chain that combines the history-aware retriever and the question answering chain 88 | rag_chain = create_retrieval_chain(history_aware_retriever, question_answer_chain) 89 | 90 | 91 | # Function to simulate a continual chat 92 | def continual_chat(): 93 | print("Start chatting with the AI! Type 'exit' to end the conversation.") 94 | chat_history = [] # Collect chat history here (a sequence of messages) 95 | while True: 96 | query = input("You: ") 97 | if query.lower() == "exit": 98 | break 99 | # Process the user's query through the retrieval chain 100 | result = rag_chain.invoke({"input": query, "chat_history": chat_history}) 101 | # Display the AI's response 102 | print(f"AI: {result['answer']}") 103 | # Update the chat history 104 | chat_history.append(HumanMessage(content=query)) 105 | chat_history.append(SystemMessage(content=result["answer"])) 106 | 107 | 108 | # Main function to start the continual chat 109 | if __name__ == "__main__": 110 | continual_chat() 111 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # LangChain Crash Course 2 | 3 | Welcome to the LangChain Crash Course repository! This repo contains all the code examples you'll need to follow along with the LangChain Master Class for Beginners video. By the end of this course, you'll know how to use LangChain to create your own AI agents, build RAG chatbots, and automate tasks with AI. 4 | 5 | ## Course Outline 6 | 7 | 1. **Setup Environment** 8 | 2. **Chat Models** 9 | 3. **Prompt Templates** 10 | 4. **Chains** 11 | 5. **RAG (Retrieval-Augmented Generation)** 12 | 6. **Agents & Tools** 13 | 14 | ## Getting Started 15 | 16 | ### Prerequisites 17 | 18 | - Python 3.10 or 3.11 19 | - Poetry (Follow this [Poetry installation tutorial](https://python-poetry.org/docs/#installation) to install Poetry on your system) 20 | 21 | ### Installation 22 | 23 | 1. Clone the repository: 24 | 25 | ```bash 26 | 27 | git clone https://github.com/bhancockio/langchain-crash-course 28 | cd langchain-crash-course 29 | ``` 30 | 31 | 2. Install dependencies using Poetry: 32 | 33 | ```bash 34 | poetry install --no-root 35 | ``` 36 | 37 | 3. Set up your environment variables: 38 | 39 | - Rename the `.env.example` file to `.env` and update the variables inside with your own values. Example: 40 | 41 | ```bash 42 | mv .env.example .env 43 | ``` 44 | 45 | 4. Activate the Poetry shell to run the examples: 46 | 47 | ```bash 48 | poetry shell 49 | ``` 50 | 51 | 5. Run the code examples: 52 | 53 | ```bash 54 | python 1_chat_models/1_chat_model_basic.py 55 | ``` 56 | 57 | ## Repository Structure 58 | 59 | Here's a breakdown of the folders and what you'll find in each: 60 | 61 | ### 1. Chat Models 62 | 63 | - `1_chat_model_basic.py` 64 | - `2_chat_model_basic_conversation.py` 65 | - `3_chat_model_alternatives.py` 66 | - `4_chat_model_conversation_with_user.py` 67 | - `5_chat_model_save_message_history_firestore.py` 68 | 69 | Learn how to interact with models like ChatGPT, Claude, and Gemini. 70 | 71 | ### 2. Prompt Templates 72 | 73 | - `1_prompt_template_basic.py` 74 | - `2_prompt_template_with_chat_model.py` 75 | 76 | Understand the basics of prompt templates and how to use them effectively. 77 | 78 | ### 3. Chains 79 | 80 | - `1_chains_basics.py` 81 | - `2_chains_under_the_hood.py` 82 | - `3_chains_extended.py` 83 | - `4_chains_parallel.py` 84 | - `5_chains_branching.py` 85 | 86 | Learn how to create chains using Chat Models and Prompts to automate tasks. 87 | 88 | ### 4. RAG (Retrieval-Augmented Generation) 89 | 90 | - `1a_rag_basics.py` 91 | - `1b_rag_basics.py` 92 | - `2a_rag_basics_metadata.py` 93 | - `2b_rag_basics_metadata.py` 94 | - `3_rag_text_splitting_deep_dive.py` 95 | - `4_rag_embedding_deep_dive.py` 96 | - `5_rag_retriever_deep_dive.py` 97 | - `6_rag_one_off_question.py` 98 | - `7_rag_conversational.py` 99 | - `8_rag_web_scrape_firecrawl.py` 100 | - `8_rag_web_scrape.py` 101 | 102 | Explore the technologies like documents, embeddings, and vector stores that enable RAG queries. 103 | 104 | ### 5. Agents & Tools 105 | 106 | - `1_agent_and_tools_basics.py` 107 | - `agent_deep_dive/` 108 | - `1_agent_react_chat.py` 109 | - `2_react_docstore.py` 110 | - `tools_deep_dive/` 111 | - `1_tool_constructor.py` 112 | - `2_tool_decorator.py` 113 | - `3_tool_base_tool.py` 114 | 115 | Learn about agents, how they work, and how to build custom tools to enhance their capabilities. 116 | 117 | ## How to Use This Repository 118 | 119 | 1. **Watch the Video:** Start by watching the LangChain Master Class for Beginners video on YouTube at 2X speed for a high-level overview. 120 | 121 | 2. **Run the Code Examples:** Follow along with the code examples provided in this repository. Each section in the video corresponds to a folder in this repo. 122 | 123 | 3. **Join the Community:** If you get stuck or want to connect with other AI developers, join the FREE Skool community [here](https://www.skool.com/ai-developer-accelerator/about). 124 | 125 | ## Comprehensive Documentation 126 | 127 | Each script in this repository contains detailed comments explaining the purpose and functionality of the code. This will help you understand the flow and logic behind each example. 128 | 129 | ## FAQ 130 | 131 | **Q: What is LangChain?** 132 | A: LangChain is a framework designed to simplify the process of building applications that utilize language models. 133 | 134 | **Q: How do I set up my environment?** 135 | A: Follow the instructions in the "Getting Started" section above. Ensure you have Python 3.10 or 3.11 installed, install Poetry, clone the repository, install dependencies, rename the `.env.example` file to `.env`, and activate the Poetry shell. 136 | 137 | **Q: I am getting an error when running the examples. What should I do?** 138 | A: Ensure all dependencies are installed correctly and your environment variables are set up properly. If the issue persists, seek help in the Skool community or open an issue on GitHub. 139 | 140 | **Q: Can I contribute to this repository?** 141 | A: Yes! Contributions are welcome. Please open an issue or submit a pull request with your changes. 142 | 143 | **Q: Where can I find more information about LangChain?** 144 | A: Check out the official LangChain documentation and join the Skool community for additional resources and support. 145 | 146 | ## Support 147 | 148 | If you encounter any issues or have questions, feel free to open an issue on GitHub or ask for help in the Skool community. 149 | 150 | ## License 151 | 152 | This project is licensed under the MIT License. 153 | -------------------------------------------------------------------------------- /4_rag/3_rag_text_splitting_deep_dive.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from langchain.text_splitter import ( 4 | CharacterTextSplitter, 5 | RecursiveCharacterTextSplitter, 6 | SentenceTransformersTokenTextSplitter, 7 | TextSplitter, 8 | TokenTextSplitter, 9 | ) 10 | from langchain_community.document_loaders import TextLoader 11 | from langchain_community.vectorstores import Chroma 12 | from langchain_openai import OpenAIEmbeddings 13 | 14 | # Define the directory containing the text file 15 | current_dir = os.path.dirname(os.path.abspath(__file__)) 16 | file_path = os.path.join(current_dir, "books", "romeo_and_juliet.txt") 17 | db_dir = os.path.join(current_dir, "db") 18 | 19 | # Check if the text file exists 20 | if not os.path.exists(file_path): 21 | raise FileNotFoundError( 22 | f"The file {file_path} does not exist. Please check the path." 23 | ) 24 | 25 | # Read the text content from the file 26 | loader = TextLoader(file_path) 27 | documents = loader.load() 28 | 29 | # Define the embedding model 30 | embeddings = OpenAIEmbeddings( 31 | model="text-embedding-3-small" 32 | ) # Update to a valid embedding model if needed 33 | 34 | 35 | # Function to create and persist vector store 36 | def create_vector_store(docs, store_name): 37 | persistent_directory = os.path.join(db_dir, store_name) 38 | if not os.path.exists(persistent_directory): 39 | print(f"\n--- Creating vector store {store_name} ---") 40 | db = Chroma.from_documents( 41 | docs, embeddings, persist_directory=persistent_directory 42 | ) 43 | print(f"--- Finished creating vector store {store_name} ---") 44 | else: 45 | print( 46 | f"Vector store {store_name} already exists. No need to initialize.") 47 | 48 | 49 | # 1. Character-based Splitting 50 | # Splits text into chunks based on a specified number of characters. 51 | # Useful for consistent chunk sizes regardless of content structure. 52 | print("\n--- Using Character-based Splitting ---") 53 | char_splitter = CharacterTextSplitter(chunk_size=1000, chunk_overlap=100) 54 | char_docs = char_splitter.split_documents(documents) 55 | create_vector_store(char_docs, "chroma_db_char") 56 | 57 | # 2. Sentence-based Splitting 58 | # Splits text into chunks based on sentences, ensuring chunks end at sentence boundaries. 59 | # Ideal for maintaining semantic coherence within chunks. 60 | print("\n--- Using Sentence-based Splitting ---") 61 | sent_splitter = SentenceTransformersTokenTextSplitter(chunk_size=1000) 62 | sent_docs = sent_splitter.split_documents(documents) 63 | create_vector_store(sent_docs, "chroma_db_sent") 64 | 65 | # 3. Token-based Splitting 66 | # Splits text into chunks based on tokens (words or subwords), using tokenizers like GPT-2. 67 | # Useful for transformer models with strict token limits. 68 | print("\n--- Using Token-based Splitting ---") 69 | token_splitter = TokenTextSplitter(chunk_overlap=0, chunk_size=512) 70 | token_docs = token_splitter.split_documents(documents) 71 | create_vector_store(token_docs, "chroma_db_token") 72 | 73 | # 4. Recursive Character-based Splitting 74 | # Attempts to split text at natural boundaries (sentences, paragraphs) within character limit. 75 | # Balances between maintaining coherence and adhering to character limits. 76 | print("\n--- Using Recursive Character-based Splitting ---") 77 | rec_char_splitter = RecursiveCharacterTextSplitter( 78 | chunk_size=1000, chunk_overlap=100) 79 | rec_char_docs = rec_char_splitter.split_documents(documents) 80 | create_vector_store(rec_char_docs, "chroma_db_rec_char") 81 | 82 | # 5. Custom Splitting 83 | # Allows creating custom splitting logic based on specific requirements. 84 | # Useful for documents with unique structure that standard splitters can't handle. 85 | print("\n--- Using Custom Splitting ---") 86 | 87 | 88 | class CustomTextSplitter(TextSplitter): 89 | def split_text(self, text): 90 | # Custom logic for splitting text 91 | return text.split("\n\n") # Example: split by paragraphs 92 | 93 | 94 | custom_splitter = CustomTextSplitter() 95 | custom_docs = custom_splitter.split_documents(documents) 96 | create_vector_store(custom_docs, "chroma_db_custom") 97 | 98 | 99 | # Function to query a vector store 100 | def query_vector_store(store_name, query): 101 | persistent_directory = os.path.join(db_dir, store_name) 102 | if os.path.exists(persistent_directory): 103 | print(f"\n--- Querying the Vector Store {store_name} ---") 104 | db = Chroma( 105 | persist_directory=persistent_directory, embedding_function=embeddings 106 | ) 107 | retriever = db.as_retriever( 108 | search_type="similarity_score_threshold", 109 | search_kwargs={"k": 1, "score_threshold": 0.1}, 110 | ) 111 | relevant_docs = retriever.invoke(query) 112 | # Display the relevant results with metadata 113 | print(f"\n--- Relevant Documents for {store_name} ---") 114 | for i, doc in enumerate(relevant_docs, 1): 115 | print(f"Document {i}:\n{doc.page_content}\n") 116 | if doc.metadata: 117 | print(f"Source: {doc.metadata.get('source', 'Unknown')}\n") 118 | else: 119 | print(f"Vector store {store_name} does not exist.") 120 | 121 | 122 | # Define the user's question 123 | query = "How did Juliet die?" 124 | 125 | # Query each vector store 126 | query_vector_store("chroma_db_char", query) 127 | query_vector_store("chroma_db_sent", query) 128 | query_vector_store("chroma_db_token", query) 129 | query_vector_store("chroma_db_rec_char", query) 130 | query_vector_store("chroma_db_custom", query) 131 | -------------------------------------------------------------------------------- /5_agents_and_tools/agent_deep_dive/2_agent_react_docstore.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from dotenv import load_dotenv 4 | from langchain import hub 5 | from langchain.agents import AgentExecutor, create_react_agent 6 | from langchain.chains import create_history_aware_retriever, create_retrieval_chain 7 | from langchain.chains.combine_documents import create_stuff_documents_chain 8 | from langchain_community.vectorstores import Chroma 9 | from langchain_core.messages import AIMessage, HumanMessage 10 | from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder 11 | from langchain_core.tools import Tool 12 | from langchain_openai import ChatOpenAI, OpenAIEmbeddings 13 | 14 | # Load environment variables from .env file 15 | load_dotenv() 16 | 17 | # Load the existing Chroma vector store 18 | current_dir = os.path.dirname(os.path.abspath(__file__)) 19 | db_dir = os.path.join(current_dir, "..", "..", "4_rag", "db") 20 | persistent_directory = os.path.join(db_dir, "chroma_db_with_metadata") 21 | 22 | # Check if the Chroma vector store already exists 23 | if os.path.exists(persistent_directory): 24 | print("Loading existing vector store...") 25 | db = Chroma(persist_directory=persistent_directory, 26 | embedding_function=None) 27 | else: 28 | raise FileNotFoundError( 29 | f"The directory {persistent_directory} does not exist. Please check the path." 30 | ) 31 | 32 | # Define the embedding model 33 | embeddings = OpenAIEmbeddings(model="text-embedding-3-small") 34 | 35 | # Load the existing vector store with the embedding function 36 | db = Chroma(persist_directory=persistent_directory, 37 | embedding_function=embeddings) 38 | 39 | # Create a retriever for querying the vector store 40 | # `search_type` specifies the type of search (e.g., similarity) 41 | # `search_kwargs` contains additional arguments for the search (e.g., number of results to return) 42 | retriever = db.as_retriever( 43 | search_type="similarity", 44 | search_kwargs={"k": 3}, 45 | ) 46 | 47 | # Create a ChatOpenAI model 48 | llm = ChatOpenAI(model="gpt-4o") 49 | 50 | # Contextualize question prompt 51 | # This system prompt helps the AI understand that it should reformulate the question 52 | # based on the chat history to make it a standalone question 53 | contextualize_q_system_prompt = ( 54 | "Given a chat history and the latest user question " 55 | "which might reference context in the chat history, " 56 | "formulate a standalone question which can be understood " 57 | "without the chat history. Do NOT answer the question, just " 58 | "reformulate it if needed and otherwise return it as is." 59 | ) 60 | 61 | # Create a prompt template for contextualizing questions 62 | contextualize_q_prompt = ChatPromptTemplate.from_messages( 63 | [ 64 | ("system", contextualize_q_system_prompt), 65 | MessagesPlaceholder("chat_history"), 66 | ("human", "{input}"), 67 | ] 68 | ) 69 | 70 | # Create a history-aware retriever 71 | # This uses the LLM to help reformulate the question based on chat history 72 | history_aware_retriever = create_history_aware_retriever( 73 | llm, retriever, contextualize_q_prompt 74 | ) 75 | 76 | # Answer question prompt 77 | # This system prompt helps the AI understand that it should provide concise answers 78 | # based on the retrieved context and indicates what to do if the answer is unknown 79 | qa_system_prompt = ( 80 | "You are an assistant for question-answering tasks. Use " 81 | "the following pieces of retrieved context to answer the " 82 | "question. If you don't know the answer, just say that you " 83 | "don't know. Use three sentences maximum and keep the answer " 84 | "concise." 85 | "\n\n" 86 | "{context}" 87 | ) 88 | 89 | # Create a prompt template for answering questions 90 | qa_prompt = ChatPromptTemplate.from_messages( 91 | [ 92 | ("system", qa_system_prompt), 93 | MessagesPlaceholder("chat_history"), 94 | ("human", "{input}"), 95 | ] 96 | ) 97 | 98 | # Create a chain to combine documents for question answering 99 | # `create_stuff_documents_chain` feeds all retrieved context into the LLM 100 | question_answer_chain = create_stuff_documents_chain(llm, qa_prompt) 101 | 102 | # Create a retrieval chain that combines the history-aware retriever and the question answering chain 103 | rag_chain = create_retrieval_chain( 104 | history_aware_retriever, question_answer_chain) 105 | 106 | 107 | # Set Up ReAct Agent with Document Store Retriever 108 | # Load the ReAct Docstore Prompt 109 | react_docstore_prompt = hub.pull("hwchase17/react") 110 | 111 | tools = [ 112 | Tool( 113 | name="Answer Question", 114 | func=lambda input, **kwargs: rag_chain.invoke( 115 | {"input": input, "chat_history": kwargs.get("chat_history", [])} 116 | ), 117 | description="useful for when you need to answer questions about the context", 118 | ) 119 | ] 120 | 121 | # Create the ReAct Agent with document store retriever 122 | agent = create_react_agent( 123 | llm=llm, 124 | tools=tools, 125 | prompt=react_docstore_prompt, 126 | ) 127 | 128 | agent_executor = AgentExecutor.from_agent_and_tools( 129 | agent=agent, tools=tools, handle_parsing_errors=True, verbose=True, 130 | ) 131 | 132 | chat_history = [] 133 | while True: 134 | query = input("You: ") 135 | if query.lower() == "exit": 136 | break 137 | response = agent_executor.invoke( 138 | {"input": query, "chat_history": chat_history}) 139 | print(f"AI: {response['output']}") 140 | 141 | # Update history 142 | chat_history.append(HumanMessage(content=query)) 143 | chat_history.append(AIMessage(content=response["output"])) 144 | -------------------------------------------------------------------------------- /4_rag/books/us_bill_of_rights.txt: -------------------------------------------------------------------------------- 1 | The Project Gutenberg eBook of The United States Bill of Rights 2 | 3 | This ebook is for the use of anyone anywhere in the United States and 4 | most other parts of the world at no cost and with almost no restrictions 5 | whatsoever. You may copy it, give it away or re-use it under the terms 6 | of the Project Gutenberg License included with this ebook or online 7 | at www.gutenberg.org. If you are not located in the United States, 8 | you will have to check the laws of the country where you are located 9 | before using this eBook. 10 | 11 | Title: The United States Bill of Rights 12 | 13 | Author: United States 14 | 15 | Release date: December 1, 1972 [eBook #2] 16 | Most recently updated: April 1, 2015 17 | 18 | Language: English 19 | 20 | 21 | 22 | *** START OF THE PROJECT GUTENBERG EBOOK THE UNITED STATES BILL OF RIGHTS *** 23 | 24 | 25 | All of the original Project Gutenberg Etexts from the 26 | 1970's were produced in ALL CAPS, no lower case. The 27 | computers we used then didn't have lower case at all. 28 | 29 | *** 30 | 31 | These original Project Gutenberg Etexts will be compiled into a file 32 | containing them all, in order to improve the content ratios of Etext 33 | to header material. 34 | 35 | *** 36 | 37 | 38 | 39 | The United States Bill of Rights. 40 | 41 | The Ten Original Amendments to the Constitution of the United States 42 | Passed by Congress September 25, 1789 43 | Ratified December 15, 1791 44 | 45 | 46 | 47 | I 48 | 49 | Congress shall make no law respecting an establishment of religion, 50 | or prohibiting the free exercise thereof; or abridging the freedom of speech, 51 | or of the press, or the right of the people peaceably to assemble, 52 | and to petition the Government for a redress of grievances. 53 | 54 | 55 | II 56 | 57 | A well-regulated militia, being necessary to the security of a free State, 58 | the right of the people to keep and bear arms, shall not be infringed. 59 | 60 | 61 | III 62 | No soldier shall, in time of peace be quartered in any house, 63 | without the consent of the owner, nor in time of war, 64 | but in a manner to be prescribed by law. 65 | 66 | 67 | IV 68 | 69 | The right of the people to be secure in their persons, houses, papers, 70 | and effects, against unreasonable searches and seizures, shall not be violated, 71 | and no Warrants shall issue, but upon probable cause, supported by oath 72 | or affirmation, and particularly describing the place to be searched, 73 | and the persons or things to be seized. 74 | 75 | 76 | V 77 | 78 | No person shall be held to answer for a capital, or otherwise infamous crime, 79 | unless on a presentment or indictment of a Grand Jury, except in cases arising 80 | in the land or naval forces, or in the Militia, when in actual service 81 | in time of War or public danger; nor shall any person be subject for 82 | the same offense to be twice put in jeopardy of life or limb; 83 | nor shall be compelled in any criminal case to be a witness against himself, 84 | nor be deprived of life, liberty, or property, without due process of law; 85 | nor shall private property be taken for public use without just compensation. 86 | 87 | 88 | VI 89 | 90 | In all criminal prosecutions, the accused shall enjoy the right to a 91 | speedy and public trial, by an impartial jury of the State and district 92 | wherein the crime shall have been committed, which district shall have 93 | been previously ascertained by law, and to be informed of the nature 94 | and cause of the accusation; to be confronted with the witnesses against him; 95 | to have compulsory process for obtaining witnesses in his favor, 96 | and to have the assistance of counsel for his defense. 97 | 98 | 99 | VII 100 | 101 | In suits at common law, where the value in controversy shall exceed 102 | twenty dollars, the right of trial by jury shall be preserved, 103 | and no fact tried by a jury shall be otherwise re-examined in any court 104 | of the United States, than according to the rules of the common law. 105 | 106 | 107 | VIII 108 | 109 | Excessive bail shall not be required nor excessive fines imposed, 110 | nor cruel and unusual punishments inflicted. 111 | 112 | 113 | IX 114 | 115 | The enumeration in the Constitution, of certain rights, 116 | shall not be construed to deny or disparage others retained by the people. 117 | 118 | X 119 | 120 | The powers not delegated to the United States by the Constitution, 121 | nor prohibited by it to the States, are reserved to the States respectively, 122 | or to the people. 123 | 124 | 125 | 126 | *** END OF THE PROJECT GUTENBERG EBOOK THE UNITED STATES BILL OF RIGHTS *** 127 | 128 | 129 | 130 | 131 | Updated editions will replace the previous one—the old editions will 132 | be renamed. 133 | 134 | Creating the works from print editions not protected by U.S. copyright 135 | law means that no one owns a United States copyright in these works, 136 | so the Foundation (and you!) can copy and distribute it in the United 137 | States without permission and without paying copyright 138 | royalties. Special rules, set forth in the General Terms of Use part 139 | of this license, apply to copying and distributing Project 140 | Gutenberg™ electronic works to protect the PROJECT GUTENBERG™ 141 | concept and trademark. Project Gutenberg is a registered trademark, 142 | and may not be used if you charge for an eBook, except by following 143 | the terms of the trademark license, including paying royalties for use 144 | of the Project Gutenberg trademark. If you do not charge anything for 145 | copies of this eBook, complying with the trademark license is very 146 | easy. You may use this eBook for nearly any purpose such as creation 147 | of derivative works, reports, performances and research. Project 148 | Gutenberg eBooks may be modified and printed and given away—you may 149 | do practically ANYTHING in the United States with eBooks not protected 150 | by U.S. copyright law. Redistribution is subject to the trademark 151 | license, especially commercial redistribution. 152 | 153 | 154 | START: FULL LICENSE 155 | 156 | THE FULL PROJECT GUTENBERG LICENSE 157 | 158 | PLEASE READ THIS BEFORE YOU DISTRIBUTE OR USE THIS WORK 159 | 160 | To protect the Project Gutenberg™ mission of promoting the free 161 | distribution of electronic works, by using or distributing this work 162 | (or any other work associated in any way with the phrase “Project 163 | Gutenberg”), you agree to comply with all the terms of the Full 164 | Project Gutenberg™ License available with this file or online at 165 | www.gutenberg.org/license. 166 | 167 | Section 1. General Terms of Use and Redistributing Project Gutenberg™ 168 | electronic works 169 | 170 | 1.A. By reading or using any part of this Project Gutenberg™ 171 | electronic work, you indicate that you have read, understand, agree to 172 | and accept all the terms of this license and intellectual property 173 | (trademark/copyright) agreement. If you do not agree to abide by all 174 | the terms of this agreement, you must cease using and return or 175 | destroy all copies of Project Gutenberg™ electronic works in your 176 | possession. If you paid a fee for obtaining a copy of or access to a 177 | Project Gutenberg™ electronic work and you do not agree to be bound 178 | by the terms of this agreement, you may obtain a refund from the person 179 | or entity to whom you paid the fee as set forth in paragraph 1.E.8. 180 | 181 | 1.B. “Project Gutenberg” is a registered trademark. It may only be 182 | used on or associated in any way with an electronic work by people who 183 | agree to be bound by the terms of this agreement. There are a few 184 | things that you can do with most Project Gutenberg™ electronic works 185 | even without complying with the full terms of this agreement. See 186 | paragraph 1.C below. There are a lot of things you can do with Project 187 | Gutenberg™ electronic works if you follow the terms of this 188 | agreement and help preserve free future access to Project Gutenberg™ 189 | electronic works. See paragraph 1.E below. 190 | 191 | 1.C. The Project Gutenberg Literary Archive Foundation (“the 192 | Foundation” or PGLAF), owns a compilation copyright in the collection 193 | of Project Gutenberg™ electronic works. Nearly all the individual 194 | works in the collection are in the public domain in the United 195 | States. If an individual work is unprotected by copyright law in the 196 | United States and you are located in the United States, we do not 197 | claim a right to prevent you from copying, distributing, performing, 198 | displaying or creating derivative works based on the work as long as 199 | all references to Project Gutenberg are removed. Of course, we hope 200 | that you will support the Project Gutenberg™ mission of promoting 201 | free access to electronic works by freely sharing Project Gutenberg™ 202 | works in compliance with the terms of this agreement for keeping the 203 | Project Gutenberg™ name associated with the work. You can easily 204 | comply with the terms of this agreement by keeping this work in the 205 | same format with its attached full Project Gutenberg™ License when 206 | you share it without charge with others. 207 | 208 | 1.D. The copyright laws of the place where you are located also govern 209 | what you can do with this work. Copyright laws in most countries are 210 | in a constant state of change. If you are outside the United States, 211 | check the laws of your country in addition to the terms of this 212 | agreement before downloading, copying, displaying, performing, 213 | distributing or creating derivative works based on this work or any 214 | other Project Gutenberg™ work. The Foundation makes no 215 | representations concerning the copyright status of any work in any 216 | country other than the United States. 217 | 218 | 1.E. Unless you have removed all references to Project Gutenberg: 219 | 220 | 1.E.1. The following sentence, with active links to, or other 221 | immediate access to, the full Project Gutenberg™ License must appear 222 | prominently whenever any copy of a Project Gutenberg™ work (any work 223 | on which the phrase “Project Gutenberg” appears, or with which the 224 | phrase “Project Gutenberg” is associated) is accessed, displayed, 225 | performed, viewed, copied or distributed: 226 | 227 | This eBook is for the use of anyone anywhere in the United States and most 228 | other parts of the world at no cost and with almost no restrictions 229 | whatsoever. You may copy it, give it away or re-use it under the terms 230 | of the Project Gutenberg License included with this eBook or online 231 | at www.gutenberg.org. If you 232 | are not located in the United States, you will have to check the laws 233 | of the country where you are located before using this eBook. 234 | 235 | 1.E.2. If an individual Project Gutenberg™ electronic work is 236 | derived from texts not protected by U.S. copyright law (does not 237 | contain a notice indicating that it is posted with permission of the 238 | copyright holder), the work can be copied and distributed to anyone in 239 | the United States without paying any fees or charges. If you are 240 | redistributing or providing access to a work with the phrase “Project 241 | Gutenberg” associated with or appearing on the work, you must comply 242 | either with the requirements of paragraphs 1.E.1 through 1.E.7 or 243 | obtain permission for the use of the work and the Project Gutenberg™ 244 | trademark as set forth in paragraphs 1.E.8 or 1.E.9. 245 | 246 | 1.E.3. If an individual Project Gutenberg™ electronic work is posted 247 | with the permission of the copyright holder, your use and distribution 248 | must comply with both paragraphs 1.E.1 through 1.E.7 and any 249 | additional terms imposed by the copyright holder. Additional terms 250 | will be linked to the Project Gutenberg™ License for all works 251 | posted with the permission of the copyright holder found at the 252 | beginning of this work. 253 | 254 | 1.E.4. Do not unlink or detach or remove the full Project Gutenberg™ 255 | License terms from this work, or any files containing a part of this 256 | work or any other work associated with Project Gutenberg™. 257 | 258 | 1.E.5. Do not copy, display, perform, distribute or redistribute this 259 | electronic work, or any part of this electronic work, without 260 | prominently displaying the sentence set forth in paragraph 1.E.1 with 261 | active links or immediate access to the full terms of the Project 262 | Gutenberg™ License. 263 | 264 | 1.E.6. You may convert to and distribute this work in any binary, 265 | compressed, marked up, nonproprietary or proprietary form, including 266 | any word processing or hypertext form. However, if you provide access 267 | to or distribute copies of a Project Gutenberg™ work in a format 268 | other than “Plain Vanilla ASCII” or other format used in the official 269 | version posted on the official Project Gutenberg™ website 270 | (www.gutenberg.org), you must, at no additional cost, fee or expense 271 | to the user, provide a copy, a means of exporting a copy, or a means 272 | of obtaining a copy upon request, of the work in its original “Plain 273 | Vanilla ASCII” or other form. Any alternate format must include the 274 | full Project Gutenberg™ License as specified in paragraph 1.E.1. 275 | 276 | 1.E.7. Do not charge a fee for access to, viewing, displaying, 277 | performing, copying or distributing any Project Gutenberg™ works 278 | unless you comply with paragraph 1.E.8 or 1.E.9. 279 | 280 | 1.E.8. You may charge a reasonable fee for copies of or providing 281 | access to or distributing Project Gutenberg™ electronic works 282 | provided that: 283 | 284 | • You pay a royalty fee of 20% of the gross profits you derive from 285 | the use of Project Gutenberg™ works calculated using the method 286 | you already use to calculate your applicable taxes. The fee is owed 287 | to the owner of the Project Gutenberg™ trademark, but he has 288 | agreed to donate royalties under this paragraph to the Project 289 | Gutenberg Literary Archive Foundation. Royalty payments must be paid 290 | within 60 days following each date on which you prepare (or are 291 | legally required to prepare) your periodic tax returns. Royalty 292 | payments should be clearly marked as such and sent to the Project 293 | Gutenberg Literary Archive Foundation at the address specified in 294 | Section 4, “Information about donations to the Project Gutenberg 295 | Literary Archive Foundation.” 296 | 297 | • You provide a full refund of any money paid by a user who notifies 298 | you in writing (or by e-mail) within 30 days of receipt that s/he 299 | does not agree to the terms of the full Project Gutenberg™ 300 | License. You must require such a user to return or destroy all 301 | copies of the works possessed in a physical medium and discontinue 302 | all use of and all access to other copies of Project Gutenberg™ 303 | works. 304 | 305 | • You provide, in accordance with paragraph 1.F.3, a full refund of 306 | any money paid for a work or a replacement copy, if a defect in the 307 | electronic work is discovered and reported to you within 90 days of 308 | receipt of the work. 309 | 310 | • You comply with all other terms of this agreement for free 311 | distribution of Project Gutenberg™ works. 312 | 313 | 314 | 1.E.9. If you wish to charge a fee or distribute a Project 315 | Gutenberg™ electronic work or group of works on different terms than 316 | are set forth in this agreement, you must obtain permission in writing 317 | from the Project Gutenberg Literary Archive Foundation, the manager of 318 | the Project Gutenberg™ trademark. Contact the Foundation as set 319 | forth in Section 3 below. 320 | 321 | 1.F. 322 | 323 | 1.F.1. Project Gutenberg volunteers and employees expend considerable 324 | effort to identify, do copyright research on, transcribe and proofread 325 | works not protected by U.S. copyright law in creating the Project 326 | Gutenberg™ collection. Despite these efforts, Project Gutenberg™ 327 | electronic works, and the medium on which they may be stored, may 328 | contain “Defects,” such as, but not limited to, incomplete, inaccurate 329 | or corrupt data, transcription errors, a copyright or other 330 | intellectual property infringement, a defective or damaged disk or 331 | other medium, a computer virus, or computer codes that damage or 332 | cannot be read by your equipment. 333 | 334 | 1.F.2. LIMITED WARRANTY, DISCLAIMER OF DAMAGES - Except for the “Right 335 | of Replacement or Refund” described in paragraph 1.F.3, the Project 336 | Gutenberg Literary Archive Foundation, the owner of the Project 337 | Gutenberg™ trademark, and any other party distributing a Project 338 | Gutenberg™ electronic work under this agreement, disclaim all 339 | liability to you for damages, costs and expenses, including legal 340 | fees. YOU AGREE THAT YOU HAVE NO REMEDIES FOR NEGLIGENCE, STRICT 341 | LIABILITY, BREACH OF WARRANTY OR BREACH OF CONTRACT EXCEPT THOSE 342 | PROVIDED IN PARAGRAPH 1.F.3. YOU AGREE THAT THE FOUNDATION, THE 343 | TRADEMARK OWNER, AND ANY DISTRIBUTOR UNDER THIS AGREEMENT WILL NOT BE 344 | LIABLE TO YOU FOR ACTUAL, DIRECT, INDIRECT, CONSEQUENTIAL, PUNITIVE OR 345 | INCIDENTAL DAMAGES EVEN IF YOU GIVE NOTICE OF THE POSSIBILITY OF SUCH 346 | DAMAGE. 347 | 348 | 1.F.3. LIMITED RIGHT OF REPLACEMENT OR REFUND - If you discover a 349 | defect in this electronic work within 90 days of receiving it, you can 350 | receive a refund of the money (if any) you paid for it by sending a 351 | written explanation to the person you received the work from. If you 352 | received the work on a physical medium, you must return the medium 353 | with your written explanation. The person or entity that provided you 354 | with the defective work may elect to provide a replacement copy in 355 | lieu of a refund. If you received the work electronically, the person 356 | or entity providing it to you may choose to give you a second 357 | opportunity to receive the work electronically in lieu of a refund. If 358 | the second copy is also defective, you may demand a refund in writing 359 | without further opportunities to fix the problem. 360 | 361 | 1.F.4. Except for the limited right of replacement or refund set forth 362 | in paragraph 1.F.3, this work is provided to you ‘AS-IS’, WITH NO 363 | OTHER WARRANTIES OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT 364 | LIMITED TO WARRANTIES OF MERCHANTABILITY OR FITNESS FOR ANY PURPOSE. 365 | 366 | 1.F.5. Some states do not allow disclaimers of certain implied 367 | warranties or the exclusion or limitation of certain types of 368 | damages. If any disclaimer or limitation set forth in this agreement 369 | violates the law of the state applicable to this agreement, the 370 | agreement shall be interpreted to make the maximum disclaimer or 371 | limitation permitted by the applicable state law. The invalidity or 372 | unenforceability of any provision of this agreement shall not void the 373 | remaining provisions. 374 | 375 | 1.F.6. INDEMNITY - You agree to indemnify and hold the Foundation, the 376 | trademark owner, any agent or employee of the Foundation, anyone 377 | providing copies of Project Gutenberg™ electronic works in 378 | accordance with this agreement, and any volunteers associated with the 379 | production, promotion and distribution of Project Gutenberg™ 380 | electronic works, harmless from all liability, costs and expenses, 381 | including legal fees, that arise directly or indirectly from any of 382 | the following which you do or cause to occur: (a) distribution of this 383 | or any Project Gutenberg™ work, (b) alteration, modification, or 384 | additions or deletions to any Project Gutenberg™ work, and (c) any 385 | Defect you cause. 386 | 387 | Section 2. Information about the Mission of Project Gutenberg™ 388 | 389 | Project Gutenberg™ is synonymous with the free distribution of 390 | electronic works in formats readable by the widest variety of 391 | computers including obsolete, old, middle-aged and new computers. It 392 | exists because of the efforts of hundreds of volunteers and donations 393 | from people in all walks of life. 394 | 395 | Volunteers and financial support to provide volunteers with the 396 | assistance they need are critical to reaching Project Gutenberg™’s 397 | goals and ensuring that the Project Gutenberg™ collection will 398 | remain freely available for generations to come. In 2001, the Project 399 | Gutenberg Literary Archive Foundation was created to provide a secure 400 | and permanent future for Project Gutenberg™ and future 401 | generations. To learn more about the Project Gutenberg Literary 402 | Archive Foundation and how your efforts and donations can help, see 403 | Sections 3 and 4 and the Foundation information page at www.gutenberg.org. 404 | 405 | Section 3. Information about the Project Gutenberg Literary Archive Foundation 406 | 407 | The Project Gutenberg Literary Archive Foundation is a non-profit 408 | 501(c)(3) educational corporation organized under the laws of the 409 | state of Mississippi and granted tax exempt status by the Internal 410 | Revenue Service. The Foundation’s EIN or federal tax identification 411 | number is 64-6221541. Contributions to the Project Gutenberg Literary 412 | Archive Foundation are tax deductible to the full extent permitted by 413 | U.S. federal laws and your state’s laws. 414 | 415 | The Foundation’s business office is located at 809 North 1500 West, 416 | Salt Lake City, UT 84116, (801) 596-1887. Email contact links and up 417 | to date contact information can be found at the Foundation’s website 418 | and official page at www.gutenberg.org/contact 419 | 420 | Section 4. Information about Donations to the Project Gutenberg 421 | Literary Archive Foundation 422 | 423 | Project Gutenberg™ depends upon and cannot survive without widespread 424 | public support and donations to carry out its mission of 425 | increasing the number of public domain and licensed works that can be 426 | freely distributed in machine-readable form accessible by the widest 427 | array of equipment including outdated equipment. Many small donations 428 | ($1 to $5,000) are particularly important to maintaining tax exempt 429 | status with the IRS. 430 | 431 | The Foundation is committed to complying with the laws regulating 432 | charities and charitable donations in all 50 states of the United 433 | States. Compliance requirements are not uniform and it takes a 434 | considerable effort, much paperwork and many fees to meet and keep up 435 | with these requirements. We do not solicit donations in locations 436 | where we have not received written confirmation of compliance. To SEND 437 | DONATIONS or determine the status of compliance for any particular state 438 | visit www.gutenberg.org/donate. 439 | 440 | While we cannot and do not solicit contributions from states where we 441 | have not met the solicitation requirements, we know of no prohibition 442 | against accepting unsolicited donations from donors in such states who 443 | approach us with offers to donate. 444 | 445 | International donations are gratefully accepted, but we cannot make 446 | any statements concerning tax treatment of donations received from 447 | outside the United States. U.S. laws alone swamp our small staff. 448 | 449 | Please check the Project Gutenberg web pages for current donation 450 | methods and addresses. Donations are accepted in a number of other 451 | ways including checks, online payments and credit card donations. To 452 | donate, please visit: www.gutenberg.org/donate. 453 | 454 | Section 5. General Information About Project Gutenberg™ electronic works 455 | 456 | Professor Michael S. Hart was the originator of the Project 457 | Gutenberg™ concept of a library of electronic works that could be 458 | freely shared with anyone. For forty years, he produced and 459 | distributed Project Gutenberg™ eBooks with only a loose network of 460 | volunteer support. 461 | 462 | Project Gutenberg™ eBooks are often created from several printed 463 | editions, all of which are confirmed as not protected by copyright in 464 | the U.S. unless a copyright notice is included. Thus, we do not 465 | necessarily keep eBooks in compliance with any particular paper 466 | edition. 467 | 468 | Most people start at our website which has the main PG search 469 | facility: www.gutenberg.org. 470 | 471 | This website includes information about Project Gutenberg™, 472 | including how to make donations to the Project Gutenberg Literary 473 | Archive Foundation, how to help produce our new eBooks, and how to 474 | subscribe to our email newsletter to hear about new eBooks. 475 | 476 | 477 | --------------------------------------------------------------------------------