Fade0510 commited on
Commit
b6ed6f7
·
1 Parent(s): 57951aa

Add initial version of the project

Browse files
.gitignore ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ .env
2
+ __pycache__
3
+ .idea
4
+ .venv
5
+ .vscode
6
+ .DS_Store
README.md CHANGED
@@ -1,20 +1,69 @@
1
  ---
2
  title: CallCenterSummarizationAgent
3
- emoji: 🚀
4
- colorFrom: red
5
- colorTo: red
6
- sdk: docker
7
- app_port: 8501
8
- tags:
9
- - streamlit
10
- pinned: false
11
- short_description: Summarize Call Center Conversations
12
  license: mit
13
  ---
 
14
 
15
- # Welcome to Streamlit!
16
 
17
- Edit `/src/streamlit_app.py` to customize this app to your heart's desire. :heart:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
18
 
19
- If you have any questions, checkout our [documentation](https://docs.streamlit.io) and [community
20
- forums](https://discuss.streamlit.io).
 
1
  ---
2
  title: CallCenterSummarizationAgent
 
 
 
 
 
 
 
 
 
3
  license: mit
4
  ---
5
+ # Call Center Data Analysis Agent
6
 
7
+ This project implements a multi-agent workflow using **LangGraph** to process, transcribe, summarize, and score call center data. It comes with a **Streamlit** user interface to easily upload `.csv`, `.mp3`, or `.wav` files and view the resulting insights.
8
 
9
+ ## Workflow Flow & Agent Classes
10
+
11
+ The core analysis logic is driven by a LangGraph StateGraph defined in `src/workflow.py`. The state moves sequentially between several agent classes located in the `src/agents/` directory:
12
+
13
+ 1. **`IntakeAgent` (`src/agents/intake_agent.py`)**
14
+ - *Entry Point*.
15
+ - Reads the uploaded file, validates the file format, extracts basic metadata, and runs a first-pass profanity scrub (using an LLM).
16
+
17
+ 2. **`Router` (`src/agents/router.py`)**
18
+ - *Conditional Routing Node*.
19
+ - Determines the next step based on the file type.
20
+ - **If Audio (`.mp3`, `.wav`)**: Routes to the `TranscriptionAgent`.
21
+ - **If Text (`.csv`)**: Routes directly to the `SummarizationAgent`.
22
+
23
+ 3. **`TranscriptionAgent` (`src/agents/transcription_agent.py`)**
24
+ - Utilizes `openai-whisper` to convert audio files into text.
25
+ - Also scrubs the resulting transcript of profanity before passing it down the pipeline.
26
+
27
+ 4. **`SummarizationAgent` (`src/agents/summarization_agent.py`)**
28
+ - Takes the clean text from either the `IntakeAgent` (if CSV) or the `TranscriptionAgent` (if Audio).
29
+ - Generates a concise summary and extracts key points using OpenAI (`gpt-3.5-turbo`).
30
+
31
+ 5. **`QualityScoringAgent` (`src/agents/quality_scoring_agent.py`)**
32
+ - Takes the clean text and evaluates it against a predefined rubric.
33
+ - Scores the transcript based on Tone, Professionalism, and Structured Resolution.
34
+
35
+ ## Prerequisites
36
+
37
+ - Python 3.9+
38
+ - An OpenAI API key
39
+ - `ffmpeg` installed on your system (required for `openai-whisper` audio transcription).
40
+ - On macOS: `brew install ffmpeg`
41
+ - On Ubuntu/Debian: `sudo apt update && sudo apt install ffmpeg`
42
+
43
+ ## Installation
44
+
45
+ 1. Create a virtual environment and activate it (if you haven't already):
46
+ ```bash
47
+ python -m venv .venv
48
+ source .venv/bin/activate
49
+ ```
50
+ 2. Install the required dependencies:
51
+ ```bash
52
+ pip install -r requirements.txt
53
+ ```
54
+
55
+ ## Running the Application
56
+
57
+ 1. Ensure your OpenAI API key is set in your environment variables:
58
+ ```bash
59
+ export OPENAI_API_KEY="your_api_key_here"
60
+ ```
61
+
62
+ 2. Start the Streamlit application:
63
+ ```bash
64
+ streamlit run app.py
65
+ ```
66
+
67
+ 3. Open your browser to the local URL provided by Streamlit (usually `http://localhost:8501`).
68
+ 4. Use the sidebar to upload a `.csv`, `.mp3`, or `.wav` file and watch the agents analyze your data!
69
 
 
 
requirements.txt CHANGED
@@ -1,3 +1,11 @@
1
  altair
2
  pandas
3
- streamlit
 
 
 
 
 
 
 
 
 
1
  altair
2
  pandas
3
+ streamlit
4
+ plotly
5
+ langgraph
6
+ langchain
7
+ langchain-openai
8
+ openai
9
+ openai-whisper
10
+ python-dotenv
11
+
src/agents/CallState.py ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import TypedDict, Optional, Dict, Any
2
+
3
+ class CallState(TypedDict):
4
+ file_path: str
5
+ file_type: str
6
+ content: Optional[str]
7
+ clean_content: Optional[str]
8
+ metadata: Optional[Dict[str, Any]]
9
+ summary: Optional[str]
10
+ key_points: Optional[str]
11
+ quality_scores: Optional[Dict[str, Any]]
src/agents/IntakeAgent.py ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import pandas as pd
2
+ from langchain_openai import ChatOpenAI
3
+ from langchain_core.prompts import PromptTemplate
4
+ from src.agents.CallState import CallState
5
+
6
+ class IntakeAgent:
7
+ def __init__(self):
8
+ self.llm = ChatOpenAI(model="gpt-3.5-turbo", temperature=0)
9
+
10
+ def __call__(self, state: CallState) -> CallState:
11
+ """Validates input formats, cleans up profanity, extracts meta."""
12
+ file_path = state["file_path"]
13
+ file_type = state["file_type"]
14
+
15
+ state["metadata"] = {"file_type": file_type, "file_path": file_path}
16
+
17
+ if file_type == "csv":
18
+ try:
19
+ df = pd.read_csv(file_path)
20
+ if "transcript" in df.columns:
21
+ content = " ".join(df["transcript"].astype(str).tolist())
22
+ else:
23
+ content = df.to_string()
24
+ state["content"] = content
25
+ except Exception as e:
26
+ state["content"] = f"Error reading CSV: {e}"
27
+
28
+ if state.get("content"):
29
+ prompt = PromptTemplate.from_template(
30
+ "Clean the following text of any profanity and fix basic grammatical errors. Return only the clean text:\n{text}"
31
+ )
32
+ chain = prompt | self.llm
33
+ clean_text = chain.invoke({"text": state["content"]}).content
34
+ state["clean_content"] = clean_text
35
+
36
+ return state
src/agents/QualityScoringAgent.py ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ from langchain_openai import ChatOpenAI
3
+ from langchain_core.prompts import PromptTemplate
4
+ from src.agents.CallState import CallState
5
+
6
+ class QualityScoringAgent:
7
+ def __init__(self):
8
+ self.llm = ChatOpenAI(model="gpt-3.5-turbo", temperature=0)
9
+
10
+ def __call__(self, state: CallState) -> CallState:
11
+ """Evaluates tone, professionalism, and structured resolution with rubric."""
12
+ clean_text = state.get("clean_content", state.get("content", ""))
13
+ if not clean_text:
14
+ return state
15
+
16
+ prompt = PromptTemplate.from_template(
17
+ "Evaluate the following call transcript for tone, professionalism, and structured resolution. "
18
+ "Score each out of 10 based on a strict rubric.\n"
19
+ "Return as JSON string with keys 'tone', 'professionalism', 'structured_resolution', and 'notes'.\n\n"
20
+ "Transcript:\n{text}"
21
+ )
22
+
23
+ chain = prompt | self.llm
24
+ result_text = chain.invoke({"text": clean_text}).content
25
+
26
+ try:
27
+ result_json = json.loads(result_text)
28
+ state["quality_scores"] = result_json
29
+ except json.JSONDecodeError:
30
+ state["quality_scores"] = {"raw_evaluation": result_text}
31
+
32
+ return state
src/agents/Router.py ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ from src.agents.CallState import CallState
2
+
3
+ class Router:
4
+ def __call__(self, state: CallState) -> str:
5
+ """Invokes above agents accordingly."""
6
+ file_type = state["file_type"]
7
+ if file_type in ["mp3", "wav"] and not state.get("content"):
8
+ return "transcribe"
9
+ return "summarize"
src/agents/SummarizationAgent.py ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ from langchain_openai import ChatOpenAI
3
+ from langchain_core.prompts import PromptTemplate
4
+ from src.agents.CallState import CallState
5
+
6
+ class SummarizationAgent:
7
+ def __init__(self):
8
+ self.llm = ChatOpenAI(model="gpt-3.5-turbo", temperature=0)
9
+
10
+ def __call__(self, state: CallState) -> CallState:
11
+ """Generates summaries and key points."""
12
+ clean_text = state.get("clean_content", state.get("content", ""))
13
+ if not clean_text:
14
+ return state
15
+
16
+ prompt = PromptTemplate.from_template(
17
+ "Summarize the following call transcript and extract key points.\n"
18
+ "Return as a JSON string with 'summary' and 'key_points' keys.\n\n"
19
+ "Transcript:\n{text}"
20
+ )
21
+
22
+ chain = prompt | self.llm
23
+ result_text = chain.invoke({"text": clean_text}).content
24
+
25
+ try:
26
+ result_json = json.loads(result_text)
27
+ state["summary"] = result_json.get("summary", "")
28
+ state["key_points"] = result_json.get("key_points", "")
29
+ except json.JSONDecodeError:
30
+ state["summary"] = result_text
31
+ state["key_points"] = "Failed to parse JSON for key points."
32
+
33
+ return state
src/agents/TranscriptionAgent.py ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import whisper
2
+ from langchain_openai import ChatOpenAI
3
+ from langchain_core.prompts import PromptTemplate
4
+ from src.agents.CallState import CallState
5
+
6
+ class TranscriptionAgent:
7
+ def __init__(self):
8
+ self.llm = ChatOpenAI(model="gpt-3.5-turbo", temperature=0)
9
+
10
+ def __call__(self, state: CallState) -> CallState:
11
+ """Converts audio to text using whisper."""
12
+ file_path = state["file_path"]
13
+
14
+ # Load whisper model - using 'base' for faster processing
15
+ model = whisper.load_model("base")
16
+ result = model.transcribe(file_path)
17
+ state["content"] = result["text"]
18
+
19
+ # Clean the transcript
20
+ prompt = PromptTemplate.from_template(
21
+ "Clean the following text of any profanity and fix basic grammatical errors. Return only the clean text:\n{text}"
22
+ )
23
+ chain = prompt | self.llm
24
+ clean_text = chain.invoke({"text": state["content"]}).content
25
+ state["clean_content"] = clean_text
26
+
27
+ return state
src/agents/__init__.py ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ from .CallState import CallState
2
+ from .IntakeAgent import IntakeAgent
3
+ from .TranscriptionAgent import TranscriptionAgent
4
+ from .SummarizationAgent import SummarizationAgent
5
+ from .QualityScoringAgent import QualityScoringAgent
6
+ from .Router import Router
src/streamlit_app.py CHANGED
@@ -1,40 +1,237 @@
1
- import altair as alt
2
- import numpy as np
3
- import pandas as pd
4
  import streamlit as st
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5
 
6
- """
7
- # Welcome to Streamlit!
8
-
9
- Edit `/streamlit_app.py` to customize this app to your heart's desire :heart:.
10
- If you have any questions, checkout our [documentation](https://docs.streamlit.io) and [community
11
- forums](https://discuss.streamlit.io).
12
-
13
- In the meantime, below is an example of what you can do with just a few lines of code:
14
- """
15
-
16
- num_points = st.slider("Number of points in spiral", 1, 10000, 1100)
17
- num_turns = st.slider("Number of turns in spiral", 1, 300, 31)
18
-
19
- indices = np.linspace(0, 1, num_points)
20
- theta = 2 * np.pi * num_turns * indices
21
- radius = indices
22
-
23
- x = radius * np.cos(theta)
24
- y = radius * np.sin(theta)
25
-
26
- df = pd.DataFrame({
27
- "x": x,
28
- "y": y,
29
- "idx": indices,
30
- "rand": np.random.randn(num_points),
31
- })
32
-
33
- st.altair_chart(alt.Chart(df, height=700, width=700)
34
- .mark_point(filled=True)
35
- .encode(
36
- x=alt.X("x", axis=None),
37
- y=alt.Y("y", axis=None),
38
- color=alt.Color("idx", legend=None, scale=alt.Scale()),
39
- size=alt.Size("rand", legend=None, scale=alt.Scale(range=[1, 150])),
40
- ))
 
 
 
 
1
  import streamlit as st
2
+ import sys
3
+ import os
4
+ import json
5
+ from pathlib import Path
6
+ from dotenv import load_dotenv
7
+
8
+ # Load environment variables from .env file
9
+ load_dotenv()
10
+
11
+ sys.path.append(os.path.dirname(os.path.dirname(__file__)))
12
+
13
+ PROCESSED_DIR = "data/processed_results"
14
+
15
+ def apply_material_css():
16
+ st.markdown("""
17
+ <style>
18
+ /* Material Design CSS Overrides */
19
+
20
+ .stApp {
21
+ font-family: 'Roboto', 'Inter', sans-serif;
22
+ background-color: #121212;
23
+ color: #FFFFFF;
24
+ }
25
+
26
+ /* Material Cards for metrics and sections */
27
+ .material-card {
28
+ background-color: #1E1E1E;
29
+ border-radius: 8px;
30
+ padding: 20px;
31
+ box-shadow: 0 4px 6px rgba(0,0,0,0.3);
32
+ margin-bottom: 20px;
33
+ }
34
+
35
+ h1, h2, h3, h4 {
36
+ font-weight: 500;
37
+ }
38
+
39
+ /* Subtle styling for Streamlit columns to look like cards */
40
+ [data-testid="column"] {
41
+ background-color: #1E1E1E;
42
+ border-radius: 8px;
43
+ padding: 20px;
44
+ box-shadow: 0 4px 6px rgba(0,0,0,0.3);
45
+ }
46
+
47
+ </style>
48
+ """, unsafe_allow_html=True)
49
+
50
+ def display_results(final_state):
51
+ st.subheader("Workflow Results")
52
+
53
+ col1, col2 = st.columns(2)
54
+ with col1:
55
+ st.markdown("### Summary")
56
+ st.write(final_state.get("summary", "No summary generated."))
57
+
58
+ st.markdown("### Key Points")
59
+ key_points = final_state.get("key_points", "No key points generated.")
60
+ if isinstance(key_points, list):
61
+ for point in key_points:
62
+ st.markdown(f"- {point}")
63
+ else:
64
+ st.write(key_points)
65
+
66
+ with col2:
67
+ st.markdown("### Quality Scores")
68
+ quality_scores = final_state.get("quality_scores", {})
69
+ if isinstance(quality_scores, dict) and quality_scores:
70
+ import plotly.graph_objects as go
71
+ for metric in ['tone', 'professionalism', 'structured_resolution']:
72
+ if metric in quality_scores:
73
+ try:
74
+ val = float(quality_scores[metric])
75
+ fig = go.Figure(go.Indicator(
76
+ mode="gauge+number",
77
+ value=val,
78
+ title={'text': metric.replace('_', ' ').title(), 'font': {'size': 16, 'color': '#FFFFFF'}},
79
+ gauge={
80
+ 'axis': {'range': [None, 10], 'tickwidth': 1, 'tickcolor': "#BB86FC"},
81
+ 'bar': {'color': "#BB86FC"},
82
+ 'bgcolor': "#1E1E1E",
83
+ 'borderwidth': 2,
84
+ 'bordercolor': "#333333",
85
+ 'steps': [
86
+ {'range': [0, 4], 'color': '#cf6679'},
87
+ {'range': [4, 7], 'color': '#ffb74d'},
88
+ {'range': [7, 10], 'color': '#81c784'}],
89
+ }
90
+ ))
91
+ # Adjust colors for dark theme
92
+ fig.update_layout(
93
+ height=180,
94
+ margin=dict(l=20, r=20, t=40, b=20),
95
+ paper_bgcolor='#1E1E1E',
96
+ plot_bgcolor='#1E1E1E',
97
+ font={'color': '#FFFFFF'}
98
+ )
99
+ st.plotly_chart(fig, use_container_width=True)
100
+ except (ValueError, TypeError):
101
+ st.write(f"**{metric.replace('_', ' ').title()}**: {quality_scores[metric]}")
102
+
103
+ if "notes" in quality_scores:
104
+ st.write("**Notes:**", quality_scores["notes"])
105
+ else:
106
+ st.write("No specific scores generated.", quality_scores)
107
+
108
+ st.markdown("### Metadata")
109
+ metadata = final_state.get("metadata", {})
110
+ if isinstance(metadata, dict) and metadata:
111
+ for k, v in metadata.items():
112
+ st.markdown(f"- **{k.replace('_', ' ').title()}**: {v}")
113
+ else:
114
+ st.write("No metadata available.")
115
+
116
+ def main():
117
+ st.set_page_config(page_title="Call Center Data Analysis", layout="wide")
118
+ apply_material_css()
119
+
120
+ st.title("Call Center Data Analysis Dashboard")
121
+
122
+ os.makedirs(PROCESSED_DIR, exist_ok=True)
123
+ os.makedirs("tmp", exist_ok=True)
124
+
125
+ if "view_mode" not in st.session_state:
126
+ st.session_state.view_mode = "none"
127
+
128
+ def set_upload_mode():
129
+ st.session_state.view_mode = "upload"
130
+
131
+ def set_dropdown_mode():
132
+ st.session_state.view_mode = "dropdown"
133
+
134
+ st.sidebar.header("Upload New File")
135
+ uploaded_file = st.sidebar.file_uploader(
136
+ "Upload a file",
137
+ type=["json", "mp3", "wav", "csv"],
138
+ on_change=set_upload_mode
139
+ )
140
+
141
+ st.sidebar.markdown("---")
142
+ st.sidebar.header("Processed Files")
143
+
144
+ # Get list of processed files
145
+ processed_files = [f for f in os.listdir(PROCESSED_DIR) if f.endswith(".json")]
146
+ processed_files.sort(reverse=True) # Show newest (or reverse alphabetical) first
147
+
148
+ selected_file = None
149
+ if processed_files:
150
+ options = ["-- Select a file --"] + processed_files
151
+ selected_dropdown = st.sidebar.selectbox("View cached results:", options, on_change=set_dropdown_mode)
152
+ if selected_dropdown != "-- Select a file --":
153
+ selected_file = selected_dropdown
154
+ else:
155
+ st.sidebar.info("No files processed yet.")
156
+
157
+ # Prioritize based on view_mode
158
+ if st.session_state.view_mode == "upload" and uploaded_file is not None:
159
+ st.success(f"File '{uploaded_file.name}' uploaded successfully!")
160
+
161
+ from src.workflow import build_workflow
162
+
163
+ file_path = os.path.join("tmp", uploaded_file.name)
164
+ with open(file_path, "wb") as f:
165
+ f.write(uploaded_file.getbuffer())
166
+
167
+ file_extension = uploaded_file.name.split('.')[-1].lower()
168
+
169
+ # Check if already processed to avoid reprocessing on rerun if same file is in uploader
170
+ cached_json_path = os.path.join(PROCESSED_DIR, f"{uploaded_file.name}.json")
171
+
172
+ if os.path.exists(cached_json_path):
173
+ st.info("Loading cached results for this file...")
174
+ with open(cached_json_path, 'r') as f:
175
+ final_state = json.load(f)
176
+ display_results(final_state)
177
+ else:
178
+ st.write("Processing file through LangGraph Workflow...")
179
+ with st.spinner("Agents are analyzing the data..."):
180
+ workflow = build_workflow()
181
+ initial_state = {
182
+ "file_path": file_path,
183
+ "file_type": file_extension
184
+ }
185
+
186
+ final_state = workflow.invoke(initial_state)
187
+
188
+ # Cache the results
189
+ with open(cached_json_path, 'w') as f:
190
+ json.dump(final_state, f)
191
+
192
+ display_results(final_state)
193
+
194
+ elif (st.session_state.view_mode == "dropdown" or st.session_state.view_mode == "none") and selected_file is not None:
195
+ st.info(f"Loading cached results for '{selected_file.replace('.json', '')}'")
196
+ cached_json_path = os.path.join(PROCESSED_DIR, selected_file)
197
+ with open(cached_json_path, 'r') as f:
198
+ final_state = json.load(f)
199
+ display_results(final_state)
200
+
201
+ elif uploaded_file is not None:
202
+ st.success(f"File '{uploaded_file.name}' uploaded successfully!")
203
+
204
+ from src.workflow import build_workflow
205
+
206
+ file_path = os.path.join("tmp", uploaded_file.name)
207
+ with open(file_path, "wb") as f:
208
+ f.write(uploaded_file.getbuffer())
209
+
210
+ file_extension = uploaded_file.name.split('.')[-1].lower()
211
+
212
+ cached_json_path = os.path.join(PROCESSED_DIR, f"{uploaded_file.name}.json")
213
+
214
+ if os.path.exists(cached_json_path):
215
+ st.info("Loading cached results for this file...")
216
+ with open(cached_json_path, 'r') as f:
217
+ final_state = json.load(f)
218
+ display_results(final_state)
219
+ else:
220
+ st.write("Processing file through LangGraph Workflow...")
221
+ with st.spinner("Agents are analyzing the data..."):
222
+ workflow = build_workflow()
223
+ initial_state = {
224
+ "file_path": file_path,
225
+ "file_type": file_extension
226
+ }
227
+ final_state = workflow.invoke(initial_state)
228
+ with open(cached_json_path, 'w') as f:
229
+ json.dump(final_state, f)
230
+ display_results(final_state)
231
+
232
+ else:
233
+ st.info("Please upload a file or select a previously processed file.")
234
+
235
+ if __name__ == "__main__":
236
+ main()
237
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/workflow.py ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from langgraph.graph import StateGraph, END
2
+ from src.agents import (
3
+ CallState,
4
+ IntakeAgent,
5
+ TranscriptionAgent,
6
+ SummarizationAgent,
7
+ QualityScoringAgent,
8
+ Router
9
+ )
10
+
11
+ def build_workflow():
12
+ workflow = StateGraph(CallState)
13
+
14
+ # Initialize agents
15
+ intake_agent = IntakeAgent()
16
+ transcription_agent = TranscriptionAgent()
17
+ summarization_agent = SummarizationAgent()
18
+ quality_scoring_agent = QualityScoringAgent()
19
+ router = Router()
20
+
21
+ # Add nodes
22
+ workflow.add_node("intake", intake_agent)
23
+ workflow.add_node("transcribe", transcription_agent)
24
+ workflow.add_node("summarize", summarization_agent)
25
+ workflow.add_node("score", quality_scoring_agent)
26
+
27
+ # Define entry point
28
+ workflow.set_entry_point("intake")
29
+
30
+ # Add conditional edges from intake
31
+ workflow.add_conditional_edges(
32
+ "intake",
33
+ router,
34
+ {
35
+ "transcribe": "transcribe",
36
+ "summarize": "summarize"
37
+ }
38
+ )
39
+
40
+ # Add standard edges
41
+ workflow.add_edge("transcribe", "summarize")
42
+ workflow.add_edge("summarize", "score")
43
+ workflow.add_edge("score", END)
44
+
45
+ return workflow.compile()