File size: 10,444 Bytes
d38e6a5
 
 
 
 
 
 
8056e83
 
 
 
 
d38e6a5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
db39ccf
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
d38e6a5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
"""

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