Skip to content

Commit c5caaf9

Browse files
Add agents, API, and comprehensive tests
Co-authored-by: MinecraftFuns <25814618+MinecraftFuns@users.noreply.github.com>
1 parent d8c4b98 commit c5caaf9

15 files changed

Lines changed: 1202 additions & 1 deletion

File tree

python/langchain/.env.example

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# OpenAI Configuration
2+
OPENAI_API_KEY=your_openai_api_key_here
3+
OPENAI_MODEL=gpt-4o-mini
4+
OPENAI_TEMPERATURE=0.1
5+
6+
# Path to Go application state directory
7+
PROMPTPIPE_STATE_DIR=/var/lib/promptpipe
8+
9+
# Prompt files (relative to python/langchain directory)
10+
INTAKE_BOT_PROMPT_FILE=../../prompts/intake_bot_system.txt
11+
COORDINATOR_PROMPT_FILE=../../prompts/conversation_system_3bot.txt
12+
FEEDBACK_TRACKER_PROMPT_FILE=../../prompts/feedback_tracker_system.txt
13+
PROMPT_GENERATOR_PROMPT_FILE=../../prompts/prompt_generator_system.txt
14+
15+
# Timeouts
16+
FEEDBACK_INITIAL_TIMEOUT=15m
17+
FEEDBACK_FOLLOWUP_DELAY=3h
18+
19+
# Chat History
20+
CHAT_HISTORY_LIMIT=-1
21+
22+
# API Configuration
23+
API_HOST=0.0.0.0
24+
API_PORT=8001
25+
26+
# Debug Mode
27+
DEBUG=false

python/langchain/.gitignore

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
# Python
2+
__pycache__/
3+
*.py[cod]
4+
*$py.class
5+
*.so
6+
.Python
7+
build/
8+
develop-eggs/
9+
dist/
10+
downloads/
11+
eggs/
12+
.eggs/
13+
lib/
14+
lib64/
15+
parts/
16+
sdist/
17+
var/
18+
wheels/
19+
*.egg-info/
20+
.installed.cfg
21+
*.egg
22+
23+
# Virtual Environment
24+
.venv/
25+
venv/
26+
ENV/
27+
env/
28+
29+
# IDE
30+
.vscode/
31+
.idea/
32+
*.swp
33+
*.swo
34+
*~
35+
36+
# Testing
37+
.pytest_cache/
38+
.coverage
39+
htmlcov/
40+
.tox/
41+
42+
# Environment
43+
.env
44+
.env.local
45+
46+
# Logs
47+
*.log
48+
49+
# Database
50+
*.db
51+
*.db-journal
52+
*.db-wal
53+
*.db-shm
54+
55+
# macOS
56+
.DS_Store
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
"""Coordinator agent for routing conversations and managing overall flow."""
2+
3+
import os
4+
from typing import Any, Optional
5+
6+
from langchain.agents import AgentExecutor, create_tool_calling_agent
7+
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
8+
from langchain_openai import ChatOpenAI
9+
10+
from promptpipe_agent.config import settings
11+
from promptpipe_agent.models.schemas import ConversationHistory, MessageRole
12+
from promptpipe_agent.models.state_manager import StateManager
13+
from promptpipe_agent.tools.profile_save_tool import ProfileSaveTool
14+
from promptpipe_agent.tools.prompt_generator_tool import PromptGeneratorTool
15+
from promptpipe_agent.tools.scheduler_tool import SchedulerTool
16+
from promptpipe_agent.tools.state_transition_tool import StateTransitionTool
17+
18+
19+
class CoordinatorAgent:
20+
"""Coordinator agent for managing conversation flow and routing."""
21+
22+
def __init__(
23+
self,
24+
state_manager: StateManager,
25+
llm: Optional[ChatOpenAI] = None,
26+
system_prompt_file: Optional[str] = None,
27+
):
28+
"""Initialize the coordinator agent.
29+
30+
Args:
31+
state_manager: State manager for conversation state
32+
llm: Language model (if None, creates default)
33+
system_prompt_file: Path to system prompt file (if None, uses default)
34+
"""
35+
self.state_manager = state_manager
36+
37+
# Initialize LLM
38+
if llm is None:
39+
self.llm = ChatOpenAI(
40+
model=settings.openai_model,
41+
temperature=settings.openai_temperature,
42+
api_key=settings.openai_api_key,
43+
)
44+
else:
45+
self.llm = llm
46+
47+
# Load system prompt
48+
self.system_prompt_file = (
49+
system_prompt_file or settings.coordinator_prompt_file
50+
)
51+
self.system_prompt = self._load_system_prompt()
52+
53+
# Initialize tools
54+
self.tools = self._create_tools()
55+
56+
# Create agent
57+
self.agent = self._create_agent()
58+
59+
def _load_system_prompt(self) -> str:
60+
"""Load the system prompt from file."""
61+
prompt_file = self.system_prompt_file
62+
if not os.path.isabs(prompt_file):
63+
# Make path relative to project root
64+
base_dir = os.path.dirname(
65+
os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
66+
)
67+
prompt_file = os.path.join(base_dir, prompt_file)
68+
69+
if os.path.exists(prompt_file):
70+
with open(prompt_file, "r") as f:
71+
return f.read().strip()
72+
else:
73+
# Fallback prompt
74+
return (
75+
"You are a helpful coordinator assistant for a habit-building conversation system. "
76+
"You can help users with general questions, route them to specialized modules, "
77+
"and use tools to manage their experience."
78+
)
79+
80+
def _create_tools(self) -> list:
81+
"""Create the tools available to the coordinator."""
82+
return [
83+
StateTransitionTool(state_manager=self.state_manager),
84+
ProfileSaveTool(state_manager=self.state_manager),
85+
SchedulerTool(state_manager=self.state_manager),
86+
PromptGeneratorTool(state_manager=self.state_manager),
87+
]
88+
89+
def _create_agent(self) -> AgentExecutor:
90+
"""Create the LangChain agent executor."""
91+
prompt = ChatPromptTemplate.from_messages(
92+
[
93+
("system", self.system_prompt),
94+
MessagesPlaceholder(variable_name="chat_history", optional=True),
95+
("human", "{input}"),
96+
MessagesPlaceholder(variable_name="agent_scratchpad"),
97+
]
98+
)
99+
100+
agent = create_tool_calling_agent(self.llm, self.tools, prompt)
101+
return AgentExecutor(agent=agent, tools=self.tools, verbose=settings.debug)
102+
103+
def process_message(
104+
self, participant_id: str, user_message: str
105+
) -> str:
106+
"""Process a user message and return the response.
107+
108+
Args:
109+
participant_id: The participant ID
110+
user_message: The user's message
111+
112+
Returns:
113+
The agent's response
114+
"""
115+
# Get conversation history
116+
history = self.state_manager.get_conversation_history(participant_id)
117+
118+
# Convert history to LangChain format (limit if needed)
119+
chat_history = []
120+
messages = history.messages
121+
if settings.chat_history_limit > 0:
122+
messages = messages[-settings.chat_history_limit :]
123+
124+
for msg in messages:
125+
if msg.role == MessageRole.USER:
126+
chat_history.append(("human", msg.content))
127+
elif msg.role == MessageRole.ASSISTANT:
128+
chat_history.append(("ai", msg.content))
129+
130+
# Add user message to history
131+
self.state_manager.add_message(participant_id, MessageRole.USER.value, user_message)
132+
133+
# Process message through agent
134+
try:
135+
result = self.agent.invoke(
136+
{
137+
"input": user_message,
138+
"chat_history": chat_history,
139+
}
140+
)
141+
response = result["output"]
142+
except Exception as e:
143+
response = f"I apologize, but I encountered an error: {str(e)}"
144+
145+
# Add response to history
146+
self.state_manager.add_message(
147+
participant_id, MessageRole.ASSISTANT.value, response
148+
)
149+
150+
return response
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
"""Feedback agent for tracking user feedback and updating profiles."""
2+
3+
import os
4+
from typing import Optional
5+
6+
from langchain.agents import AgentExecutor, create_tool_calling_agent
7+
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
8+
from langchain_openai import ChatOpenAI
9+
10+
from promptpipe_agent.config import settings
11+
from promptpipe_agent.models.schemas import MessageRole
12+
from promptpipe_agent.models.state_manager import StateManager
13+
from promptpipe_agent.tools.profile_save_tool import ProfileSaveTool
14+
from promptpipe_agent.tools.scheduler_tool import SchedulerTool
15+
from promptpipe_agent.tools.state_transition_tool import StateTransitionTool
16+
17+
18+
class FeedbackAgent:
19+
"""Feedback agent for tracking habit feedback and updating profiles."""
20+
21+
def __init__(
22+
self,
23+
state_manager: StateManager,
24+
llm: Optional[ChatOpenAI] = None,
25+
system_prompt_file: Optional[str] = None,
26+
):
27+
"""Initialize the feedback agent.
28+
29+
Args:
30+
state_manager: State manager for conversation state
31+
llm: Language model (if None, creates default)
32+
system_prompt_file: Path to system prompt file (if None, uses default)
33+
"""
34+
self.state_manager = state_manager
35+
36+
# Initialize LLM
37+
if llm is None:
38+
self.llm = ChatOpenAI(
39+
model=settings.openai_model,
40+
temperature=settings.openai_temperature,
41+
api_key=settings.openai_api_key,
42+
)
43+
else:
44+
self.llm = llm
45+
46+
# Load system prompt
47+
self.system_prompt_file = (
48+
system_prompt_file or settings.feedback_tracker_prompt_file
49+
)
50+
self.system_prompt = self._load_system_prompt()
51+
52+
# Initialize tools
53+
self.tools = self._create_tools()
54+
55+
# Create agent
56+
self.agent = self._create_agent()
57+
58+
def _load_system_prompt(self) -> str:
59+
"""Load the system prompt from file."""
60+
prompt_file = self.system_prompt_file
61+
if not os.path.isabs(prompt_file):
62+
# Make path relative to project root
63+
base_dir = os.path.dirname(
64+
os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
65+
)
66+
prompt_file = os.path.join(base_dir, prompt_file)
67+
68+
if os.path.exists(prompt_file):
69+
with open(prompt_file, "r") as f:
70+
return f.read().strip()
71+
else:
72+
# Fallback prompt
73+
return (
74+
"You are a feedback tracker that helps users reflect on their habit attempts. "
75+
"Ask about what worked, what didn't, and help them improve their approach."
76+
)
77+
78+
def _create_tools(self) -> list:
79+
"""Create the tools available to the feedback agent."""
80+
return [
81+
StateTransitionTool(state_manager=self.state_manager),
82+
ProfileSaveTool(state_manager=self.state_manager),
83+
SchedulerTool(state_manager=self.state_manager),
84+
]
85+
86+
def _create_agent(self) -> AgentExecutor:
87+
"""Create the LangChain agent executor."""
88+
prompt = ChatPromptTemplate.from_messages(
89+
[
90+
("system", self.system_prompt),
91+
MessagesPlaceholder(variable_name="chat_history", optional=True),
92+
("human", "{input}"),
93+
MessagesPlaceholder(variable_name="agent_scratchpad"),
94+
]
95+
)
96+
97+
agent = create_tool_calling_agent(self.llm, self.tools, prompt)
98+
return AgentExecutor(agent=agent, tools=self.tools, verbose=settings.debug)
99+
100+
def process_message(
101+
self, participant_id: str, user_message: str
102+
) -> str:
103+
"""Process a user message and return the response.
104+
105+
Args:
106+
participant_id: The participant ID
107+
user_message: The user's message
108+
109+
Returns:
110+
The agent's response
111+
"""
112+
# Get conversation history
113+
history = self.state_manager.get_conversation_history(participant_id)
114+
115+
# Get user profile for context
116+
profile = self.state_manager.get_user_profile(participant_id)
117+
profile_context = ""
118+
if profile:
119+
profile_context = f"\n\nCurrent Profile:\n{profile.to_context_string()}"
120+
121+
# Convert history to LangChain format (limit if needed)
122+
chat_history = []
123+
messages = history.messages
124+
if settings.chat_history_limit > 0:
125+
messages = messages[-settings.chat_history_limit :]
126+
127+
for msg in messages:
128+
if msg.role == MessageRole.USER:
129+
chat_history.append(("human", msg.content))
130+
elif msg.role == MessageRole.ASSISTANT:
131+
chat_history.append(("ai", msg.content))
132+
133+
# Add user message to history
134+
self.state_manager.add_message(participant_id, MessageRole.USER.value, user_message)
135+
136+
# Enhance input with profile context
137+
enhanced_input = user_message
138+
if profile_context:
139+
enhanced_input = f"{user_message}{profile_context}"
140+
141+
# Process message through agent
142+
try:
143+
result = self.agent.invoke(
144+
{
145+
"input": enhanced_input,
146+
"chat_history": chat_history,
147+
}
148+
)
149+
response = result["output"]
150+
except Exception as e:
151+
response = f"I apologize, but I encountered an error: {str(e)}"
152+
153+
# Add response to history
154+
self.state_manager.add_message(
155+
participant_id, MessageRole.ASSISTANT.value, response
156+
)
157+
158+
return response

0 commit comments

Comments
 (0)