""" Conversation Flow Management - Design and store conversation paths """ import json import uuid from typing import Dict, List, Optional from datetime import datetime import sys import os # Add parent directory to path for imports sys.path.insert(0, os.path.dirname(__file__)) class ConversationNode: """Represents a single node in a conversation flow""" def __init__(self, node_id: str = None, node_type: str = "question", content: str = "", next_node: str = None, branches: List[Dict] = None): self.id = node_id or str(uuid.uuid4()) self.type = node_type # "question", "branch", "end" self.content = content self.next = next_node self.branches = branches or [] # For conditional branching def to_dict(self) -> Dict: """Convert node to dictionary""" return { "id": self.id, "type": self.type, "content": self.content, "next": self.next, "branches": self.branches } @classmethod def from_dict(cls, data: Dict) -> 'ConversationNode': """Create node from dictionary""" return cls( node_id=data.get("id"), node_type=data.get("type", "question"), content=data.get("content", ""), next_node=data.get("next"), branches=data.get("branches", []) ) class ConversationFlow: """Manages a complete conversation flow""" def __init__(self, flow_id: str = None, name: str = "Untitled Flow", description: str = "", nodes: List[ConversationNode] = None): self.id = flow_id or str(uuid.uuid4()) self.name = name self.description = description self.nodes = nodes or [] self.created_at = datetime.now().isoformat() self.updated_at = datetime.now().isoformat() def add_node(self, node: ConversationNode, position: int = None): """Add a node to the flow""" if position is None: self.nodes.append(node) else: self.nodes.insert(position, node) self.updated_at = datetime.now().isoformat() def remove_node(self, node_id: str): """Remove a node from the flow""" self.nodes = [n for n in self.nodes if n.id != node_id] self.updated_at = datetime.now().isoformat() def get_node(self, node_id: str) -> Optional[ConversationNode]: """Get a node by ID""" for node in self.nodes: if node.id == node_id: return node return None def get_start_node(self) -> Optional[ConversationNode]: """Get the first node in the flow""" return self.nodes[0] if self.nodes else None def reorder_node(self, node_id: str, new_position: int): """Move a node to a different position""" node = self.get_node(node_id) if node: self.nodes.remove(node) self.nodes.insert(new_position, node) self.updated_at = datetime.now().isoformat() def to_dict(self) -> Dict: """Convert flow to dictionary""" return { "id": self.id, "name": self.name, "description": self.description, "nodes": [node.to_dict() for node in self.nodes], "created_at": self.created_at, "updated_at": self.updated_at } @classmethod def from_dict(cls, data: Dict) -> 'ConversationFlow': """Create flow from dictionary""" flow = cls( flow_id=data.get("id"), name=data.get("name", "Untitled Flow"), description=data.get("description", "") ) flow.nodes = [ConversationNode.from_dict(n) for n in data.get("nodes", [])] flow.created_at = data.get("created_at", datetime.now().isoformat()) flow.updated_at = data.get("updated_at", datetime.now().isoformat()) return flow def save_to_file(self, filepath: str): """Save flow to JSON file""" with open(filepath, 'w') as f: json.dump(self.to_dict(), f, indent=2) @classmethod def load_from_file(cls, filepath: str) -> 'ConversationFlow': """Load flow from JSON file""" with open(filepath, 'r') as f: data = json.load(f) return cls.from_dict(data) def validate(self) -> tuple[bool, str]: """Validate the flow structure""" if not self.nodes: return False, "Flow must have at least one node" if not self.name or not self.name.strip(): return False, "Flow must have a name" # Check for orphaned nodes (nodes that can't be reached) reachable = set() if self.nodes: current = self.nodes[0] reachable.add(current.id) # Simple validation: check if nodes are in sequence for node in self.nodes: if not node.content or not node.content.strip(): return False, f"Node {node.id} has no content" return True, "Flow is valid" def generate_flow_with_ai(self, llm_backend, num_questions: int = 5): """ Generate conversation flow nodes using AI based on flow name and description. Args: llm_backend: LLM backend to use for generation num_questions: Number of conversation steps to generate """ if not self.name or not self.description: raise ValueError("Flow must have a name and description to generate nodes") # Build prompt for generating conversation flow prompt = f"""Task: Design a structured conversation flow **Interview Topic:** {self.name} **Interview Purpose:** {self.description} **Your Task:** Create {num_questions} conversation steps for a structured qualitative research interview. **Guidelines for Each Step:** - Start with an opening that builds rapport and explains the purpose - Progress from general to specific questions - Each step should be clear, open-ended, and encourage detailed responses - Include natural transition phrases - End with a closing that thanks the respondent - Make questions natural and conversational, not robotic **Output Format:** Number each step (1., 2., 3., etc.) with the exact question or statement to use. **Generate {num_questions} Interview Steps:** 1.""" messages = [ { "role": "system", "content": "You are an expert qualitative research interviewer designing a conversation flow. Create engaging, professional interview questions that will elicit detailed, meaningful responses." }, {"role": "user", "content": prompt} ] try: response = llm_backend.generate(messages, max_tokens=1500, temperature=0.7) self._parse_and_add_nodes(response) return True, "Flow generated successfully!" except Exception as e: return False, f"Flow generation failed: {str(e)}" def _parse_and_add_nodes(self, response: str): """ Parse LLM response and create conversation nodes. Args: response: The LLM-generated response containing numbered questions """ import re # Pattern to match numbered items: "1. Question" or "1) Question" pattern = r'\d+[\.\)]\s+(.+?)(?=\d+[\.\)]|\Z)' matches = re.findall(pattern, response, re.DOTALL) if not matches: # Fallback: split by lines and look for question-like content lines = response.split('\n') matches = [line.strip() for line in lines if line.strip() and len(line.strip()) > 20] for i, match in enumerate(matches): # Clean up the match content = match.split('\n')[0].strip() if not content or len(content) < 10: continue # Determine node type node_type = "question" if i == 0: node_type = "opening" elif i == len(matches) - 1: node_type = "end" # Create and add node node = ConversationNode(content=content, node_type=node_type) if self.nodes: # Link to previous node self.nodes[-1].next = node.id self.add_node(node) def create_example_flow() -> ConversationFlow: """Create an example conversation flow""" flow = ConversationFlow( name="Customer Feedback Interview", description="Structured interview to gather customer feedback on product experience" ) # Add nodes node1 = ConversationNode( content="Hello! Thank you for taking the time to speak with me today. I'd like to understand your experience with our product. First, can you tell me what initially attracted you to our product?", node_type="question" ) node2 = ConversationNode( content="That's interesting. How would you describe your overall experience using the product so far?", node_type="question" ) node3 = ConversationNode( content="What specific features do you find most valuable, and why?", node_type="question" ) node4 = ConversationNode( content="Have you encountered any challenges or frustrations while using the product? If so, can you describe them?", node_type="question" ) node5 = ConversationNode( content="Based on your experience, what improvements or new features would you most like to see?", node_type="question" ) node6 = ConversationNode( content="Thank you so much for sharing your thoughts! Your feedback is incredibly valuable and will help us improve the product. Is there anything else you'd like to add?", node_type="end" ) # Link nodes node1.next = node2.id node2.next = node3.id node3.next = node4.id node4.next = node5.id node5.next = node6.id flow.add_node(node1) flow.add_node(node2) flow.add_node(node3) flow.add_node(node4) flow.add_node(node5) flow.add_node(node6) return flow