·43 min

Complete LangChain Ollama Integration: Building Graph-Based Multi-Persona Conversations with Local LLMs and CLI/GUI Interfaces

Comprehensive guide to integrating LangChain with Ollama for local LLM usage, featuring graph-based conversation orchestration, persona-driven responses, CLI and GUI interfaces, and iterative multi-round conversations.

DK

Daniel Kliewer

Author, Sovereign AI

LangChainOllamaLLMAIPythonGraph-based OrchestrationMulti-Persona SystemsInteractive CLIStreamlit GUI
Sovereign AI book cover

From the Book

This is from Sovereign AI: Building Local-First Intelligent Systems.

Get the Book — $88
Complete LangChain Ollama Integration: Building Graph-Based Multi-Persona Conversations with Local LLMs and CLI/GUI Interfaces

Image

High-Level Architecture for the LangChain Application using Ollama:

The application leverages a graph structure to manage and orchestrate interactions with a Language Model (LLM) using LangChain and Ollama. The key components and their interactions are:

  1. Graph Manager:

    • Purpose: Manages a directed graph where each node represents an LLM prompt and its corresponding response.
    • Implementation: Utilizes a graph data structure (e.g., from the networkx library) to model nodes (prompts and responses) and edges (data flow between prompts).
  2. Persona Manager:

    • Purpose: Handles different personas, each providing unique perspectives or areas of knowledge.
    • Implementation: Defines personas as configurations or templates that tailor prompts to reflect specific viewpoints.
  3. Context Manager:

    • Purpose: Manages the context passed between LLM calls, ensuring each prompt is aware of relevant previous interactions.
    • Implementation: Accumulates and updates context based on the graph's edges, feeding necessary information to subsequent prompts.
  4. LLM Interface (via LangChain and Ollama):

    • Purpose: Facilitates interactions with the LLM, generating responses to prompts with the given context and persona.
    • Implementation: Uses LangChain's LLMChain and PromptTemplate, with the Ollama LLM wrapper to construct and execute prompts.
  5. Markdown Logger:

    • Purpose: Records all prompts, responses, and analyses in a structured markdown file for tracking and reviewing.
    • Implementation: Appends entries to a markdown file, formatting the content for readability and organization.
  6. Analysis Module:

    • Purpose: Analyzes previous prompts and responses, potentially generating new insights or directing the flow of the conversation.
    • Implementation: Creates specialized nodes in the graph that process and reflect on prior interactions.

Implementing the Application with Ollama:

Below is a step-by-step guide to building the application using Ollama, including code snippets and explanations.

1. Set Up the Environment

Install the Necessary Python Libraries:

Ensure you have Python installed (preferably 3.7 or higher), and then install the required packages:

bash
1pip install langchain networkx markdown

Install Ollama:

Ollama is a tool for running language models locally. Follow the installation instructions for your operating system:

  • macOS:

    bash
    1brew install ollama/tap/ollama
  • Linux and Windows:

    Visit the Ollama GitHub repository for installation instructions specific to your platform.

Download a Model for Ollama:

Ollama can run various models. For this application, we'll use llama2 or any compatible model.

bash
1ollama pull llama2

2. Import Required Modules

python
1import os
2import networkx as nx
3from langchain import PromptTemplate, LLMChain
4from langchain.llms import Ollama

3. Define the Node Class

Create a class to encapsulate the properties of each node in the graph:

python
1class Node:
2 def __init__(self, node_id, prompt_text, persona):
3 self.id = node_id
4 self.prompt_text = prompt_text
5 self.response_text = None
6 self.context = ""
7 self.persona = persona

4. Initialize the Graph

Initialize a directed graph using networkx:

python
1G = nx.DiGraph()

5. Define Personas

Create a dictionary to hold different personas and their corresponding system prompts:

python
1personas = {
2 "Historian": "You are a knowledgeable historian specializing in the industrial revolution.",
3 "Scientist": "You are a scientist with expertise in technological advancements.",
4 "Philosopher": "You are a philosopher pondering the societal impacts.",
5 "Analyst": "You analyze information critically to provide insights.",
6 # Add additional personas as needed
7}

6. Implement the Graph Manager

Add nodes and edges to construct the conversation flow:

python
1# Create initial prompt nodes with different personas
2node1 = Node(1, prompt_text="Discuss the impacts of the industrial revolution.", persona="Historian")
3G.add_node(node1.id, data=node1)
4
5node2 = Node(2, prompt_text="Discuss the technological advancements during the industrial revolution.", persona="Scientist")
6G.add_node(node2.id, data=node2)
7
8# Add edges if node2 should consider node1's context
9G.add_edge(node1.id, node2.id)
10
11# Add an analysis node
12node3 = Node(3, prompt_text="", persona="Analyst")
13G.add_node(node3.id, data=node3)
14G.add_edge(node1.id, node3.id)
15G.add_edge(node2.id, node3.id)

7. Implement the Context Manager

Define a function to collect context from predecessor nodes:

python
1def collect_context(node_id):
2 predecessors = list(G.predecessors(node_id))
3 context = ""
4 for pred_id in predecessors:
5 pred_node = G.nodes[pred_id]['data']
6 if pred_node.response_text:
7 context += f"From {pred_node.persona}:\n{pred_node.response_text}\n\n"
8 return context

8. Implement the LLM Interface with Ollama

Create a function to generate responses using LangChain and Ollama:

python
1def generate_response(node):
2 system_prompt = personas[node.persona]
3 # Build the complete prompt
4 prompt_template = PromptTemplate(
5 input_variables=["system_prompt", "context", "prompt"],
6 template="{system_prompt}\n\n{context}\n\n{prompt}"
7 )
8 # Instantiate the Ollama LLM
9 llm = Ollama(
10 base_url="http://localhost:11434", # Default Ollama server URL
11 model="llama2", # or specify the model you have downloaded
12 )
13 chain = LLMChain(llm=llm, prompt=prompt_template)
14 response = chain.run(
15 system_prompt=system_prompt,
16 context=node.context,
17 prompt=node.prompt_text
18 )
19 return response

Note: Ensure that the Ollama server is running before executing the script:

bash
1ollama serve

9. Implement the Markdown Logger

Define a function to log interactions to a markdown file:

python
1def update_markdown(node):
2 with open("conversation.md", "a", encoding="utf-8") as f:
3 f.write(f"## Node {node.id}: {node.persona}\n\n")
4 f.write(f"**Prompt:**\n\n{node.prompt_text}\n\n")
5 f.write(f"**Response:**\n\n{node.response_text}\n\n---\n\n")

10. Implement the Analysis Module

Create a function for nodes that perform analysis:

python
1def analyze_responses(node):
2 # Collect responses from predecessor nodes
3 predecessors = list(G.predecessors(node.id))
4 analysis_input = ""
5 for pred_id in predecessors:
6 pred_node = G.nodes[pred_id]['data']
7 analysis_input += f"{pred_node.persona}'s response:\n{pred_node.response_text}\n\n"
8
9 node.prompt_text = f"Provide an analysis comparing the following perspectives:\n\n{analysis_input}"
10 node.context = "" # Analysis can be based solely on the provided responses
11 node.response_text = generate_response(node)
12 update_markdown(node)

11. Process the Nodes

Iterate over the graph to process each node:

python
1for node_id in nx.topological_sort(G):
2 node = G.nodes[node_id]['data']
3 if node.persona != "Analyst":
4 node.context = collect_context(node_id)
5 node.response_text = generate_response(node)
6 update_markdown(node)
7 else:
8 analyze_responses(node)

Detailed Explanation:

  • Graph Processing Order:

    • Use nx.topological_sort(G) to process nodes in an order that respects dependencies, ensuring predecessor nodes are processed before successors.
  • Context Collection:

    • For each node, the collect_context function gathers responses from predecessor nodes, forming the context that will be included in the prompt.
  • Persona-Specific Prompts:

    • The system_prompt variable injects persona characteristics into the prompt via LangChain's templating, guiding the LLM to respond from that perspective.
  • Response Generation with Ollama:

    • The generate_response function constructs the prompt using the PromptTemplate and retrieves the LLM's response using LangChain's LLMChain with the Ollama LLM.
  • Logging Interactions:

    • The update_markdown function appends each interaction to the conversation.md file, using markdown formatting for clarity and organization.
  • Analysis Nodes:

    • Nodes with the persona "Analyst" execute the analyze_responses function, which compiles predecessor responses and generates an analytical output.

Example Output in Markdown:

The conversation.md file will contain formatted entries like:

text
1## Node 1: Historian
2
3**Prompt:**
4
5Discuss the impacts of the industrial revolution.
6
7**Response:**
8
9[Historian's response...]
10
11---
12
13## Node 2: Scientist
14
15**Prompt:**
16
17Discuss the technological advancements during the industrial revolution.
18
19**Response:**
20
21[Scientist's response...]
22
23---
24
25## Node 3: Analyst
26
27**Prompt:**
28
29Provide an analysis comparing the following perspectives:
30...
31
32**Response:**
33
34[Analyst's comparative analysis...]
35
36---

12. Expanding the Application

To enhance the application further:

  • Dynamic Node Creation:

    • Based on responses, new nodes can be added to explore emerging topics.
  • Advanced Personas:

    • Enrich personas with more detailed backgrounds or expertise.
  • User Interaction:

    • Introduce mechanisms for user input to guide the conversation.
  • Visualization:

    • Generate visual representations of the graph to illustrate the conversation flow.

13. Considerations and Best Practices

  • Ollama Model Selection:

    • Ensure that the model used with Ollama is appropriate for the application's needs. Some models may require specific handling or have different capabilities.
  • Context Window Limitations:

    • Be mindful of the token limit for the LLM's context window; if necessary, truncate or summarize context.
  • Error Handling:

    • Implement robust error handling around LLM calls and file operations to handle exceptions gracefully.
  • Concurrency:

    • For large graphs, consider asynchronous processing where dependencies allow.
  • Configuration Management:

    • Use configuration files or environment variables to manage settings like the Ollama server URL and model name.
  • Privacy and Security:

    • Ensure sensitive information is not exposed, especially when logging prompts and responses.

Summary:

By integrating these components with Ollama, the application can:

  • Orchestrate LLM Calls via a Graph:

    • Manage complex conversational flows where prompts and responses are interconnected in non-linear ways.
  • Update Context Dynamically:

    • Pass information between nodes, ensuring that each prompt is informed by relevant preceding interactions.
  • Utilize Multiple Personas:

    • Simulate different perspectives by tailoring prompts to various personas, enriching the conversation.
  • Track Interaction History:

    • Maintain a comprehensive record of the conversation, including analyses, in a markdown file for transparency and review.
  • Analyze and Reflect:

    • Incorporate analysis steps that synthesize previous responses, potentially guiding future prompts.

Implementation Steps Recap:

  1. Set Up Environment and Libraries:

    • Install langchain, networkx, markdown, and set up Ollama.
  2. Define Data Structures (Nodes and Edges):

    • Create the Node class to represent each point in the conversation.
  3. Initialize the Directed Graph:

    • Use networkx to manage the flow of conversations.
  4. Define Personas and Their Prompts:

    • Establish different perspectives through personas.
  5. Build the Graph Manager Functions:

    • Construct the conversation flow by adding nodes and edges.
  6. Implement Context Collection Mechanism:

    • Gather context from predecessor nodes for each prompt.
  7. Create the LLM Interface with Ollama:

    • Use LangChain's Ollama integration to interface with the local LLM.
  8. Set Up the Markdown Logger:

    • Record the prompts and responses in a markdown file.
  9. Develop the Analysis Module:

    • Analyze previous responses to generate insights.
  10. Process Nodes in Topological Order:

    • Execute the conversation flow respecting dependencies.

By following this architecture and implementation plan with Ollama, you can create a robust application that leverages the power of LangChain and local LLMs to generate rich, context-aware conversations from multiple perspectives, all while maintaining a clear and organized record of the interaction history.


Next Steps:

  • Testing:

    • Run the application with sample prompts and personas to verify functionality.
  • Refinement:

    • Adjust personas, context management, and logging based on observed outcomes.
  • Scaling:

    • Expand the graph to include more nodes and complex interactions, testing the application's scalability.
  • Documentation:

    • Document the code thoroughly, explaining how each component works for future maintenance and updates.
  • Model Optimization:

    • Experiment with different models available in Ollama to find the best fit for your application.

By iteratively refining the application, you can tailor it to specific use cases, such as educational tools, collaborative brainstorming platforms, or complex simulation environments, all powered locally using Ollama.


Additional Resources:

  • Ollama Documentation:

  • LangChain Documentation:

  • Community Support:

    • Engage with the communities around LangChain and Ollama for support, updates, and shared experiences.

Troubleshooting Tips:

  • Ollama Server Not Running:

    • If you encounter connection errors, ensure the Ollama server is running with ollama serve.
  • Model Not Found:

    • Verify that the model specified in the Ollama LLM instantiation is correctly downloaded and available.
  • Performance Issues:

    • Running large models locally may require significant computational resources. Ensure your hardware meets the requirements.
  • Compatibility:

    • Ensure all libraries are up-to-date to avoid compatibility issues. Use virtual environments to manage dependencies.

layout: post title: LangChain Ollama date: 2024-12-19 07:42:44 -0500 categories: ["AI Integration & Development", "Technical Tutorials"] tags: ["LangChain", "Ollama", "AI", "Python", "LLM", "Graph", "Interactive"]

1. Create the Project Directory and File Structure

Open your terminal and run the following commands to set up the project directory and files:

bash
1# Create the project directory and navigate into it
2mkdir langchain_graph_app
3cd langchain_graph_app
4
5# Create the main Python script
6touch main.py
7
8# Create a requirements.txt file to list dependencies
9touch requirements.txt

2. Write the Code for the Files

I'll provide the code for each file. Please copy and paste the code into the respective files.


File: requirements.txt

First, specify the dependencies in a requirements.txt file. This will allow you to install all the necessary Python packages easily.

Content of requirements.txt:

text
1langchain
2networkx
3markdown
4langchain_community

File: main.py

This is the main script containing the code for the application.

Content of main.py:

python
1# main.py
2
3import os
4import networkx as nx
5from langchain import PromptTemplate, LLMChain
6from langchain.llms import Ollama
7
8# Define the Node class
9class Node:
10 def __init__(self, node_id, prompt_text, persona):
11 self.id = node_id
12 self.prompt_text = prompt_text
13 self.response_text = None
14 self.context = ""
15 self.persona = persona
16
17# Initialize the graph
18G = nx.DiGraph()
19
20# Define personas
21personas = {
22 "Historian": "You are a knowledgeable historian specializing in the industrial revolution.",
23 "Scientist": "You are a scientist with expertise in technological advancements.",
24 "Philosopher": "You are a philosopher pondering societal impacts.",
25 "Analyst": "You analyze information critically to provide insights.",
26 # Add additional personas as needed
27}
28
29# Function to collect context from predecessor nodes
30def collect_context(node_id):
31 predecessors = list(G.predecessors(node_id))
32 context = ""
33 for pred_id in predecessors:
34 pred_node = G.nodes[pred_id]['data']
35 if pred_node.response_text:
36 context += f"From {pred_node.persona}:\n{pred_node.response_text}\n\n"
37 return context
38
39# Function to generate responses using LangChain and Ollama
40def generate_response(node):
41 system_prompt = personas[node.persona]
42 # Build the complete prompt
43 prompt_template = PromptTemplate(
44 input_variables=["system_prompt", "context", "prompt"],
45 template="{system_prompt}\n\n{context}\n\n{prompt}"
46 )
47 # Instantiate the Ollama LLM
48 llm = Ollama(
49 base_url="http://localhost:11434", # Default Ollama server URL
50 model="llama2", # Replace with the model you have downloaded
51 )
52 chain = LLMChain(llm=llm, prompt=prompt_template)
53 response = chain.run(
54 system_prompt=system_prompt,
55 context=node.context,
56 prompt=node.prompt_text
57 )
58 return response
59
60# Function to log interactions to a markdown file
61def update_markdown(node):
62 with open("conversation.md", "a", encoding="utf-8") as f:
63 f.write(f"## Node {node.id}: {node.persona}\n\n")
64 f.write(f"**Prompt:**\n\n{node.prompt_text}\n\n")
65 f.write(f"**Response:**\n\n{node.response_text}\n\n---\n\n")
66
67# Function for nodes that perform analysis
68def analyze_responses(node):
69 # Collect responses from predecessor nodes
70 predecessors = list(G.predecessors(node.id))
71 analysis_input = ""
72 for pred_id in predecessors:
73 pred_node = G.nodes[pred_id]['data']
74 analysis_input += f"{pred_node.persona}'s response:\n{pred_node.response_text}\n\n"
75
76 node.prompt_text = f"Provide an analysis comparing the following perspectives:\n\n{analysis_input}"
77 node.context = "" # Analysis is based solely on the provided responses
78 node.response_text = generate_response(node)
79 update_markdown(node)
80
81# Build the graph
82
83# Create initial prompt nodes with different personas
84node1 = Node(1, prompt_text="Discuss the impacts of the industrial revolution.", persona="Historian")
85G.add_node(node1.id, data=node1)
86
87node2 = Node(2, prompt_text="Discuss the technological advancements during the industrial revolution.", persona="Scientist")
88G.add_node(node2.id, data=node2)
89
90# Add edges if node2 should consider node1's context
91G.add_edge(node1.id, node2.id) # node2 considers node1's context
92
93# Add an analysis node
94node3 = Node(3, prompt_text="", persona="Analyst")
95G.add_node(node3.id, data=node3)
96G.add_edge(node1.id, node3.id)
97G.add_edge(node2.id, node3.id)
98
99# Process the nodes
100for node_id in nx.topological_sort(G):
101 node = G.nodes[node_id]['data']
102 if node.persona != "Analyst":
103 node.context = collect_context(node_id)
104 node.response_text = generate_response(node)
105 update_markdown(node)
106 else:
107 analyze_responses(node)
108
109print("Conversation has been generated and logged to conversation.md")

3. Install Dependencies

Make sure you have all the required Python packages installed.

In your terminal, from within the langchain_graph_app directory, run:

bash
1pip install -r requirements.txt

4. Install and Set Up Ollama

Install Ollama

Follow the installation instructions specific to your operating system.

  • macOS (via Homebrew):

    bash
    1brew install ollama/tap/ollama
  • Other Platforms:

    Visit the Ollama GitHub repository for installation instructions.

Download a Model for Ollama

Ollama runs models locally. You need to download a model compatible with your application.

For example, to download the llama2 model:

bash
1ollama pull llama2

Note: Replace llama2 with the name of the model you wish to use if different.


5. Run the Ollama Server

Before running the application, ensure that the Ollama server is running.

In a separate terminal window, run:

bash
1ollama serve

This will start the Ollama server on the default port 11434.


6. Run the Application

Now you can run the application:

bash
1python main.py

This will execute the script, generate the conversation, and create (or update) the conversation.md file with the prompts and responses.


7. View the Output

Open the conversation.md file to see the generated conversation:

bash
1cat conversation.md

Or open it in a text editor that supports Markdown to view it with proper formatting.


Explanation of the Files and Code

main.py Overview

  • Imports:

    • networkx: For creating and managing the directed graph.
    • langchain: For interacting with the language model.
    • Ollama: LangChain's wrapper for the Ollama LLM.
  • Node Class:

    • Represents each node in the graph.
    • Stores the node ID, prompt text, persona, response text, and context.
  • Graph Initialization:

    • A directed graph G is created using networkx.DiGraph().
  • Personas:

    • A dictionary mapping persona names to their descriptions (system prompts).
    • Personas influence how the LLM generates responses.
  • Functions:

    • collect_context(node_id): Gathers responses from predecessor nodes to form the context.
    • generate_response(node): Uses the Ollama LLM via LangChain to generate a response based on the persona, context, and prompt.
    • update_markdown(node): Appends the prompt and response to conversation.md in a structured format.
    • analyze_responses(node): Handles nodes designated for analysis, compiling predecessor responses and generating an analytical output.
  • Graph Construction:

    • Nodes are added to the graph with unique IDs and associated data.
    • Edges define the flow of information (i.e., which nodes' contexts are considered in generating responses).
  • Processing Nodes:

    • Nodes are processed in topological order to respect dependencies.
    • Responses are generated and logged for each node.

Understanding the Flow

  1. Graph Creation:

    • Three nodes are created: two with specific personas (Historian and Scientist) and one Analyst.
    • Edges are added to define how context flows between nodes.
  2. Processing:

    • Nodes are processed so that predecessors are handled before successors.
    • For non-analyst nodes, the context is collected from predecessors, responses are generated, and the interaction is logged.
    • For the analyst node, it compiles the responses from its predecessors and generates an analysis.
  3. Logging:

    • All prompts and responses are appended to conversation.md with markdown formatting for readability.

Sample Output in conversation.md

The conversation.md file will contain entries like:

text
1## Node 1: Historian
2
3**Prompt:**
4
5Discuss the impacts of the industrial revolution.
6
7**Response:**
8
9[Historian's response...]
10
11---
12
13## Node 2: Scientist
14
15**Prompt:**
16
17Discuss the technological advancements during the industrial revolution.
18
19**Response:**
20
21[Scientist's response...]
22
23---
24
25## Node 3: Analyst
26
27**Prompt:**
28
29Provide an analysis comparing the following perspectives:
30
31Historian's response:
32[Historian's response...]
33
34Scientist's response:
35[Scientist's response...]
36
37**Response:**
38
39[Analyst's comparative analysis...]
40
41---

Additional Notes

  • Model Configuration:

    • Ensure the model parameter in the Ollama LLM instantiation matches the model you have downloaded.
    • Example: If you downloaded llama2, set model="llama2".
  • Expanding the Graph:

    • You can add more nodes and edges to create more complex conversations.
    • Be mindful of the context window limitations of the LLM.
  • Error Handling:

    • The script does not include extensive error handling.
    • Consider adding try-except blocks around network calls, file operations, and LLM interactions.
  • Environment:

    • It's recommended to use a virtual environment to manage dependencies.

      bash
      1python -m venv venv
      2source venv/bin/activate # On Windows use venv\Scripts\activate
  • Dependencies:

    • Ensure all dependencies are properly installed and compatible with your Python interpreter.

Next Steps

  • Customize Personas and Prompts:

    • Modify the personas dictionary to add or adjust personas.
    • Change the prompt_text for each node to explore different topics.
  • Enhance Functionality:

    • Add user input to create dynamic prompts or personas.
    • Implement a GUI or web interface for interactive usage.
  • Visualization:

    • Use graph visualization libraries, like matplotlib or pygraphviz, to visualize the conversation flow.
  • Documentation and Testing:

    • Document any changes or additions you make to the code.
    • Write unit tests for your functions to ensure reliability.

Recap of Commands

Project Setup

bash
1# Create project directory and navigate into it
2mkdir langchain_graph_app
3cd langchain_graph_app
4
5# Create the main script and requirements file
6touch main.py requirements.txt
7
8# Open the files in a text editor to add the provided code

Installing Dependencies

bash
1# Install Python packages
2pip install -r requirements.txt

Installing and Running Ollama

bash
1# Install Ollama (if not already installed)
2# For macOS:
3brew install ollama/tap/ollama
4
5# Download the desired model
6ollama pull llama2
7
8# Start the Ollama server
9ollama serve

Running the Application

bash
1# Run the main script
2python main.py
3
4# View the generated conversation
5cat conversation.md

Troubleshooting Tips

  • Ollama Server Issues:

    • Ensure the Ollama server is running (ollama serve) before running the script.
    • Check if the server is accessible at http://localhost:11434.
  • Model Not Found:

    • Verify that the model name in the script matches the model you downloaded.
    • Run ollama list to see the available models.
  • Dependency Errors:

    • Double-check that all dependencies are installed.
    • Use pip list to view installed packages.
  • Python Version:

    • Ensure you're using a compatible Python version (3.7 or higher recommended).
  • Permission Issues:

    • If you encounter permission errors when accessing files, ensure you have the necessary rights.

Additional Resources


Let's extend the application to include detailed personas with stylistic attributes stored in JSON files. We'll modify the program to select a persona from a JSON file and adjust the LLM prompts to reflect the selected persona's writing style.


Overview of the Enhancement

  • Personas with Stylistic Attributes:

    • Store detailed personas in JSON format.
    • Each persona includes various stylistic and psychological attributes.
  • Integration with the Application:

    • Load personas from JSON files.
    • Modify the prompt generation to include these attributes.
    • Adjust the LLM output to match the selected persona's style.
  • Implementation Steps:

    1. Update the File Structure:
      • Create a personas directory to store JSON files.
    2. Modify the Code:
      • Load personas from JSON files.
      • Update the generate_response function to incorporate stylistic attributes.
    3. Provide Instructions:
      • Explain how to add new personas.
      • Show how to use the updated application.

1. Update the File Structure

In your project directory (langchain_graph_app), create a new directory called personas to store persona JSON files.

bash
1mkdir personas

2. Create Persona JSON Files

Each persona will be stored as an individual JSON file within the personas directory. Let's create a sample persona.

Example Persona: "Ernest Hemingway"

Create a file named ernest_hemingway.json in the personas directory.

Content of personas/ernest_hemingway.json:

json
1{
2 "name": "Ernest Hemingway",
3 "vocabulary_complexity": 3,
4 "sentence_structure": "simple",
5 "paragraph_organization": "structured",
6 "idiom_usage": 2,
7 "metaphor_frequency": 4,
8 "simile_frequency": 5,
9 "tone": "formal",
10 "punctuation_style": "minimal",
11 "contraction_usage": 5,
12 "pronoun_preference": "first-person",
13 "passive_voice_frequency": 2,
14 "rhetorical_question_usage": 3,
15 "list_usage_tendency": 2,
16 "personal_anecdote_inclusion": 7,
17 "pop_culture_reference_frequency": 1,
18 "technical_jargon_usage": 2,
19 "parenthetical_aside_frequency": 1,
20 "humor_sarcasm_usage": 3,
21 "emotional_expressiveness": 6,
22 "emphatic_device_usage": 4,
23 "quotation_frequency": 3,
24 "analogy_usage": 5,
25 "sensory_detail_inclusion": 8,
26 "onomatopoeia_usage": 2,
27 "alliteration_frequency": 2,
28 "word_length_preference": "short",
29 "foreign_phrase_usage": 3,
30 "rhetorical_device_usage": 4,
31 "statistical_data_usage": 1,
32 "personal_opinion_inclusion": 7,
33 "transition_usage": 5,
34 "reader_question_frequency": 2,
35 "imperative_sentence_usage": 3,
36 "dialogue_inclusion": 8,
37 "regional_dialect_usage": 4,
38 "hedging_language_frequency": 2,
39 "language_abstraction": "concrete",
40 "personal_belief_inclusion": 6,
41 "repetition_usage": 5,
42 "subordinate_clause_frequency": 3,
43 "verb_type_preference": "active",
44 "sensory_imagery_usage": 8,
45 "symbolism_usage": 6,
46 "digression_frequency": 2,
47 "formality_level": 5,
48 "reflection_inclusion": 7,
49 "irony_usage": 3,
50 "neologism_frequency": 1,
51 "ellipsis_usage": 2,
52 "cultural_reference_inclusion": 3,
53 "stream_of_consciousness_usage": 2,
54 "openness_to_experience": 8,
55 "conscientiousness": 6,
56 "extraversion": 5,
57 "agreeableness": 7,
58 "emotional_stability": 6,
59 "dominant_motivations": "adventure",
60 "core_values": "courage",
61 "decision_making_style": "intuitive",
62 "empathy_level": 7,
63 "self_confidence": 8,
64 "risk_taking_tendency": 9,
65 "idealism_vs_realism": "realistic",
66 "conflict_resolution_style": "assertive",
67 "relationship_orientation": "independent",
68 "emotional_response_tendency": "calm",
69 "creativity_level": 8,
70 "age": "Late 50s",
71 "gender": "Male",
72 "education_level": "High School",
73 "professional_background": "Writer and Journalist",
74 "cultural_background": "American",
75 "primary_language": "English",
76 "language_fluency": "Native"
77}

3. Modify main.py to Load and Use Personas

3.1. Import JSON and OS Modules

Add imports at the top of main.py:

python
1import json
2import os

3.2. Update the Node Class

Modify the Node class to include a persona_attributes field:

python
1class Node:
2 def __init__(self, node_id, prompt_text, persona_name):
3 self.id = node_id
4 self.prompt_text = prompt_text
5 self.response_text = None
6 self.context = ""
7 self.persona_name = persona_name
8 self.persona_attributes = {}

3.3. Load Personas from JSON Files

Create a function to load personas from the personas directory:

python
1def load_personas(persona_dir):
2 personas = {}
3 for filename in os.listdir(persona_dir):
4 if filename.endswith('.json'):
5 filepath = os.path.join(persona_dir, filename)
6 with open(filepath, 'r', encoding='utf-8') as f:
7 persona_data = json.load(f)
8 name = persona_data.get('name')
9 if name:
10 personas[name] = persona_data
11 return personas

Load the personas after defining the function:

python
1# Load personas
2persona_dir = 'personas' # Directory where persona JSON files are stored
3personas = load_personas(persona_dir)

3.4. Update the generate_response Function

Modify the generate_response function to include persona attributes in the system prompt:

python
1def generate_response(node):
2 persona = personas.get(node.persona_name)
3 if not persona:
4 raise ValueError(f"Persona '{node.persona_name}' not found.")
5
6 node.persona_attributes = persona
7
8 # Build the system prompt based on persona attributes
9 system_prompt = build_system_prompt(persona)
10
11 # Build the complete prompt
12 prompt_template = PromptTemplate(
13 input_variables=["system_prompt", "context", "prompt"],
14 template="{system_prompt}\n\n{context}\n\n{prompt}"
15 )
16 # Instantiate the Ollama LLM
17 llm = Ollama(
18 base_url="http://localhost:11434", # Default Ollama server URL
19 model="llama2", # Replace with the model you have downloaded
20 )
21 chain = LLMChain(llm=llm, prompt=prompt_template)
22 response = chain.run(
23 system_prompt=system_prompt,
24 context=node.context,
25 prompt=node.prompt_text
26 )
27 return response

3.5. Create the build_system_prompt Function

This function constructs the system prompt using the persona's attributes.

python
1def build_system_prompt(persona):
2 # Construct descriptive sentences based on persona attributes
3 # We'll focus on key attributes for brevity
4 name = persona.get('name', 'The speaker')
5 tone = persona.get('tone', 'neutral')
6 sentence_structure = persona.get('sentence_structure', 'varied')
7 vocabulary_complexity = persona.get('vocabulary_complexity', 5)
8 formality_level = persona.get('formality_level', 5)
9 pronoun_preference = persona.get('pronoun_preference', 'third-person')
10 language_abstraction = persona.get('language_abstraction', 'mixed')
11
12 # Create a description
13 description = (
14 f"You are {name}, writing in a {tone} tone using {sentence_structure} sentences. "
15 f"Your vocabulary complexity is {vocabulary_complexity}/10, and your formality level is {formality_level}/10. "
16 f"You prefer {pronoun_preference} narration and your language abstraction is {language_abstraction}."
17 )
18
19 # Include any other attributes as needed
20 # ...
21
22 return description

3.6. Update the Graph Definition

Update the creation of nodes to use the new persona names.

For example, replace the old personas with the new ones:

python
1# Create initial prompt nodes with different personas
2node1 = Node(1, prompt_text="Discuss the impacts of the industrial revolution.", persona_name="Ernest Hemingway")
3G.add_node(node1.id, data=node1)
4
5# You can create more nodes with different personas
6node2 = Node(2, prompt_text="Explain quantum mechanics in simple terms.", persona_name="Albert Einstein")
7G.add_node(node2.id, data=node2)
8
9# Add edges between nodes if needed
10# G.add_edge(node1.id, node2.id)
11
12# Add an analysis node (you can define a generic analyst persona)
13node3 = Node(3, prompt_text="", persona_name="Analyst")
14G.add_node(node3.id, data=node3)
15G.add_edge(node1.id, node3.id)
16G.add_edge(node2.id, node3.id)

3.7. Create an Analyst Persona

Create an analyst persona JSON file analyst.json or handle the analyst within the code.

Option 1: Create personas/analyst.json

json
1{
2 "name": "Analyst",
3 "tone": "formal",
4 "sentence_structure": "complex",
5 "vocabulary_complexity": 7,
6 "formality_level": 8,
7 "pronoun_preference": "third-person",
8 "language_abstraction": "mixed"
9}

Option 2: If the analyst persona is generic, handle the absence of specific attributes in build_system_prompt by providing defaults.


4. Run the Updated Application

4.1. Ensure All Persona Files are Created

Make sure you've created the necessary persona JSON files in the personas directory.

  • ernest_hemingway.json (as above)
  • analyst.json (if needed)
  • Additional personas (e.g., albert_einstein.json if you use that persona)

4.2. Install Any New Dependencies

If you haven't already imported the json module, ensure it's included in your code. No additional installations are required as json and os are part of the Python standard library.

4.3. Run the Application

bash
1python main.py

This will generate the conversation with responses tailored to the selected personas.


5. View the Output

Check the conversation.md file to see the prompts and responses with the new personas.


6. Additional Personas

To add more personas, create new JSON files in the personas directory with the required attributes.

Example: "Albert Einstein" Persona

Create personas/albert_einstein.json:

json
1{
2 "name": "Albert Einstein",
3 "vocabulary_complexity": 8,
4 "sentence_structure": "complex",
5 "paragraph_organization": "structured",
6 "idiom_usage": 3,
7 "metaphor_frequency": 6,
8 "simile_frequency": 5,
9 "tone": "academic",
10 "punctuation_style": "conventional",
11 "contraction_usage": 2,
12 "pronoun_preference": "first-person",
13 "passive_voice_frequency": 6,
14 "rhetorical_question_usage": 4,
15 "list_usage_tendency": 3,
16 "personal_anecdote_inclusion": 5,
17 "technical_jargon_usage": 9,
18 "parenthetical_aside_frequency": 2,
19 "humor_sarcasm_usage": 4,
20 "emotional_expressiveness": 5,
21 "emphatic_device_usage": 6,
22 "quotation_frequency": 3,
23 "analogy_usage": 7,
24 "sensory_detail_inclusion": 4,
25 "onomatopoeia_usage": 1,
26 "alliteration_frequency": 2,
27 "word_length_preference": "long",
28 "foreign_phrase_usage": 5,
29 "rhetorical_device_usage": 7,
30 "statistical_data_usage": 8,
31 "personal_opinion_inclusion": 6,
32 "transition_usage": 7,
33 "reader_question_frequency": 2,
34 "imperative_sentence_usage": 2,
35 "dialogue_inclusion": 3,
36 "regional_dialect_usage": 1,
37 "hedging_language_frequency": 5,
38 "language_abstraction": "abstract",
39 "personal_belief_inclusion": 6,
40 "repetition_usage": 3,
41 "subordinate_clause_frequency": 7,
42 "verb_type_preference": "active",
43 "sensory_imagery_usage": 3,
44 "symbolism_usage": 5,
45 "digression_frequency": 2,
46 "formality_level": 8,
47 "reflection_inclusion": 7,
48 "irony_usage": 2,
49 "neologism_frequency": 3,
50 "ellipsis_usage": 2,
51 "cultural_reference_inclusion": 3,
52 "stream_of_consciousness_usage": 2,
53 "openness_to_experience": 9,
54 "conscientiousness": 7,
55 "extraversion": 4,
56 "agreeableness": 6,
57 "emotional_stability": 7,
58 "dominant_motivations": "knowledge",
59 "core_values": "integrity",
60 "decision_making_style": "analytical",
61 "empathy_level": 7,
62 "self_confidence": 8,
63 "risk_taking_tendency": 6,
64 "idealism_vs_realism": "idealistic",
65 "conflict_resolution_style": "collaborative",
66 "relationship_orientation": "independent",
67 "emotional_response_tendency": "calm",
68 "creativity_level": 9,
69 "age": "Mid 40s",
70 "gender": "Male",
71 "education_level": "Doctorate",
72 "professional_background": "Physicist",
73 "cultural_background": "German-American",
74 "primary_language": "German",
75 "language_fluency": "Fluent in English"
76}

7. Testing and Refinement

7.1. Test the Application

Run the application again and observe the differences in the responses based on the personas.

7.2. Refine the build_system_prompt Function

You can enhance the build_system_prompt function to incorporate more attributes and create more nuanced system prompts.

For example:

python
1def build_system_prompt(persona):
2 attributes = []
3
4 # Add tone
5 tone = persona.get('tone')
6 if tone:
7 attributes.append(f"Your tone is {tone}.")
8
9 # Add vocabulary complexity
10 vocab_complexity = persona.get('vocabulary_complexity')
11 if vocab_complexity:
12 attributes.append(f"Your vocabulary complexity is rated {vocab_complexity}/10.")
13
14 # Add sentence structure
15 sentence_structure = persona.get('sentence_structure')
16 if sentence_structure:
17 attributes.append(f"You use {sentence_structure} sentence structures.")
18
19 # Add more attributes as needed
20 # ...
21
22 description = ' '.join(attributes)
23 return f"You are {persona.get('name', 'a speaker')}. {description}"

7.3. Adjusting Prompts for LLM

Ensure that the system prompt is concise but informative. Overly long prompts may exceed the LLM's context window or lead to less coherent responses.


8. Considerations and Best Practices

  • LLM Limitations:

    • The LLM's ability to mimic detailed stylistic attributes may vary.
    • Some attributes may have a more pronounced effect on the output than others.
  • Prompt Engineering:

    • Experiment with how you convey persona attributes in the prompt.
    • You may need to adjust wording to achieve the desired effect.
  • Performance:

    • Loading many large persona files might impact performance.
    • Consider optimizing persona file sizes if necessary.
  • Error Handling:

    • Add error handling for missing attributes or files.
    • Validate persona data when loading.

9. Summary of Changes

  • File Structure:

    • Added a personas directory containing JSON files for each persona.
  • Code Modifications:

    • Added functions to load personas from JSON files.
    • Updated the Node class to handle persona attributes.
    • Modified generate_response to include persona attributes in system prompts.
    • Created build_system_prompt to construct system prompts from persona attributes.
    • Adjusted node creation to reference personas by name from the JSON files.
  • Usage:

    • Personas can now be added or modified by editing the JSON files.
    • The application uses these personas to tailor LLM outputs accordingly.

10. Next Steps

  • Enhance Persona Attributes Handling:

    • Implement more sophisticated mapping between persona attributes and system prompts.
    • Possibly use templates or mapping dictionaries to handle various attribute values.
  • Integrate with a Database (Optional):

    • If you prefer using SQLite for storing personas, you can modify the code to load personas from a SQLite database instead of JSON files.
  • User Interface:

    • Create a CLI or GUI to select personas and customize prompts.
  • Logging and Analysis:

    • Enhance logging to include which attributes were applied.
    • Analyze how different attributes affect the generated responses.

11. Conclusion

By incorporating detailed personas with stylistic attributes, the application can generate more personalized and varied responses from the LLM. This enhancement adds depth to the generated content and allows for experimentation with different writing styles and perspectives.


Debugged final main.py

python
1# main.py
2
3import os
4import networkx as nx
5from langchain import PromptTemplate, LLMChain
6from langchain.llms import Ollama
7import json
8
9
10
11
12
13# Define the Node class
14class Node:
15 def __init__(self, node_id, prompt_text, persona_name):
16 self.id = node_id
17 self.prompt_text = prompt_text
18 self.response_text = None
19 self.context = ""
20 self.persona_name = persona_name
21 self.persona_attributes = {}
22
23# Initialize the graph
24G = nx.DiGraph()
25
26# Define personas
27personas = {
28 "Historian": "You are a knowledgeable historian specializing in the industrial revolution.",
29 "Scientist": "You are a scientist with expertise in technological advancements.",
30 "Philosopher": "You are a philosopher pondering societal impacts.",
31 "Analyst": "You analyze information critically to provide insights.",
32 # Add additional personas as needed
33}
34
35def load_personas(persona_dir):
36 personas = {}
37 for filename in os.listdir(persona_dir):
38 if filename.endswith('.json'):
39 filepath = os.path.join(persona_dir, filename)
40 with open(filepath, 'r', encoding='utf-8') as f:
41 persona_data = json.load(f)
42 name = persona_data.get('name')
43 if name:
44 personas[name] = persona_data
45 return personas
46
47# Load personas
48persona_dir = 'personas' # Directory where persona JSON files are stored
49personas = load_personas(persona_dir)
50
51# Function to collect context from predecessor nodes
52def collect_context(node_id):
53 predecessors = list(G.predecessors(node_id))
54 context = ""
55 for pred_id in predecessors:
56 pred_node = G.nodes[pred_id]['data']
57 if pred_node.response_text:
58 context += f"From {pred_node.persona}:\n{pred_node.response_text}\n\n"
59 return context
60
61# Function to generate responses using LangChain and Ollama
62def generate_response(node):
63 persona = personas.get(node.persona_name)
64 if not persona:
65 raise ValueError(f"Persona '{node.persona_name}' not found.")
66
67 node.persona_attributes = persona
68
69 # Build the system prompt based on persona attributes
70 system_prompt = build_system_prompt(persona)
71
72 # Build the complete prompt
73 prompt_template = PromptTemplate(
74 input_variables=["system_prompt", "context", "prompt"],
75 template="{system_prompt}\n\n{context}\n\n{prompt}"
76 )
77 # Instantiate the Ollama LLM
78 llm = Ollama(
79 base_url="http://localhost:11434", # Default Ollama server URL
80 model="qwq", # Replace with the model you have downloaded
81 )
82 chain = LLMChain(llm=llm, prompt=prompt_template)
83 response = chain.run(
84 system_prompt=system_prompt,
85 context=node.context,
86 prompt=node.prompt_text
87 )
88 return response
89
90def build_system_prompt(persona):
91 # Construct descriptive sentences based on persona attributes
92 # We'll focus on key attributes for brevity
93 name = persona.get('name', 'The speaker')
94 tone = persona.get('tone', 'neutral')
95 sentence_structure = persona.get('sentence_structure', 'varied')
96 vocabulary_complexity = persona.get('vocabulary_complexity', 5)
97 formality_level = persona.get('formality_level', 5)
98 pronoun_preference = persona.get('pronoun_preference', 'third-person')
99 language_abstraction = persona.get('language_abstraction', 'mixed')
100
101 # Create a description
102 description = (
103 f"You are {name}, writing in a {tone} tone using {sentence_structure} sentences. "
104 f"Your vocabulary complexity is {vocabulary_complexity}/10, and your formality level is {formality_level}/10. "
105 f"You prefer {pronoun_preference} narration and your language abstraction is {language_abstraction}."
106 )
107
108 # Include any other attributes as needed
109 # ...
110
111 return description
112
113
114# Function to log interactions to a markdown file
115def update_markdown(node):
116 with open("conversation.md", "a", encoding="utf-8") as f:
117 f.write(f"## Node {node.id}: {node.persona_name}\n\n")
118 f.write(f"**Prompt:**\n\n{node.prompt_text}\n\n")
119 f.write(f"**Response:**\n\n{node.response_text}\n\n---\n\n")
120
121# Function for nodes that perform analysis
122def analyze_responses(node):
123 # Collect responses from predecessor nodes
124 predecessors = list(G.predecessors(node.id))
125 analysis_input = ""
126 for pred_id in predecessors:
127 pred_node = G.nodes[pred_id]['data']
128 analysis_input += f"{pred_node.persona_name}'s response:\n{pred_node.response_text}\n\n"
129
130 node.prompt_text = f"Provide an analysis comparing the following perspectives:\n\n{analysis_input}"
131 node.context = "" # Analysis is based solely on the provided responses
132 node.response_text = generate_response(node)
133 update_markdown(node)
134
135# Build the graph
136
137# Create initial prompt nodes with different personas
138node1 = Node(1, prompt_text="Discuss the impacts of the industrial revolution.", persona_name="Ernest Hemingway")
139G.add_node(node1.id, data=node1)
140
141# You can create more nodes with different personas
142node2 = Node(2, prompt_text="Explain quantum mechanics in simple terms.", persona_name="Albert Einstein")
143G.add_node(node2.id, data=node2)
144
145# Add edges between nodes if needed
146# G.add_edge(node1.id, node2.id)
147
148# Add an analysis node (you can define a generic analyst persona)
149node3 = Node(3, prompt_text="", persona_name="Analyst")
150G.add_node(node3.id, data=node3)
151G.add_edge(node1.id, node3.id)
152G.add_edge(node2.id, node3.id)
153
154# Process the nodes
155for node_id in nx.topological_sort(G):
156 node = G.nodes[node_id]['data']
157 if node.persona_name != "Analyst":
158 node.context = collect_context(node_id)
159 node.response_text = generate_response(node)
160 update_markdown(node)
161 else:
162 analyze_responses(node)
163
164print("Conversation has been generated and logged to conversation.md")

Given that a CLI is generally quicker to implement and can provide immediate utility, let's start with creating a CLI using the argparse or click library. After that, I'll provide guidance on how you can set up a GUI using Streamlit, which is a Python library that allows for quick and easy creation of web apps.


Option 1: Creating a Command Line Interface (CLI)

We'll enhance your application to allow users to:

  • List available personas.
  • Select personas for nodes.
  • Input custom prompts.
  • Customize the graph structure if desired.

1. Install the Required Libraries

We can use the argparse module from the standard library or the click library, which is more user-friendly for complex CLIs.

Let's use click:

bash
1pip install click

Add this to your requirements.txt:

click

2. Modify main.py to Include CLI Functionality

2.1. Import click Module

At the top of your main.py, import click:

python
1import click

2.2. Refactor the Code into Functions

We'll encapsulate the main logic into functions that we can call from the CLI commands.

Encapsulate Graph Building into a Function:

python
1def build_graph(nodes_info, edges_info):
2 G = nx.DiGraph()
3 nodes = {}
4 # Create nodes
5 for node_info in nodes_info:
6 node_id = node_info['id']
7 prompt_text = node_info['prompt_text']
8 persona_name = node_info['persona_name']
9 node = Node(node_id, prompt_text, persona_name)
10 G.add_node(node_id, data=node)
11 nodes[node_id] = node
12
13 # Add edges
14 for edge in edges_info:
15 G.add_edge(edge['from'], edge['to'])
16
17 return G

Encapsulate Node Processing into a Function:

python
1def process_graph(G):
2 for node_id in nx.topological_sort(G):
3 node = G.nodes[node_id]['data']
4 if node.persona_name != "Analyst":
5 node.context = collect_context(node_id, G)
6 node.response_text = generate_response(node)
7 update_markdown(node)
8 else:
9 analyze_responses(node, G)

Update the collect_context and analyze_responses functions to accept G as a parameter:

python
1def collect_context(node_id, G):
2 # Existing code...
python
1def analyze_responses(node, G):
2 # Existing code...

2.3. Create CLI Commands with click

Below all the functions, add the CLI commands:

python
1@click.group()
2def cli():
3 pass

Command to List Available Personas:

python
1@cli.command()
2def list_personas():
3 """List all available personas."""
4 for persona_name in personas.keys():
5 print(persona_name)

Command to Run the Application with Custom Inputs:

python
1@cli.command()
2@click.option('--nodes', '-n', default=2, help='Number of nodes (excluding the analyst node).')
3def run(nodes):
4 """Run the application with the specified number of nodes."""
5 # Let the user select personas and input prompts for each node
6 nodes_info = []
7 for i in range(1, nodes + 1):
8 print(f"\nConfiguring Node {i}")
9 persona_name = click.prompt('Enter the persona name', type=str)
10 while persona_name not in personas:
11 print('Persona not found. Available personas:')
12 for name in personas.keys():
13 print(f" - {name}")
14 persona_name = click.prompt('Enter the persona name', type=str)
15
16 prompt_text = click.prompt('Enter the prompt text', type=str)
17 node_info = {
18 'id': i,
19 'prompt_text': prompt_text,
20 'persona_name': persona_name
21 }
22 nodes_info.append(node_info)
23
24 # Add the analyst node
25 analyst_node_id = nodes + 1
26 analyst_node_info = {
27 'id': analyst_node_id,
28 'prompt_text': '',
29 'persona_name': 'Analyst'
30 }
31 nodes_info.append(analyst_node_info)
32
33 # Define edges (here we assume that the analyst node depends on all other nodes)
34 edges_info = []
35 for i in range(1, nodes + 1):
36 edges_info.append({'from': i, 'to': analyst_node_id})
37
38 # Build and process the graph
39 G = build_graph(nodes_info, edges_info)
40 process_graph(G)
41 print("\nConversation has been generated and logged to conversation.md")

2.4. Update the Main Execution Block

Replace the existing execution code at the bottom of main.py with:

python
1if __name__ == '__main__':
2 cli()

3. Running the Updated Application

3.1. List Available Personas

bash
1python main.py list-personas

Output:

text
1Ernest Hemingway
2Albert Einstein
3Analyst

3.2. Run the Application with Custom Inputs

bash
1python main.py run --nodes 2

The application will prompt you for inputs:

text
1Configuring Node 1
2Enter the persona name: Ernest Hemingway
3Enter the prompt text: Discuss the impacts of the industrial revolution.
4
5Configuring Node 2
6Enter the persona name: Albert Einstein
7Enter the prompt text: Explain quantum mechanics in simple terms.
8
9Conversation has been generated and logged to conversation.md

Option 2: Creating a Graphical User Interface (GUI) with Streamlit

Streamlit allows you to turn your Python scripts into interactive web apps with minimal effort.

1. Install Streamlit

bash
1pip install streamlit

Add this to your requirements.txt:

streamlit

2. Create a New Streamlit App File

Create a new file called app.py in your project directory.

3. Write the Streamlit App

Import Necessary Modules in app.py:

python
1import streamlit as st
2import os
3import json
4import networkx as nx
5from main import Node, generate_response, collect_context, analyze_responses, build_system_prompt, load_personas, update_markdown, process_graph, build_graph

Ensure main.py Functions are Importable

  • Modify your main.py functions to be importable without executing the CLI commands.
  • Place the CLI commands under if __name__ == '__main__':.

Load Personas

python
1# Load personas
2persona_dir = 'personas'
3personas = load_personas(persona_dir)

Streamlit Interface

python
1def main():
2 st.title("LangChain Graph App")
3
4 st.header("Create a Conversation Graph")
5
6 # Select number of nodes
7 num_nodes = st.number_input('Number of nodes (excluding the analyst node):', min_value=1, value=2)
8
9 nodes_info = []
10 for i in range(1, int(num_nodes) + 1):
11 st.subheader(f"Node {i} Configuration")
12 persona_name = st.selectbox(f"Select persona for Node {i}:", options=list(personas.keys()), key=f"persona_{i}")
13 prompt_text = st.text_area(f"Enter the prompt for Node {i}:", key=f"prompt_{i}")
14 node_info = {
15 'id': i,
16 'prompt_text': prompt_text,
17 'persona_name': persona_name
18 }
19 nodes_info.append(node_info)
20
21 # Assume the analyst node
22 analyst_node_id = int(num_nodes) + 1
23 analyst_node_info = {
24 'id': analyst_node_id,
25 'prompt_text': '',
26 'persona_name': 'Analyst'
27 }
28 nodes_info.append(analyst_node_info)
29
30 # Define edges
31 edges_info = []
32 for i in range(1, int(num_nodes) + 1):
33 edges_info.append({'from': i, 'to': analyst_node_id})
34
35 if st.button("Generate Conversation"):
36 G = build_graph(nodes_info, edges_info)
37 process_graph(G)
38 st.success("Conversation has been generated and logged to conversation.md")
39
40 # Display the conversation
41 with open("conversation.md", "r", encoding="utf-8") as f:
42 content = f.read()
43 st.markdown(content)

Run the Streamlit App in app.py:

python
1if __name__ == '__main__':
2 main()

4. Running the Streamlit App

In your terminal, run:

bash
1streamlit run app.py

A web browser will open displaying your app.

5. Interact with the GUI

  • Select the number of nodes.
  • Choose personas from dropdown menus.
  • Enter prompts for each node.
  • Click "Generate Conversation" to run the application.
  • The conversation will be displayed on the page.

Adjustments to main.py

To make the functions importable for the GUI, ensure that your main.py does not execute any code upon import.

  • Place the CLI commands and any execution code under:
python
1if __name__ == '__main__':
2 cli()

Summary

  • CLI Option:

    • Use click to create a user-friendly command-line interface.
    • Allows users to select personas and customize prompts interactively in the terminal.
  • GUI Option:

    • Use Streamlit to build a simple web application.
    • Users can select personas and enter prompts in a web browser interface.

Next Steps

  • Add Error Handling:

    • Validate user inputs for personas and prompts.
    • Handle exceptions that may occur during execution.
  • Enhance the GUI:

    • Allow users to define custom personas through the interface.
    • Provide visualization of the conversation graph.
  • Improve the CLI:

    • Add more options for advanced customization.
    • Save and load previous configurations.
  • Documentation:

    • Update README files with instructions on how to use the CLI and GUI.
    • Provide examples and screenshots for clarity.

Testing

  • CLI Testing:

    • Run various configurations through the CLI to ensure functionality.
    • Test with different numbers of nodes and personas.
  • GUI Testing:

    • Interact with the app in the browser.
    • Confirm that conversations are generated as expected.

Conclusion

By implementing either the CLI or GUI, you've enhanced your application to be more user-friendly and interactive. Users can now select personas and customize prompts without modifying the code directly.

Final main.py:

python
1# main.py
2import click
3import os
4import networkx as nx
5from langchain import PromptTemplate, LLMChain
6from langchain.llms import Ollama
7import json
8
9
10
11
12
13# Define the Node class
14class Node:
15 def __init__(self, node_id, prompt_text, persona_name):
16 self.id = node_id
17 self.prompt_text = prompt_text
18 self.response_text = None
19 self.context = ""
20 self.persona_name = persona_name
21 self.persona_attributes = {}
22
23# Initialize the graph
24G = nx.DiGraph()
25
26def build_graph(nodes_info, edges_info):
27 G = nx.DiGraph()
28 nodes = {}
29 # Create nodes
30 for node_info in nodes_info:
31 node_id = node_info['id']
32 prompt_text = node_info['prompt_text']
33 persona_name = node_info['persona_name']
34 node = Node(node_id, prompt_text, persona_name)
35 G.add_node(node_id, data=node)
36 nodes[node_id] = node
37
38 # Add edges
39 for edge in edges_info:
40 G.add_edge(edge['from'], edge['to'])
41
42 return G
43
44def process_graph(G):
45 for node_id in nx.topological_sort(G):
46 node = G.nodes[node_id]['data']
47 if node.persona_name != "Analyst":
48 node.context = collect_context(node_id, G)
49 node.response_text = generate_response(node)
50 update_markdown(node)
51 else:
52 analyze_responses(node, G)
53
54def load_personas(persona_dir):
55 personas = {}
56 for filename in os.listdir(persona_dir):
57 if filename.endswith('.json'):
58 filepath = os.path.join(persona_dir, filename)
59 with open(filepath, 'r', encoding='utf-8') as f:
60 persona_data = json.load(f)
61 name = persona_data.get('name')
62 if name:
63 personas[name] = persona_data
64 return personas
65
66# Load personas
67persona_dir = 'personas' # Directory where persona JSON files are stored
68personas = load_personas(persona_dir)
69
70# Function to collect context from predecessor nodes
71def collect_context(node_id, G):
72 predecessors = list(G.predecessors(node_id))
73 context = ""
74 for pred_id in predecessors:
75 pred_node = G.nodes[pred_id]['data']
76 if pred_node.response_text:
77 context += f"From {pred_node.persona}:\n{pred_node.response_text}\n\n"
78 return context
79
80# Function to generate responses using LangChain and Ollama
81def generate_response(node):
82 persona = personas.get(node.persona_name)
83 if not persona:
84 raise ValueError(f"Persona '{node.persona_name}' not found.")
85
86 node.persona_attributes = persona
87
88 # Build the system prompt based on persona attributes
89 system_prompt = build_system_prompt(persona)
90
91 # Build the complete prompt
92 prompt_template = PromptTemplate(
93 input_variables=["system_prompt", "context", "prompt"],
94 template="{system_prompt}\n\n{context}\n\n{prompt}"
95 )
96 # Instantiate the Ollama LLM
97 llm = Ollama(
98 base_url="http://localhost:11434", # Default Ollama server URL
99 model="qwq", # Replace with the model you have downloaded
100 )
101 chain = LLMChain(llm=llm, prompt=prompt_template)
102 response = chain.run(
103 system_prompt=system_prompt,
104 context=node.context,
105 prompt=node.prompt_text
106 )
107 return response
108
109def build_system_prompt(persona):
110 # Construct descriptive sentences based on persona attributes
111 # We'll focus on key attributes for brevity
112 name = persona.get('name', 'The speaker')
113 tone = persona.get('tone', 'neutral')
114 sentence_structure = persona.get('sentence_structure', 'varied')
115 vocabulary_complexity = persona.get('vocabulary_complexity', 5)
116 formality_level = persona.get('formality_level', 5)
117 pronoun_preference = persona.get('pronoun_preference', 'third-person')
118 language_abstraction = persona.get('language_abstraction', 'mixed')
119
120 # Create a description
121 description = (
122 f"You are {name}, writing in a {tone} tone using {sentence_structure} sentences. "
123 f"Your vocabulary complexity is {vocabulary_complexity}/10, and your formality level is {formality_level}/10. "
124 f"You prefer {pronoun_preference} narration and your language abstraction is {language_abstraction}."
125 )
126
127 # Include any other attributes as needed
128 # ...
129
130 return description
131
132
133# Function to log interactions to a markdown file
134def update_markdown(node):
135 with open("conversation.md", "a", encoding="utf-8") as f:
136 f.write(f"## Node {node.id}: {node.persona_name}\n\n")
137 f.write(f"**Prompt:**\n\n{node.prompt_text}\n\n")
138 f.write(f"**Response:**\n\n{node.response_text}\n\n---\n\n")
139
140# Function for nodes that perform analysis
141def analyze_responses(node, G):
142 # Collect responses from predecessor nodes
143 predecessors = list(G.predecessors(node.id))
144 analysis_input = ""
145 for pred_id in predecessors:
146 pred_node = G.nodes[pred_id]['data']
147 analysis_input += f"{pred_node.persona_name}'s response:\n{pred_node.response_text}\n\n"
148
149 node.prompt_text = f"Provide an analysis comparing the following perspectives:\n\n{analysis_input}"
150 node.context = "" # Analysis is based solely on the provided responses
151 node.response_text = generate_response(node)
152 update_markdown(node)
153
154
155@click.group()
156def cli():
157 pass
158
159@cli.command()
160def list_personas():
161 """List all available personas."""
162 for persona_name in personas.keys():
163 print(persona_name)
164
165@cli.command()
166@click.option('--nodes', '-n', default=2, help='Number of nodes (excluding the analyst node).')
167def run(nodes):
168 """Run the application with the specified number of nodes."""
169 # Let the user select personas and input prompts for each node
170 nodes_info = []
171 for i in range(1, nodes + 1):
172 print(f"\nConfiguring Node {i}")
173 persona_name = click.prompt('Enter the persona name', type=str)
174 while persona_name not in personas:
175 print('Persona not found. Available personas:')
176 for name in personas.keys():
177 print(f" - {name}")
178 persona_name = click.prompt('Enter the persona name', type=str)
179
180 prompt_text = click.prompt('Enter the prompt text', type=str)
181 node_info = {
182 'id': i,
183 'prompt_text': prompt_text,
184 'persona_name': persona_name
185 }
186 nodes_info.append(node_info)
187
188 # Add the analyst node
189 analyst_node_id = nodes + 1
190 analyst_node_info = {
191 'id': analyst_node_id,
192 'prompt_text': '',
193 'persona_name': 'Analyst'
194 }
195 nodes_info.append(analyst_node_info)
196
197 # Define edges (here we assume that the analyst node depends on all other nodes)
198 edges_info = []
199 for i in range(1, nodes + 1):
200 edges_info.append({'from': i, 'to': analyst_node_id})
201
202 # Build and process the graph
203 G = build_graph(nodes_info, edges_info)
204 process_graph(G)
205 print("\nConversation has been generated and logged to conversation.md")
206
207if __name__ == '__main__':
208 cli()

Then I added the ability to increase the number of iterations through the cli command --iterations X where X is number of iterations.

python
1# main.py
2
3import click
4
5import os
6
7import networkx as nx
8
9from langchain import PromptTemplate, LLMChain
10
11from langchain.llms import Ollama
12
13import json
14
15
16
17
18
19
20
21# Define the Node class
22
23class Node:
24
25def __init__(self, node_id, prompt_text, persona_name):
26
27self.id = node_id
28
29self.prompt_text = prompt_text
30
31self.response_text = None
32
33self.context = ""
34
35self.persona_name = persona_name
36
37self.persona_attributes = {}
38
39
40
41# Initialize the graph
42
43G = nx.DiGraph()
44
45
46
47def build_graph(nodes_info, edges_info):
48
49G = nx.DiGraph()
50
51nodes = {}
52
53# Create nodes
54
55for node_info in nodes_info:
56
57node_id = node_info['id']
58
59prompt_text = node_info['prompt_text']
60
61persona_name = node_info['persona_name']
62
63node = Node(node_id, prompt_text, persona_name)
64
65G.add_node(node_id, data=node)
66
67nodes[node_id] = node
68
69
70
71# Add edges
72
73for edge in edges_info:
74
75G.add_edge(edge['from'], edge['to'])
76
77return G
78
79
80
81def process_graph(G, iterations):
82
83"""Process the graph for the specified number of iterations."""
84
85for iteration in range(iterations):
86
87print(f"\nProcessing iteration {iteration + 1}/{iterations}")
88
89# Store previous responses for context
90
91previous_responses = {}
92
93for node_id in G.nodes():
94
95node = G.nodes[node_id]['data']
96
97if node.response_text:
98
99previous_responses[node_id] = node.response_text
100
101
102
103# Process each node in topological order
104
105for node_id in nx.topological_sort(G):
106
107node = G.nodes[node_id]['data']
108
109if node.persona_name != "Analyst":
110
111node.context = collect_context(node_id, G, iteration, previous_responses)
112
113node.response_text = generate_response(node, iteration)
114
115update_markdown(node, iteration)
116
117else:
118
119analyze_responses(node, G, iteration)
120
121
122
123
124def load_personas(persona_dir):
125
126personas = {}
127
128for filename in os.listdir(persona_dir):
129
130if filename.endswith('.json'):
131
132filepath = os.path.join(persona_dir, filename)
133
134with open(filepath, 'r', encoding='utf-8') as f:
135
136persona_data = json.load(f)
137
138name = persona_data.get('name')
139
140if name:
141
142personas[name] = persona_data
143
144return personas
145
146
147
148# Load personas
149
150persona_dir = 'personas' # Directory where persona JSON files are stored
151
152personas = load_personas(persona_dir)
153
154
155
156# Function to collect context from predecessor nodes
157
158def collect_context(node_id, G, iteration, previous_responses):
159
160"""Collect context including previous iterations."""
161
162predecessors = list(G.predecessors(node_id))
163
164context = ""
165
166# Add context from previous iterations if they exist
167
168if iteration > 0:
169
170context += f"\nPrevious iteration responses:\n"
171
172for pred_id in predecessors:
173
174if pred_id in previous_responses:
175
176pred_node = G.nodes[pred_id]['data']
177
178context += f"From {pred_node.persona_name} (previous round):\n{previous_responses[pred_id]}\n\n"
179
180# Add context from current iteration
181
182context += f"\nCurrent iteration responses:\n"
183
184for pred_id in predecessors:
185
186pred_node = G.nodes[pred_id]['data']
187
188if pred_node.response_text:
189
190context += f"From {pred_node.persona_name}:\n{pred_node.response_text}\n\n"
191
192return context
193
194
195
196# Function to generate responses using LangChain and Ollama
197
198def generate_response(node, iteration):
199
200"""Generate response with awareness of the current iteration."""
201
202persona = personas.get(node.persona_name)
203
204if not persona:
205
206raise ValueError(f"Persona '{node.persona_name}' not found.")
207
208node.persona_attributes = persona
209
210system_prompt = build_system_prompt(persona)
211
212# Modify the prompt to include iteration information
213
214iteration_prompt = f"This is round {iteration + 1} of the conversation. "
215
216if iteration > 0:
217
218iteration_prompt += "Please consider the previous responses in your reply. "
219
220prompt_template = PromptTemplate(
221
222input_variables=["system_prompt", "iteration_prompt", "context", "prompt"],
223
224template="{system_prompt}\n\n{iteration_prompt}\n\n{context}\n\n{prompt}"
225
226)
227
228llm = Ollama(
229
230base_url="http://localhost:11434",
231
232model="qwq",
233
234)
235
236chain = LLMChain(llm=llm, prompt=prompt_template)
237
238response = chain.run(
239
240system_prompt=system_prompt,
241
242iteration_prompt=iteration_prompt,
243
244context=node.context,
245
246prompt=node.prompt_text
247
248)
249
250return response
251
252
253
254def build_system_prompt(persona):
255
256# Construct descriptive sentences based on persona attributes
257
258# We'll focus on key attributes for brevity
259
260name = persona.get('name', 'The speaker')
261
262tone = persona.get('tone', 'neutral')
263
264sentence_structure = persona.get('sentence_structure', 'varied')
265
266vocabulary_complexity = persona.get('vocabulary_complexity', 5)
267
268formality_level = persona.get('formality_level', 5)
269
270pronoun_preference = persona.get('pronoun_preference', 'third-person')
271
272language_abstraction = persona.get('language_abstraction', 'mixed')
273
274
275
276# Create a description
277
278description = (
279
280f"You are {name}, writing in a {tone} tone using {sentence_structure} sentences. "
281
282f"Your vocabulary complexity is {vocabulary_complexity}/10, and your formality level is {formality_level}/10. "
283
284f"You prefer {pronoun_preference} narration and your language abstraction is {language_abstraction}."
285
286)
287
288
289
290# Include any other attributes as needed
291
292# ...
293
294
295
296return description
297
298
299
300
301# Function to log interactions to a markdown file
302
303def update_markdown(node, iteration):
304
305"""Update markdown file with iteration information."""
306
307with open("conversation.md", "a", encoding="utf-8") as f:
308
309f.write(f"## Iteration {iteration + 1} - Node {node.id}: {node.persona_name}\n\n")
310
311f.write(f"**Prompt:**\n\n{node.prompt_text}\n\n")
312
313f.write(f"**Response:**\n\n{node.response_text}\n\n---\n\n")
314
315
316
317# Function for nodes that perform analysis
318
319def analyze_responses(node, G, iteration):
320
321"""Analyze responses with awareness of iteration context."""
322
323predecessors = list(G.predecessors(node.id))
324
325analysis_input = f"Analysis for Iteration {iteration + 1}:\n\n"
326
327for pred_id in predecessors:
328
329pred_node = G.nodes[pred_id]['data']
330
331analysis_input += f"{pred_node.persona_name}'s response:\n{pred_node.response_text}\n\n"
332
333
334
335node.prompt_text = (
336
337f"Provide an analysis comparing the following perspectives from iteration {iteration + 1}:\n\n"
338
339f"{analysis_input}\n"
340
341f"Consider how the conversation has evolved across iterations."
342
343)
344
345node.context = ""
346
347node.response_text = generate_response(node, iteration)
348
349update_markdown(node, iteration)
350
351
352
353
354@click.group()
355
356def cli():
357
358pass
359
360
361
362@cli.command()
363
364def list_personas():
365
366"""List all available personas."""
367
368for persona_name in personas.keys():
369
370print(persona_name)
371
372
373
374@cli.command()
375
376@click.option('--nodes', '-n', default=2, help='Number of nodes (excluding the analyst node).')
377
378@click.option('--iterations', '-i', default=1, help='Number of conversation iterations.')
379
380def run(nodes, iterations):
381
382"""Run the application with the specified number of nodes and iterations."""
383
384# Clear previous conversation file
385
386with open("conversation.md", "w", encoding="utf-8") as f:
387
388f.write("# Conversation Log\n\n")
389
390
391
392# Let the user select personas and input prompts for each node
393
394nodes_info = []
395
396for i in range(1, nodes + 1):
397
398print(f"\nConfiguring Node {i}")
399
400persona_name = click.prompt('Enter the persona name', type=str)
401
402while persona_name not in personas:
403
404print('Persona not found. Available personas:')
405
406for name in personas.keys():
407
408print(f" - {name}")
409
410persona_name = click.prompt('Enter the persona name', type=str)
411
412prompt_text = click.prompt('Enter the prompt text', type=str)
413
414node_info = {
415
416'id': i,
417
418'prompt_text': prompt_text,
419
420'persona_name': persona_name
421
422}
423
424nodes_info.append(node_info)
425
426# Add the analyst node
427
428analyst_node_id = nodes + 1
429
430analyst_node_info = {
431
432'id': analyst_node_id,
433
434'prompt_text': '',
435
436'persona_name': 'Analyst'
437
438}
439
440nodes_info.append(analyst_node_info)
441
442# Define edges
443
444edges_info = []
445
446for i in range(1, nodes + 1):
447
448edges_info.append({'from': i, 'to': analyst_node_id})
449
450# Build and process the graph
451
452G = build_graph(nodes_info, edges_info)
453
454# Process the graph for the specified number of iterations
455
456process_graph(G, iterations)
457
458print(f"\nConversation with {iterations} iterations has been generated and logged to conversation.md")
459
460
461
462if __name__ == '__main__':
463
464cli()
Sovereign AI book cover

Sovereign AI: Building Local-First Intelligent Systems

by Daniel Kliewer · Paperback · 72 pages

The hands-on guide to building AI that runs on your hardware, keeps your data private, and eliminates cloud dependence. Working code included.