These are core message types in LangChain for structuring chat interactions:
- HumanMessage
- Represents user input
- Like: "What's the weather?"
- Used in chat prompts
HumanMessage(content="Hello AI")- AIMessage
- Represents AI assistant responses
- Previous outputs in conversation
AIMessage(content="Hello human, how can I help?")- SystemMessage
- Sets behavior/context for AI
- Like personality, rules, or context,role
- Usually used at start of chat
SystemMessage(content="You are a helpful assistant")Together they create chat history/memory:
messages = [
SystemMessage(content="You are helpful"),
HumanMessage(content="Hi"),
AIMessage(content="Hello!")
]These are the building blocks for chat-based interactions in LangChain.
Chat Model Documents: https://python.langchain.com/v0.2/docs/integrations/chat/
OpenAI Chat Model Documents: https://python.langchain.com/v0.2/docs/integrations/chat/openai/
Prompt Template Docs:
https://python.langchain.com/v0.2/docs/concepts/#prompt-templateshttps://python.langchain.com/v0.2/docs/concepts/#prompt-templates
Chains in LangChain combine multiple components to create a sequence of operations. Here are the key types:
- LLMChain
- Most basic chain
- Combines prompt template + LLM
chain = LLMChain(llm=llm, prompt=prompt)
chain.run(input)- SimpleSequentialChain
- Single input -> passes through steps -> single output
- Output of each step is input to next
chain = SimpleSequentialChain(chains=[chain1, chain2])- SequentialChain
- Multiple inputs/outputs
- More flexible than SimpleSequential
chain = SequentialChain(
chains=[chain1, chain2],
input_variables=[...],
output_variables=[...]
)- RouterChain
- Routes inputs to different chains based on conditions
- Like a switch statement for chains
- TransformChain
- Custom transformations on data
- No LLM calls, just data processing
Common Pattern:
prompt -> LLM -> Parser -> Next StepKey benefit: Chains let you compose complex workflows from simple components.
The pipe operator ( | ) in LangChain lets you chain components together in a clean, readable way, similar to Unix pipes:
# Instead of nested calls like:
chain = LLMChain(llm=llm, prompt=prompt)
chain.invoke({"input": "hello"})
# You can do:
prompt | llm
# Complex chains:
prompt | llm | output_parser | next_chain
# With RunniableMap (parallel operations):
docs | RunniableMap(prompt | llm) | output_parserKey uses:
- Chain components
- Transform data
- Create pipelines
- Parallel processing
Benefits:
- More readable code
- Less nesting
- Functional programming style
- Easy to modify pipeline steps
It's similar to pandas' pipe operator but for LLM operations.
Runnable Lambdas refer to the ability to create and execute small, self-contained functions or code snippets in LangChain. These Runnable Lambdas are similar to the concept of "lambda functions" in programming, which are anonymous, inline functions that can be defined and executed on the fly.
In the context of LangChain, Runnable Lambdas provide a way to easily incorporate custom logic or functionality into your LangChain workflows, without the need to create a separate, standalone Python module or function.
Some key characteristics and use cases of Runnable Lambdas in LangChain include:
-
Inline Functionality: Runnable Lambdas allow you to define small, focused pieces of logic directly within your LangChain code, rather than having to define them in a separate Python file.
-
Dynamic Customization: You can use Runnable Lambdas to dynamically customize the behavior of your LangChain components or chains, based on specific requirements or inputs.
-
Rapid Prototyping: Runnable Lambdas make it easier to experiment and iterate on your LangChain workflows, as you can quickly add, modify, or remove small pieces of functionality without having to restructure your entire codebase.
-
Modularity and Composability: Runnable Lambdas can be composed together with other LangChain components, allowing you to build more complex and modular workflows.
Runnable Lambdas provide a flexible and convenient way to incorporate custom logic into your LangChain workflows, helping you to build more powerful and tailored language models and applications.
In ChromaDB, the persistent_directory is the location where the database stores its data on disk permanently, rather than just in memory.
Key points:
- Without it, data exists only in memory (lost when program ends)
- With it, data persists between sessions
- Stores embeddings, metadata, and index structures
- Can be reloaded and reused later
Simple explanation:
# In-memory (temporary):
db = chromadb.Client()
# Persistent (saved to disk):
db = chromadb.PersistentClient(path="./my_chroma_db")Think of it like:
- In-memory = temporary notebook
- Persistent = actual filing cabinet
This is crucial for:
- Saving embeddings for reuse
- Not having to recreate DB every run
- Production applications
- Backing up data
The path you specify becomes your "database folder" where ChromaDB saves everything.
Agents in LangChain are autonomous systems that combine language models with tools to solve complex tasks. Think of an agent as an AI assistant that can use various tools and make decisions about how to accomplish a given objective. Unlike simple chains or models, agents can determine which actions to take and in what order, making them powerful for tasks that require multiple steps or different approaches.
Tools are the functions or capabilities that an agent can use. These can range from simple calculators to complex APIs. Here's a basic example of setting up tools:
from langchain.agents import Tool
from langchain.utilities import DuckDuckGoSearchRun
from langchain.tools import WikipediaAPIWrapper
# Initialize tool instances
search = DuckDuckGoSearchRun()
wikipedia = WikipediaAPIWrapper()
# Create tool definitions
tools = [
Tool(
name="Search",
func=search.run,
description="Useful for searching the internet for current information"
),
Tool(
name="Wikipedia",
func=wikipedia.run,
description="Useful for getting detailed information from Wikipedia"
)
]The language model serves as the brain of the agent, making decisions about which tools to use and how to interpret their outputs. Here's how to set it up:
from langchain.llms import OpenAI
# Initialize with temperature=0 for more consistent outputs
llm = OpenAI(temperature=0)Let's start with a simple agent that can search the internet and use Wikipedia:
from langchain.agents import initialize_agent, AgentType
# Initialize the agent
agent = initialize_agent(
tools,
llm,
agent=AgentType.ZERO_SHOT_REACT_DESCRIPTION,
verbose=True
)
# Use the agent
question = "What is quantum computing and who are its pioneers?"
response = agent.run(question)When you run this code, the agent will:
- Analyze the question
- Decide whether to search the internet or Wikipedia
- Process the information
- Provide a comprehensive answer
Let's create a more sophisticated agent that can handle complex tasks:
from langchain.memory import ConversationBufferMemory
from langchain.tools import PythonREPLTool
from langchain.prompts import PromptTemplate
# Initialize additional tools
python_repl = PythonREPLTool()
# Create a custom tool
class DataAnalysisTool(BaseTool):
name = "Data Analyzer"
description = "Analyzes numerical data and provides statistical insights"
def _run(self, data: str) -> str:
# Convert string input to numerical data and analyze
import pandas as pd
import json
try:
data = json.loads(data)
df = pd.DataFrame(data)
analysis = {
"mean": df.mean().to_dict(),
"median": df.median().to_dict(),
"std": df.std().to_dict()
}
return json.dumps(analysis, indent=2)
except Exception as e:
return f"Error analyzing data: {str(e)}"
# Create a custom prompt template
custom_prompt = """You are a sophisticated AI assistant with expertise in data analysis and research.
You have access to the following tools:
{tools}
When approaching a task:
1. Carefully analyze the request
2. Break down complex problems into steps
3. Use appropriate tools for each step
4. Synthesize information into a coherent response
Use this format:
Question: the input question
Thought: your reasoning about what needs to be done
Action: the tool to use
Action Input: what you pass to the tool
Observation: the tool's output
... (repeat as needed)
Thought: final reasoning
Final Answer: your complete response
Begin!
Question: {input}
Thought: """
# Set up memory
memory = ConversationBufferMemory(
memory_key="chat_history",
return_messages=True
)
# Create the advanced agent
advanced_agent = initialize_agent(
tools=[
Tool(
name="Python REPL",
func=python_repl.run,
description="Execute Python code for calculations"
),
DataAnalysisTool(),
*tools # Include our previous tools
],
llm=llm,
agent=AgentType.CHAT_CONVERSATIONAL_REACT_DESCRIPTION,
memory=memory,
agent_kwargs={'prefix': custom_prompt},
verbose=True
)This example shows how to use an agent for comprehensive research:
def research_topic(topic: str, depth: str = "basic"):
"""
Conducts research on a given topic with specified depth
"""
research_prompt = f"""
Conduct a {depth} research analysis on {topic}.
If basic: Focus on key definitions and main concepts.
If detailed: Include historical context, current developments, and future implications.
"""
return advanced_agent.run(research_prompt)
# Usage example
research_result = research_topic("artificial photosynthesis", "detailed")Here's an example of using an agent for data analysis:
def analyze_dataset(data: str):
"""
Analyzes a dataset and provides insights
"""
analysis_steps = [
"1. Examine the data structure",
"2. Perform statistical analysis",
"3. Identify patterns and trends",
"4. Generate visualizations if needed",
"5. Provide actionable insights"
]
prompt = f"""
Analyze the following dataset:
{data}
Follow these steps:
{'\n'.join(analysis_steps)}
Provide a comprehensive analysis with visualizations where appropriate.
"""
return advanced_agent.run(prompt)
# Example usage
sample_data = {
"sales": [100, 150, 200, 180, 220],
"costs": [80, 90, 120, 100, 130],
"customers": [20, 25, 30, 28, 35]
}
analysis_result = analyze_dataset(str(sample_data))-
Tool Description Clarity
- Write clear, specific tool descriptions
- Include example inputs/outputs in descriptions
- Specify limitations and requirements
-
Error Handling
def safe_agent_execution(agent, prompt, max_retries=3):
"""
Safely execute agent tasks with error handling
"""
for attempt in range(max_retries):
try:
return agent.run(prompt)
except Exception as e:
if attempt == max_retries - 1:
return f"Failed after {max_retries} attempts. Error: {str(e)}"
continue- Memory Management
- Use appropriate memory types for your use case
- Clear memory when switching contexts
- Consider implementing memory persistence for long-running applications
For specialized use cases, you can create custom agents:
class SpecializedAgent(Agent):
@property
def observation_prefix(self) -> str:
"""Prefix to use for observations."""
return "Observation: "
@property
def llm_prefix(self) -> str:
"""Prefix to use for the llm."""
return "Assistant: "
def _construct_scratchpad(self, intermediate_steps: List[Tuple[AgentAction, str]]) -> str:
"""Construct the scratchpad that lets the agent continue its thought process."""
thoughts = ""
for action, observation in intermediate_steps:
thoughts += action.log
thoughts += f"\n{self.observation_prefix}{observation}\n"
return thoughtsAgents in LangChain provide a powerful framework for creating autonomous AI systems that can use tools and make decisions. By combining different tools, custom prompts, and memory systems, you can create sophisticated agents capable of handling complex tasks across various domains.
Remember to:
- Choose appropriate tools for your use case
- Design clear and specific prompts
- Implement proper error handling
- Consider memory requirements
- Test thoroughly with various inputs