Code Less, Prompt Better: Unlocking Python's Built-in LLM Enhancers
In the rapidly evolving landscape of Large Language Models (LLMs), effective prompt engineering has become a crucial skill. While much attention is given to the art of crafting effective prompts, less focus has been placed on how to efficiently manage these prompts programmatically. Python, with its rich set of built-in features, offers powerful tools to dynamically construct, optimize, and manage LLM prompts.
This article explores how Python's built-in features can transform your approach to LLM prompt engineering, making your code more efficient, maintainable, and powerful.
1. Using locals() for Dynamic Context Injection
The Problem
When working with LLMs, we often need to inject contextual information into our prompts. The traditional approach involves manual string formatting:
def generate_response(user_name, user_query, previous_context):
prompt = f"""
User name: {user_name}
User query: {user_query}
Previous context: {previous_context}
Please respond to the user's query considering the context above.
"""
return call_llm_api(prompt)
This works well for simple cases, but becomes unwieldy as the number of variables increases. It's also error-prone – you might forget to include a variable or update a variable name.
The Solution with locals()
Python's locals() function returns a dictionary containing all local variables in the current scope. We can leverage this to automatically include all relevant context:
def generate_response(user_name, user_query, previous_context, user_preferences=None, user_history=None):
# All local variables are now accessible
context_dict = locals()
# Build a dynamic prompt section with all available context
context_sections = []
for key, value in context_dict.items():
if value is not None: # Only include non-None values
context_sections.append(f"{key}: {value}")
context_text = "\n".join(context_sections)
prompt = f"""
Context information:
{context_text}
Please respond to the user's query considering the context above.
"""
return call_llm_api(prompt)
Benefits:
Automatic variable inclusion: If you add a new parameter to your function, it's automatically included in the context.
Reduced errors: No need to manually update string formatting when variables change.
Cleaner code: Separates the mechanism of context injection from the specific variables.
2. Using inspect for Function Documentation
The Problem
When creating LLM prompts that involve function execution or code generation, providing accurate function documentation is crucial:
def create_function_prompt(func_name, params):
prompt = f"""
Create a Python function named '{func_name}' with the following parameters:
{params}
"""
return prompt
This approach requires manually specifying function details, which can be tedious and error-prone.
The Solution with inspect
Python's inspect module allows us to extract rich metadata from functions:
import inspect
def create_function_prompt(func_reference):
# Get the function signature
signature = inspect.signature(func_reference)
# Get the function docstring
doc = inspect.getdoc(func_reference) or "No documentation available"
# Get source code if available
try:
source = inspect.getsource(func_reference)
except:
source = "Source code not available"
prompt = f"""
Function name: {func_reference.__name__}
Signature: {signature}
Documentation:
{doc}
Original source code:
{source}
Please create an improved version of this function.
"""
return prompt
# Example usage
def example_func(a, b=10):
"""This function adds two numbers together."""
return a + b
improved_function_prompt = create_function_prompt(example_func)
# Send to LLM for improvement
This dynamically extracts all relevant information about the function, making the prompt much more informative.
3. Context Management with Class Attributes
The Problem
Managing conversation history and context with LLMs often leads to repetitive code:
conversation_history = []
def chat_with_llm(user_input):
# Manually build the prompt with history
prompt = "Previous conversation:\n"
for entry in conversation_history:
prompt += f"{entry['role']}: {entry['content']}\n"
prompt += f"User: {user_input}\n"
prompt += "Assistant: "
response = call_llm_api(prompt)
# Update history
conversation_history.append({"role": "User", "content": user_input})
conversation_history.append({"role": "Assistant", "content": response})
return response
The Solution with Class Attributes and dict
We can create a conversation manager class that uses Python's object attributes:
class ConversationManager:
def __init__(self, system_prompt=None, max_history=10):
self.history = []
self.system_prompt = system_prompt
self.max_history = max_history
self.user_info = {}
self.conversation_attributes = {
"tone": "helpful",
"style": "concise",
"knowledge_level": "expert"
}
def add_user_info(self, **kwargs):
"""Add user-specific information to the conversation context."""
self.user_info.update(kwargs)
def set_attribute(self, key, value):
"""Set a conversation attribute."""
self.conversation_attributes[key] = value
def build_prompt(self, user_input):
"""Build a complete prompt using object attributes."""
prompt_parts = []
# Add system prompt if available
if self.system_prompt:
prompt_parts.append(f"System: {self.system_prompt}")
# Add conversation attributes
prompt_parts.append("Conversation attributes:")
for key, value in self.conversation_attributes.items():
prompt_parts.append(f"- {key}: {value}")
# Add user info if available
if self.user_info:
prompt_parts.append("\nUser information:")
for key, value in self.user_info.items():
prompt_parts.append(f"- {key}: {value}")
# Add conversation history
if self.history:
prompt_parts.append("\nConversation history:")
for entry in self.history[-self.max_history:]:
prompt_parts.append(f"{entry['role']}: {entry['content']}")
# Add current user input
prompt_parts.append(f"\nUser: {user_input}")
prompt_parts.append("Assistant:")
return "\n".join(prompt_parts)
def chat(self, user_input):
"""Process a user message and get response from LLM."""
prompt = self.build_prompt(user_input)
response = call_llm_api(prompt)
# Update history
self.history.append({"role": "User", "content": user_input})
self.history.append({"role": "Assistant", "content": response})
return response
def get_state_as_dict(self):
"""Return a dictionary of the conversation state using __dict__."""
return self.__dict__
def save_state(self, filename):
"""Save the conversation state to a file."""
import json
with open(filename, 'w') as f:
json.dump(self.get_state_as_dict(), f)
def load_state(self, filename):
"""Load the conversation state from a file."""
import json
with open(filename, 'r') as f:
state = json.load(f)
self.__dict__.update(state)```
Using this approach:
# Create a conversation manager
convo = ConversationManager(system_prompt="You are a helpful assistant.")
# Add user information
convo.add_user_info(name="John", expertise="beginner", interests=["Python", "AI"])
# Set conversation attributes
convo.set_attribute("tone", "friendly")
# Chat with the LLM
response = convo.chat("Can you help me understand how Python dictionaries work?")
print(response)
# Later, save the conversation state
convo.save_state("conversation_backup.json")
# And load it back
new_convo = ConversationManager()
new_convo.load_state("conversation_backup.json")
4. Using dir() for Object Exploration
The Problem
When working with complex objects or APIs, it can be challenging to know what data is available to include in prompts:
def generate_data_analysis_prompt(dataset):
# Manually specifying what we think is available
prompt = f"""
Dataset name: {dataset.name}
Number of rows: {len(dataset)}
Please analyze this dataset.
"""
return prompt
The Solution with dir()
Python's dir() function lets us dynamically discover object attributes and methods:
def generate_data_analysis_prompt(dataset):
# Discover available attributes
attributes = dir(dataset)
# Filter out private attributes (those starting with _)
public_attrs = [attr for attr in attributes if not attr.startswith('_')]
# Build metadata section
metadata = []
for attr in public_attrs:
try:
value = getattr(dataset, attr)
# Only include non-method attributes with simple values
if not callable(value) and not hasattr(value, '__dict__'):
metadata.append(f"{attr}: {value}")
except:
pass # Skip attributes that can't be accessed
metadata_text = "\n".join(metadata)
prompt = f"""
Dataset metadata:
{metadata_text}
Please analyze this dataset based on the metadata above.
"""
return prompt
This approach automatically discovers and includes relevant metadata without requiring us to know the exact structure of the dataset object in advance.
5. String Manipulation for Prompt Cleaning
The Problem
User inputs and other text data often contain formatting issues that can affect LLM performance:
def process_document(document_text):
prompt = f"""
Document:
{document_text}
Please summarize the key points from this document.
"""
return call_llm_api(prompt)
The Solution with String Methods
Python's rich set of string manipulation methods can clean and normalize text:
def process_document(document_text):
# Remove excessive whitespace
cleaned_text = ' '.join(document_text.split())
# Normalize line breaks
cleaned_text = cleaned_text.replace('\r\n', '\n').replace('\r', '\n')
# Limit length (many LLMs have token limits)
max_chars = 5000
if len(cleaned_text) > max_chars:
cleaned_text = cleaned_text[:max_chars] + "... [truncated]"
# Replace problematic characters
for char, replacement in [('\u2018', "'"), ('\u2019', "'"), ('\u201c', '"'), ('\u201d', '"')]:
cleaned_text = cleaned_text.replace(char, replacement)
prompt = f"""
Document:
{cleaned_text}
Please summarize the key points from this document.
"""
return call_llm_api(prompt)
Conclusion
Python's built-in features offer powerful capabilities for enhancing LLM prompts:
Dynamic Context: Using locals() and dict to automatically include relevant variables
Introspection: Using inspect and dir() to extract rich metadata from objects and functions
String Manipulation: Using Python's string methods to clean and normalize text
By leveraging these built-in features, you can create more robust, maintainable, and dynamic LLM interactions. The techniques in this article can help you move beyond static prompt templates to create truly adaptive and context-aware LLM applications.
Most importantly, these approaches scale well as your LLM applications become more complex, allowing you to maintain clean, readable code while supporting sophisticated prompt engineering techniques.
Whether you're building a simple chatbot or a complex AI assistant, Python's built-in features can help you create more effective LLM interactions with less code and fewer errors.