├── .github └── workflows │ └── main.yml ├── .gitignore ├── README.md ├── app ├── __init__.py ├── agents │ ├── agent.py │ └── agent_vision.py ├── cli.py ├── db │ ├── __init__.py │ └── databases.py ├── log_service │ ├── chat_log_creation.py │ └── sip_log_creation.py ├── main.py ├── models │ ├── __init__.py │ ├── model.py │ └── schemas.py └── services │ ├── __init__.py │ ├── campaign_helper.py │ ├── livkit_rag.py │ ├── llama_index_integration.py │ ├── llm.py │ └── utils.py ├── docs ├── Images │ ├── 11r_Image_14.png │ ├── 4Cj_Image_2.png │ ├── 4K6_Image_13.png │ ├── 9Yp_Image_6.png │ ├── Dns_Image_25.png │ ├── GTe_Image_10.png │ ├── HWk_Image_1.png │ ├── He6_Image_7.png │ ├── MG0_Image_24.png │ ├── Njd_Image_15.png │ ├── PYD_Image_20.png │ ├── PfR_Image_21.png │ ├── PyO_Image_22.png │ ├── UDL_Image_19.png │ ├── VXK_Image_18.png │ ├── aAi_Image_17.png │ ├── bBC_Image_3.png │ ├── eSe_Image_23.png │ ├── eij_Image_8.png │ ├── ezZ_Image_4.png │ ├── gbz_Image_16.png │ ├── q45_Image_9.png │ ├── sO1_Image_12.png │ ├── tyu_Image_26.png │ ├── ufu_Image_11.png │ └── xFN_Image_5.png ├── intro.gif ├── livkit_server_setup.md ├── livkit_sip_server_setup.md ├── quick_start.md └── road_map.md ├── requirements.txt └── setup.py /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: "Nidum.ai Multi-Platform Build and Release" 2 | on: 3 | push: 4 | branches: 5 | - main 6 | 7 | jobs: 8 | create-release: 9 | runs-on: ubuntu-latest 10 | permissions: 11 | contents: write 12 | strategy: 13 | matrix: 14 | os: [macos-latest, windows-latest, ubuntu-latest] 15 | steps: 16 | - name: Check out the repository 17 | uses: actions/checkout@v3 18 | 19 | - name: Setup Node.js 20 | uses: actions/setup-node@v3 21 | with: 22 | node-version: '20.16.0' 23 | 24 | - name: Install dependencies 25 | run: npm install # Use npm install instead of npm ci to avoid lockfile dependency 26 | 27 | - name: Get version from package.json 28 | id: get_version 29 | run: | 30 | VERSION=$(node -p "require('./package.json').version") 31 | echo "VERSION=$VERSION" >> $GITHUB_ENV 32 | shell: bash 33 | 34 | - name: Build Electron App for ${{ matrix.os }} 35 | run: | 36 | if [ "${{ matrix.os }}" == "macos-latest" ]; then 37 | npm run build -- --mac || { echo "Mac build failed"; exit 1; } 38 | elif [ "${{ matrix.os }}" == "windows-latest" ]; then 39 | npm run build -- --win || { echo "Windows build failed"; exit 1; } 40 | elif [ "${{ matrix.os }}" == "ubuntu-latest" ]; then 41 | npm run build -- --linux || { echo "Linux build failed"; exit 1; } 42 | fi 43 | DIST_PATH="./dist/${{ matrix.os }}" 44 | mkdir -p "$DIST_PATH" 45 | mv dist/* "$DIST_PATH" 46 | shell: bash 47 | 48 | - name: Check if Release Exists 49 | id: check_release 50 | env: 51 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 52 | run: | 53 | if gh release view "v${{ env.VERSION }}" > /dev/null 2>&1; then 54 | echo "RELEASE_EXISTS=true" >> $GITHUB_ENV 55 | else 56 | echo "RELEASE_EXISTS=false" >> $GITHUB_ENV 57 | fi 58 | 59 | - name: Locate Built Files 60 | id: locate_files 61 | run: | 62 | DIST_PATH="./dist/${{ matrix.os }}" 63 | APP_FILES=$(find "$DIST_PATH" -name "*.*" -type f) 64 | if [ -z "$APP_FILES" ]; then 65 | echo "Error: No files found in $DIST_PATH for ${matrix.os}." 66 | exit 1 67 | fi 68 | echo "APP_FILES=$APP_FILES" >> $GITHUB_ENV 69 | shell: bash 70 | 71 | - name: Upload Build Files to Release 72 | if: success() 73 | env: 74 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 75 | run: | 76 | if [ "${{ env.RELEASE_EXISTS }}" = "true" ]; then 77 | echo "Updating existing release for version v${{ env.VERSION }}" 78 | gh release upload "v${{ env.VERSION }}" ${{ env.APP_FILES }} --clobber 79 | else 80 | echo "Creating new release for version v${{ env.VERSION }}" 81 | gh release create "v${{ env.VERSION }}" ${{ env.APP_FILES }} \ 82 | --title "Release v${{ env.VERSION }}" \ 83 | --generate-notes 84 | fi 85 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | *.egg-info/ 23 | .installed.cfg 24 | *.egg 25 | *.whl 26 | 27 | # Installer logs 28 | pip-log.txt 29 | pip-delete-this-directory.txt 30 | 31 | # Unit test / coverage reports 32 | htmlcov/ 33 | .tox/ 34 | .nox/ 35 | .coverage 36 | .coverage.* 37 | .cache 38 | nosetests.xml 39 | coverage.xml 40 | *.cover 41 | *.py,cover 42 | .hypothesis/ 43 | .pytest_cache/ 44 | coverage/ 45 | 46 | # Virtual environments 47 | venv/ 48 | ENV/ 49 | env/ 50 | venv.bak/ 51 | venv_old/ 52 | 53 | # Jupyter Notebook 54 | .ipynb_checkpoints 55 | 56 | # PyCharm / IntelliJ IDE 57 | .idea/ 58 | 59 | # VSCode 60 | .vscode/ 61 | 62 | # Sublime Text 63 | *.sublime-project 64 | *.sublime-workspace 65 | 66 | # Spyder project settings 67 | .spyderproject 68 | .spyproject 69 | 70 | # Rope project settings 71 | .ropeproject 72 | 73 | # Environments 74 | .env 75 | .env.* 76 | .venv 77 | venv/ 78 | 79 | # mypy 80 | .mypy_cache/ 81 | .dmypy.json 82 | dmypy.json 83 | 84 | # Pyre type checker 85 | .pyre/ 86 | 87 | # Pytype static type analyzer 88 | .pytype/ 89 | 90 | # Pyright type checker 91 | .pyright/ 92 | 93 | # Cython debug symbols 94 | cython_debug/ 95 | 96 | # Django stuff: 97 | *.log 98 | local_settings.py 99 | db.sqlite3 100 | db.sqlite3-journal 101 | 102 | # Flask stuff: 103 | instance/ 104 | .webassets-cache 105 | 106 | # Scrapy stuff: 107 | .scrapy 108 | 109 | # Sphinx documentation 110 | docs/_build/ 111 | 112 | # PyBuilder 113 | target/ 114 | 115 | # IPython 116 | profile_default/ 117 | ipython_config.py 118 | 119 | # Celery stuff 120 | celerybeat-schedule.* 121 | celerybeat.pid 122 | 123 | # SageMath parsed files 124 | *.sage.py 125 | 126 | # Encrypted credentials 127 | *.key 128 | 129 | # dotenv 130 | *.env 131 | .env 132 | 133 | # FastAPI specific 134 | uploads/ 135 | logs/ 136 | csv_files/ 137 | 138 | # MacOS specific files 139 | .DS_Store 140 | 141 | # Thumbnails 142 | *_thumb.* 143 | 144 | # Temporary files 145 | *.bak 146 | *.swp 147 | *~ 148 | 149 | # Files generated by setup.py 150 | *.egg-info/ 151 | 152 | # Ignore compiled C extensions 153 | *.pyd 154 | 155 | # Livekit logs or configs (if any) 156 | livekit.log 157 | venv/ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # 🚧 Agent Studio Installation Guide 3 | 4 | *This project is currently under active development. You might encounter some issues during setup or usage. We appreciate your understanding and welcome any feedback to improve the experience.* 5 | 6 | --- 7 | ## Featured on Product Hunt 🎉 8 | 9 | [![NidumAI Agent Studio on Product Hunt](https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=nidumai-agent-studio&theme=light)](https://www.producthunt.com/posts/nidumai-agent-studio?utm_source=badge-featured&utm_medium=badge&utm_souce=badge-nidumai-agent-studio) 10 | 11 | # **ROADMAP** 12 | 13 | **Explore our plans and upcoming features for Agent Studio!** 14 | The full roadmap is available [here](https://github.com/NidumAI-Inc/agent-studio/blob/main/docs/road_map.md). 15 | 16 | --- 17 | 18 | ![Agent Studio Introduction](docs/intro.gif) 19 | 20 | Alternatively, you can view it directly on [Vimeo](https://vimeo.com/1027002403). 21 | 22 | --- 23 | 24 | ## Important Note 25 | 26 | **Agent Studio** is an AI agent application designed to handle real-time interactions through phone calls, web-based voice user interfaces (VUI), and SIP capabilities. This open-source platform allows you to create advanced AI-driven communication systems. If you need an on-premise solution, customization, or a secure app setup without third-party APIs, please contact us at **info@nidum.ai**. We are available to assist with setup and provide tailored solutions to meet your requirements. 27 | 28 | --- 29 | 30 | Welcome to the **Agent Studio** installation guide! This document will guide you through setting up the backend server, SIP server, and the user interface (UI). The **Agent Studio** leverages powerful technologies like **Groq**, a highly optimized LLM processing system, and **LlamaIndex** for retrieval-augmented generation (RAG) to provide real-time communication and data management. 31 | 32 | The code repository for **Agent Studio** is available on GitHub: [NidumAI-Inc/agent-studio](https://github.com/NidumAI-Inc/agent-studio). 33 | 34 | --- 35 | 36 | ## Table of Contents 37 | 38 | 1. [Prerequisites](#prerequisites) 39 | 2. [Step 1: Set Up Agent Studio Backend](#step-1-set-up-nidum-ai-studio-backend) 40 | 3. [Step 2: LiveKit On-Premise Installation](#step-2-livekit-on-premise-installation) 41 | 4. [Step 3: SIP Server On-Premise Installation](#step-3-sip-server-on-premise-installation) 42 | 5. [Step 4: Set Up the User Interface (UI)](#step-4-set-up-the-user-interface-ui) 43 | 6. [Step 5: Run Agent Studio and UI](#step-5-run-nidum-ai-studio-and-ui) 44 | 7. [Step 6: Start Services in `tmux` Sessions](#step-6-start-services-in-tmux-sessions) 45 | 8. [Credits](#credits) 46 | 9. [Support](#support) 47 | 48 | --- 49 | 50 | ## Prerequisites 51 | 52 | Before you begin, ensure you have the following installed on your system: 53 | 54 | - **Python 3.8+** 55 | - **MongoDB** (or access to a MongoDB instance) 56 | - **Node.js and npm** (for the UI) 57 | - **Git** 58 | - **tmux** (for managing multiple terminal sessions) 59 | 60 | --- 61 | 62 | ## Step 1: Set Up Agent Studio Backend 63 | 64 | ### 1.1 Clone the Repository 65 | 66 | ```bash 67 | git clone https://github.com/NidumAI-Inc/agent-studio.git 68 | cd aistudio 69 | ``` 70 | 71 | ### 1.2 Create a Virtual Environment 72 | 73 | It’s recommended to use a virtual environment to manage dependencies. 74 | 75 | ```bash 76 | python3 -m venv venv 77 | source venv/bin/activate # For Linux and macOS 78 | # For Windows: 79 | # venv\Scripts\activate 80 | ``` 81 | 82 | ### 1.3 Install Dependencies 83 | 84 | ```bash 85 | pip install --upgrade pip 86 | pip install -r requirements.txt 87 | ``` 88 | 89 | ### 1.4 Configure Environment Variables 90 | 91 | Create a `.env` file in the `app/env/` directory: 92 | 93 | ```bash 94 | mkdir -p app/env 95 | touch app/env/.env 96 | ``` 97 | 98 | Add the following content to `app/env/.env`: 99 | 100 | ```ini 101 | # MongoDB settings 102 | MONGO_USER=your_mongo_user 103 | MONGO_PASSWORD=your_mongo_password 104 | MONGO_HOST=your_mongo_host 105 | 106 | # API Keys 107 | OPENAI_API_KEY=your_openai_api_key 108 | LIVEKIT_API_KEY=your_livekit_api_key 109 | LIVEKIT_API_SECRET=your_livekit_api_secret 110 | DEEPGRAM_API_KEY=your_deepgram_api_key 111 | 112 | # LiveKit URL 113 | LIVEKIT_URL=ws://localhost:7880 114 | ``` 115 | 116 | **Note:** Replace placeholders (e.g., `your_mongo_user`, `your_openai_api_key`) with actual credentials. 117 | 118 | ### 1.5 Install the Agent Studio Package 119 | 120 | ```bash 121 | pip install -e . 122 | ``` 123 | 124 | --- 125 | 126 | ## Step 2: LiveKit and SIP Server On-Premise Installation 127 | 128 | For setting up the LiveKit server and enabling SIP voice communication features, refer to the guides below. These guides will walk you through downloading, configuring, and running LiveKit and the SIP server for real-time communication: 129 | 130 | - **[LiveKit On-Premise Installation Guide](https://github.com/NidumAI-Inc/agent-studio/blob/main/docs/livkit_server_setup.md)** 131 | - **[SIP Server On-Premise Installation Guide](https://github.com/NidumAI-Inc/agent-studio/blob/main/docs/livkit_sip_server_setup.md)** 132 | 133 | --- 134 | 135 | ## Step 4: Set Up the User Interface (UI) 136 | 137 | This section will guide you in setting up the Nidum Voice Agent and Nidum Bot projects. These two UI components are interlinked and must be configured correctly. 138 | 139 | ### 4.1 Clone the Repositories 140 | 141 | Clone both repositories to your local machine: 142 | 143 | ```bash 144 | # Clone Nidum Voice Agent repository 145 | git clone https://github.com/NidumAI-Inc/agent-studio-ui.git 146 | cd agent-studio-ui # Nidum Voice Agent directory 147 | 148 | # Clone Nidum VUI (Nidum Bot) repository 149 | git clone https://github.com/NidumAI-Inc/agent-studio-vui-widget.git 150 | cd agent-studio-vui-widget # Nidum Bot directory 151 | ``` 152 | 153 | ### 4.2 Install Dependencies 154 | 155 | Install the dependencies for both projects separately. 156 | 157 | **Nidum Voice Agent:** 158 | 159 | ```bash 160 | cd aistudio_ui 161 | npm install 162 | # or 163 | yarn install 164 | # or 165 | pnpm install 166 | ``` 167 | 168 | **Nidum Bot:** 169 | 170 | ```bash 171 | cd aistudio_vui_widget 172 | npm install 173 | # or 174 | yarn install 175 | # or 176 | pnpm install 177 | ``` 178 | 179 | ### 4.3 Environment Configuration 180 | 181 | Both projects require environment variables for configuration. Set up the environment variables for each project before running them. 182 | 183 | #### Nidum Voice Agent Environment Variables 184 | 185 | Create a `.env` file in the `aistudio_ui` directory and add the following variables: 186 | 187 | ```ini 188 | MONGODB_URI=your_mongodb_uri 189 | NEXTAUTH_SECRET=your_nextauth_secret 190 | 191 | LIVEKIT_API_KEY=your_livekit_api_key 192 | LIVEKIT_API_SECRET=your_livekit_api_secret 193 | NEXT_PUBLIC_LIVEKIT_URL=ws://localhost:7880 194 | 195 | GMAIL_ID=your_gmail_id 196 | GMAIL_PASS=your_gmail_password 197 | 198 | NEXT_PUBLIC_ML_BACKEND_URL=http://localhost:8000 199 | ALLOWED_BOT_ORIGINS=[] 200 | NEXT_PUBLIC_BOT_LIVE_URL=http://localhost:5000 # Adjust the port if necessary 201 | ``` 202 | 203 | **Note:** Refer to the `.env.example` file in the repository for details on each variable. 204 | 205 | #### Nidum Bot Environment Variables 206 | 207 | Create a `.env` file in the `aistudio_vui_widget` directory and add the following variables: 208 | 209 | ```ini 210 | VITE_API_LIVEKIT_URL=ws://localhost:7880 211 | VITE_API_NEXT_backend=http://localhost:3000 # Nidum Voice Agent backend URL 212 | VITE_API_ML_Backend=http://localhost:8000 # Agent Studio backend URL 213 | ``` 214 | 215 | **Note:** Check the `.env.example` file in the repository for more details. 216 | 217 | ### 4.4 Running the Development Servers 218 | 219 | #### 4.4.1 Start the Nidum Voice Agent Development Server 220 | 221 | Navigate to the `aistudio_ui` directory and run: 222 | 223 | ```bash 224 | npm run dev 225 | # or 226 | yarn dev 227 | # or 228 | pnpm dev 229 | ``` 230 | 231 | Open [http://localhost:3000](http://localhost:3000) in your browser to access the Nidum Voice Agent. 232 | 233 | #### 4.4.2 Start the Nidum Bot Development Server 234 | 235 | Navigate to the `aistudio_vui_widget` directory and run: 236 | 237 | ```bash 238 | npm run build 239 | # or 240 | yarn build 241 | # or 242 | pnpm build 243 | 244 | # Install serve globally if not already installed 245 | npm install -g serve 246 | # or 247 | yarn global add serve 248 | # or 249 | pnpm add -g serve 250 | 251 | # Serve the build 252 | serve -s dist 253 | ``` 254 | 255 | Now, Nidum Bot will be accessible and can be integrated with the Nidum Voice Agent. 256 | 257 | ### 4.5 Project Integration 258 | 259 | Both projects are interdependent and must be configured correctly to interact: 260 | 261 | - **Nidum Voice Agent** relies on **Nidum Bot** for bot communication capabilities. Ensure that `NEXT_PUBLIC_BOT_LIVE_URL` in Nidum Voice Agent's `.env` file points to the correct URL of the Nidum Bot (e.g., `http://localhost:5000`). 262 | - **Nidum Bot** needs to communicate with the Nidum Voice Agent backend. Ensure that the `VITE_API_NEXT_backend` environment variable is correctly set to point to the Nidum Voice Agent’s backend URL (`http://localhost:3000`). 263 | 264 | --- 265 | 266 | ## Step 5: Run Agent Studio and UI 267 | 268 | ### 5.1 Start Agent Studio Backend 269 | 270 | In one terminal window, activate your virtual environment and run: 271 | 272 | ```bash 273 | aistudio start api 274 | 275 | 276 | ``` 277 | 278 | ### 5.2 Start the Agent 279 | 280 | In another terminal window, activate your virtual environment and run: 281 | 282 | ```bash 283 | aistudio start agent 284 | ``` 285 | 286 | ### 5.3 Start the Vision Agent 287 | 288 | The `vision-agent` feature provides additional capabilities for visual data processing, though it currently has limited functionality. Start it in a new terminal window: 289 | 290 | ```bash 291 | aistudio start vision-agent 292 | ``` 293 | 294 | **Note:** The `vision-agent` is in its initial stages and currently offers limited functionality. Further updates will expand its capabilities. 295 | 296 | ### 5.4 Access the Application 297 | 298 | - **Agent Studio Backend API Docs:** [http://localhost:8000/docs](http://localhost:8000/docs) 299 | - **Nidum Voice Agent UI:** [http://localhost:3000](http://localhost:3000) 300 | - **Nidum Bot UI:** Accessible through the Nidum Voice Agent interface. 301 | 302 | --- 303 | 304 | ## Step 6: Start Services in `tmux` Sessions 305 | 306 | For continuous logging and management, we’ll start the following scripts in separate `tmux` sessions: 307 | 308 | 1. **Chat Log Creation Service** 309 | 2. **SIP Log Creation Service** 310 | 3. **Agent Studio App** 311 | 312 | To do this, follow the instructions below. 313 | 314 | ### 6.1 Open a `tmux` Session for Chat Log Creation 315 | 316 | Start `chat_log_creation.py` in a new `tmux` session: 317 | 318 | ```bash 319 | tmux new-session -d -s chat_log "python /app/log_service/chat_log_creation.py" 320 | ``` 321 | 322 | This will create a detached `tmux` session named `chat_log` running `chat_log_creation.py`. You can attach to this session using: 323 | 324 | ```bash 325 | tmux attach -t chat_log 326 | ``` 327 | 328 | ### 6.2 Open a `tmux` Session for SIP Log Creation 329 | 330 | Start `sip_log_creation.py` in a new `tmux` session: 331 | 332 | ```bash 333 | tmux new-session -d -s sip_log "python /Users/kesavan/aistudio/app/log_service/sip_log_creation.py" 334 | ``` 335 | 336 | This will create a detached `tmux` session named `sip_log` running `sip_log_creation.py`. You can attach to this session using: 337 | 338 | ```bash 339 | tmux attach -t sip_log 340 | ``` 341 | 342 | ### 6.3 Open a `tmux` Session for Agent Studio 343 | 344 | To start the main `aistudio` app in a `tmux` session, replace the incorrect command with: 345 | 346 | ```bash 347 | tmux new-session -d -s aistudio "aistudio start api" 348 | ``` 349 | 350 | This will create a detached `tmux` session named `aistudio`. You can attach to this session using: 351 | 352 | ```bash 353 | tmux attach -t aistudio 354 | ``` 355 | 356 | To view or manage any of these sessions, list them using: 357 | 358 | ```bash 359 | tmux list-sessions 360 | ``` 361 | 362 | --- 363 | 364 | ## Credits 365 | 366 | Agent Studio integrates several powerful technologies: 367 | 368 | - **Groq**: The fastest large language model (LLM) processing system, offering unparalleled speed for AI model deployment. 369 | - **LlamaIndex**: Optimized for retrieval-augmented generation (RAG), LlamaIndex enhances the application’s ability to access and generate relevant, contextually informed responses. 370 | - **LiveKit**: For real-time communication capabilities. 371 | 372 | --- 373 | 374 | ## Support 375 | 376 | For inquiries or support, please contact us at **info@nidum.ai**. 377 | 378 | --- 379 | 380 | Thank you for trying out **Agent Studio**! Your feedback is invaluable in helping us improve the platform. 381 | -------------------------------------------------------------------------------- /app/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NidumAI-Inc/agent-studio/67840a495da2220790f00a477b13d1deac722223/app/__init__.py -------------------------------------------------------------------------------- /app/agents/agent.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from datetime import datetime 3 | from pymongo import MongoClient, errors 4 | from aiofile import async_open as open 5 | from dotenv import load_dotenv 6 | from livekit import rtc 7 | from livekit.agents import AutoSubscribe, JobContext, WorkerOptions, cli, llm, JobProcess 8 | from livekit.agents.voice_assistant import VoiceAssistant 9 | from livekit.plugins import deepgram, openai, silero 10 | from livekit.plugins.openai import LLM 11 | from llama_index.core import ( 12 | StorageContext, 13 | load_index_from_storage, 14 | ) 15 | import argparse 16 | from llama_index.core.schema import MetadataMode 17 | import os 18 | import uuid 19 | import jwt 20 | from dotenv import load_dotenv 21 | 22 | import os 23 | 24 | current_dir = os.path.dirname(os.path.abspath(__file__)) 25 | 26 | # Construct the path to the .env file dynamically 27 | env_path = os.path.join(current_dir, "..", "env", ".env") 28 | load_dotenv(dotenv_path=env_path) 29 | 30 | # Access environment variables 31 | mongo_user = os.getenv("MONGO_USER") 32 | mongo_password = os.getenv("MONGO_PASSWORD") 33 | mongo_host = os.getenv("MONGO_HOST") 34 | 35 | openai_api_key = os.getenv("OPENAI_API_KEY") 36 | livekit_url = os.getenv("LIVEKIT_URL") 37 | livekit_api_key = os.getenv("LIVEKIT_API_KEY") 38 | livekit_api_secret = os.getenv("LIVEKIT_API_SECRET") 39 | deepgram_api_key = os.getenv("DEEPGRAM_API_KEY") 40 | 41 | database_name = "voice_ai_app_db" 42 | collection_name = "users" 43 | 44 | # Check if all required environment variables are loaded 45 | if not mongo_user or not mongo_password or not mongo_host: 46 | raise ValueError("MongoDB connection details are missing.") 47 | if not openai_api_key: 48 | raise ValueError("OPENAI_API_KEY is missing.") 49 | if not livekit_api_key or not livekit_api_secret: 50 | raise ValueError("LIVEKIT_API_KEY or LIVEKIT_API_SECRET is missing.") 51 | if not deepgram_api_key: 52 | raise ValueError("DEEPGRAM_API_KEY is missing.") 53 | 54 | def parse_running_job_info(running_job_info): 55 | parsed_data = {} 56 | 57 | # Extract the basic accept_arguments info 58 | accept_arguments = running_job_info.accept_arguments 59 | parsed_data['name'] = accept_arguments.name 60 | parsed_data['identity'] = accept_arguments.identity 61 | parsed_data['metadata'] = accept_arguments.metadata 62 | 63 | # Extract the job details 64 | job = running_job_info.job 65 | parsed_data['job_id'] = job.id 66 | 67 | # Extract room information 68 | room = job.room 69 | parsed_data['room_sid'] = room.sid 70 | parsed_data['room_name'] = room.name 71 | parsed_data['empty_timeout'] = room.empty_timeout 72 | parsed_data['creation_time'] = room.creation_time 73 | parsed_data['turn_password'] = room.turn_password 74 | 75 | # Extract enabled codecs 76 | enabled_codecs = [] 77 | for codec in room.enabled_codecs: 78 | enabled_codecs.append(codec.mime) 79 | parsed_data['enabled_codecs'] = enabled_codecs 80 | 81 | # Extract state information 82 | state = job.state 83 | parsed_data['state_updated_at'] = state.updated_at 84 | 85 | # Use getattr to safely access dispatch_id, default to None if it doesn't exist 86 | parsed_data['dispatch_id'] = getattr(running_job_info, 'dispatch_id', None) 87 | 88 | # Use getattr to safely access other optional attributes like url and token 89 | parsed_data['url'] = getattr(running_job_info, 'url', None) 90 | parsed_data['token'] = getattr(running_job_info, 'token', None) 91 | # token = getattr(running_job_info, 'token', None) 92 | return parsed_data 93 | 94 | def decode_jwt_and_get_room(token): 95 | # Decode the payload without verifying the signature 96 | payload = jwt.decode(token, options={"verify_signature": False}) 97 | # Extract the room from the payload 98 | room = payload.get('video', {}).get('room', None) 99 | if room and '_id_' in room: 100 | return room.split('_id_')[0] 101 | return room 102 | 103 | 104 | 105 | def get_mongo_client(): 106 | connection_string = f"mongodb+srv://{mongo_user}:{mongo_password}@{mongo_host}?retryWrites=true&w=majority" 107 | return MongoClient(connection_string) 108 | 109 | # Function to retrieve assistant data from MongoDB by phone number 110 | def get_assistant_data(phone_number): 111 | client = get_mongo_client() 112 | db = client[database_name] 113 | collection = db[collection_name] 114 | 115 | # Normalize the phone number by stripping any '+' prefix 116 | normalized_phone_number = phone_number.lstrip('+') 117 | 118 | # Create both versions of the phone number (with and without '+') 119 | phone_numbers_to_check = [normalized_phone_number, f'+{normalized_phone_number}'] 120 | 121 | # Use the $in operator to check for both versions in the query 122 | user_data = collection.find_one({ 123 | "agents.phone_number": {"$in": phone_numbers_to_check} 124 | }) 125 | 126 | if user_data: 127 | for agent in user_data['agents']: 128 | # Normalize the agent's phone number as well 129 | agent_phone_number = agent['phone_number'].lstrip('+') 130 | if agent_phone_number == normalized_phone_number: 131 | # Extract user_id and agent_id for persist directory 132 | user_id = user_data['_id'] 133 | agent_id = agent['id'] 134 | rag_enabled = agent.get('rag_enabled', False) 135 | 136 | # If RAG is enabled, use RAG-specific assistant data 137 | if rag_enabled: 138 | return get_rag_assistant_data(agent, user_id, agent_id) 139 | else: 140 | return agent, None # No persist directory needed without RAG 141 | print(f"No agent found with phone number: {phone_number}") 142 | return None, None 143 | else: 144 | print(f"No user found with an agent having phone number: {phone_number}") 145 | return None, None 146 | 147 | # RAG-specific assistant data retrieval 148 | def get_rag_assistant_data(agent, user_id, agent_id): 149 | print("RAG is enabled. Fetching RAG-specific assistant data...") 150 | 151 | # Define persist directory based on user_id and agent_id 152 | persist_dir = f"uploads/{user_id}/{agent_id}/lamadir" 153 | 154 | # Ensure the directory exists or contains necessary files 155 | if not os.path.exists(persist_dir): 156 | print(f"Persist directory not found: {persist_dir}") 157 | 158 | return agent, persist_dir 159 | 160 | def create_identity_folder(identity): 161 | folder_name = f"logs/{identity}" 162 | os.makedirs(folder_name, exist_ok=True) 163 | return folder_name 164 | 165 | def prewarm_fnc(proc: JobProcess): 166 | proc.userdata["vad"] = silero.VAD.load() 167 | 168 | # RAG-specific assistant reply synthesis 169 | async def _will_synthesize_assistant_reply(assistant: VoiceAssistant, chat_ctx: llm.ChatContext, persist_dir): 170 | storage_context = StorageContext.from_defaults(persist_dir=persist_dir) 171 | index = load_index_from_storage(storage_context) 172 | system_msg = chat_ctx.messages[0].copy() # copy system message 173 | user_msg = chat_ctx.messages[-1] # last message from user 174 | 175 | retriever = index.as_retriever() 176 | nodes = await retriever.aretrieve(user_msg.content) 177 | 178 | system_msg.content = "Context that might help answer the user's question:" 179 | for node in nodes: 180 | node_content = node.get_content(metadata_mode=MetadataMode.LLM) 181 | system_msg.content += f"\n\n{node_content}" 182 | 183 | chat_ctx.messages[0] = system_msg 184 | return assistant.llm.chat(chat_ctx=chat_ctx) 185 | 186 | async def entrypoint(ctx: JobContext): 187 | room = ctx.room 188 | room_items = ctx.__dict__.items() 189 | for key, value in room_items: 190 | if key == "_info": 191 | web_identity = decode_jwt_and_get_room(parse_running_job_info(value)["token"]) 192 | break 193 | await ctx.connect(auto_subscribe=AutoSubscribe.AUDIO_ONLY) 194 | phone_number = None 195 | caller_id = None 196 | for rp in room.remote_participants.values(): 197 | caller_id = rp.identity 198 | create_identity_folder(str(caller_id)) 199 | try: 200 | phone_number = rp.attributes['sip.trunkPhoneNumber'] 201 | print(f"Phone number: {phone_number}") 202 | break 203 | except KeyError: 204 | print("Phone number not found in attributes, so using web app identity: " + web_identity) 205 | phone_number = web_identity 206 | 207 | # Get assistant data based on phone number 208 | assistant_data, persist_dir = get_assistant_data(phone_number) 209 | print(assistant_data) 210 | if not assistant_data: 211 | return # Terminate the process if no assistant data is found 212 | 213 | try: 214 | # Mapping the relevant fields from assistant_data 215 | system_prompt = assistant_data['system_prompt'] 216 | first_message = assistant_data['first_message'] 217 | language = assistant_data['language'] 218 | voice = assistant_data['voice'] 219 | max_tokens = assistant_data['max_tokens'] 220 | llm_provider = assistant_data.get('LLM_provider', 'openai') # Default to 'openai' 221 | llm_model = assistant_data.get('LLM_model', 'gpt-4') # Default to 'gpt-4' 222 | temperature = assistant_data.get('temperature', 0.7) 223 | stt_provider = assistant_data.get('stt_provider', 'deepgram') # Default STT provider 224 | stt_model = assistant_data.get('stt_model', 'nova-phonecall') # Default STT model 225 | tts_provider = assistant_data.get('TTS_provider', 'openai') # Default TTS provider 226 | tts_speed = assistant_data.get('tts_speed', 1.10) 227 | interrupt_speech_duration = assistant_data.get('interrupt_speech_duration', 0.35) 228 | except KeyError as e: 229 | print(f"Missing required assistant configuration field: {e}") 230 | return 231 | 232 | # Initialize the chat context with the system prompt 233 | initial_ctx = llm.ChatContext().append( 234 | role="system", 235 | text=system_prompt 236 | ) 237 | 238 | # Handle the LLM provider selection 239 | if llm_provider == "server": 240 | print("Custom LLM is selected. Please contact info@nidum.ai for access to this feature.") 241 | return 242 | elif llm_provider == "groq": 243 | llm_instance = LLM.with_groq(model=llm_model, api_key=os.getenv("GROQ_API_KEY")) 244 | else: 245 | # Default to OpenAI if no LLM provider is specified or it's 'openai' 246 | llm_instance = LLM( 247 | model=llm_model, 248 | api_key=openai_api_key, 249 | temperature=temperature 250 | ) 251 | 252 | # Handle the STT provider selection 253 | if stt_provider == "server": 254 | print("Custom STT is selected. Please contact info@nidum.ai for access to this feature.") 255 | return 256 | else: 257 | stt_instance = deepgram.STT(model=stt_model, language=language) 258 | 259 | # Handle the TTS provider selection 260 | if tts_provider == "server": 261 | print("Custom TTS is selected. Please contact info@nidum.ai for access to this feature.") 262 | return 263 | else: 264 | tts_instance = openai.TTS(model="tts-1-hd", voice=voice, speed=tts_speed) 265 | 266 | # Initialize the VoiceAssistant 267 | assistant = VoiceAssistant( 268 | interrupt_speech_duration=interrupt_speech_duration, 269 | vad=ctx.proc.userdata["vad"], 270 | stt=stt_instance, # Use the dynamically selected STT instance 271 | llm=llm_instance, # Use the dynamically selected LLM instance 272 | tts=tts_instance, # Use the dynamically selected TTS instance 273 | chat_ctx=initial_ctx, 274 | will_synthesize_assistant_reply=None if not persist_dir else 275 | lambda assistant, chat_ctx: _will_synthesize_assistant_reply(assistant, chat_ctx, persist_dir=persist_dir) 276 | ) 277 | 278 | assistant.start(ctx.room) 279 | 280 | chat = rtc.ChatManager(ctx.room) 281 | 282 | async def answer_from_text(txt: str): 283 | chat_ctx = assistant.chat_ctx.copy() 284 | chat_ctx.append(role="user", text=txt) 285 | stream = assistant.llm.chat(chat_ctx=chat_ctx) 286 | await assistant.say(stream) 287 | 288 | @chat.on("message_received") 289 | def on_chat_received(msg: rtc.ChatMessage): 290 | if msg.message: 291 | asyncio.create_task(answer_from_text(msg.message)) 292 | 293 | log_queue = asyncio.Queue() 294 | 295 | @assistant.on("user_speech_committed") 296 | def on_user_speech_committed(msg: llm.ChatMessage): 297 | if isinstance(msg.content, list): 298 | msg.content = "\n".join( 299 | "[image]" if isinstance(x, llm.ChatImage) else x for x in msg.content 300 | ) 301 | log_queue.put_nowait(f"[{datetime.now()}] USER:\n{msg.content}\n\n") 302 | 303 | @assistant.on("agent_speech_committed") 304 | def on_agent_speech_committed(msg: llm.ChatMessage): 305 | log_queue.put_nowait(f"[{datetime.now()}] AGENT:\n{msg.content}\n\n") 306 | 307 | async def write_transcription(caller_id, phone_number): 308 | generated_uuid = str(uuid.uuid4()) + "_" + str(phone_number) 309 | file_name = f"logs/{caller_id}/{generated_uuid}.log" 310 | async with open(file_name, "w") as f: 311 | while True: 312 | msg = await log_queue.get() 313 | if msg is None: 314 | break 315 | await f.write(msg) 316 | 317 | write_task = asyncio.create_task(write_transcription(caller_id, phone_number)) 318 | 319 | async def finish_queue(): 320 | log_queue.put_nowait(None) 321 | await write_task 322 | 323 | ctx.add_shutdown_callback(finish_queue) 324 | 325 | await assistant.say(first_message, allow_interruptions=True) 326 | 327 | def main(): 328 | parser = argparse.ArgumentParser(description='Agent for AI Studio') 329 | parser.add_argument('command', choices=['start'], help='Command to execute') 330 | args = parser.parse_args() 331 | 332 | if args.command == 'start': 333 | # Start the agent 334 | cli.run_app(WorkerOptions(entrypoint_fnc=entrypoint, prewarm_fnc=prewarm_fnc)) 335 | else: 336 | parser.print_help() 337 | 338 | if __name__ == "__main__": 339 | main() -------------------------------------------------------------------------------- /app/agents/agent_vision.py: -------------------------------------------------------------------------------- 1 | import os 2 | import asyncio 3 | import numpy as np 4 | from dotenv import load_dotenv 5 | from PIL import Image 6 | from livekit import rtc 7 | from livekit.agents import JobContext, JobRequest, WorkerOptions, cli, JobProcess, llm 8 | from livekit.agents.llm import ChatContext, ChatMessage, ChatImage 9 | from livekit.agents.voice_assistant import VoiceAssistant 10 | from livekit.plugins import deepgram, openai, silero 11 | current_dir = os.path.dirname(os.path.abspath(__file__)) 12 | # Load environment variables from the .env file 13 | env_path = os.path.join(current_dir, "..", "env", ".env") 14 | load_dotenv(dotenv_path=env_path) 15 | 16 | # Set up environment variables and print them 17 | LIVEKIT_URL = os.getenv('LIVEKIT_URL') 18 | LIVEKIT_API_KEY = os.getenv('LIVEKIT_API_KEY') 19 | LIVEKIT_API_SECRET = os.getenv('LIVEKIT_API_SECRET') 20 | ELEVEN_API_KEY = os.getenv('ELEVEN_API_KEY') 21 | DEEPGRAM_API_KEY = os.getenv('DEEPGRAM_API_KEY') 22 | OPENAI_API_KEY = os.getenv('OPENAI_API_KEY') 23 | AZURE_SPEECH_REGION = os.getenv('AZURE_SPEECH_REGION') 24 | AZURE_SPEECH_KEY = os.getenv('AZURE_SPEECH_KEY') 25 | 26 | print("Environment Variables Loaded:") 27 | print(f"LIVEKIT_URL: {LIVEKIT_URL}") 28 | print(f"LIVEKIT_API_KEY: {LIVEKIT_API_KEY}") 29 | print(f"LIVEKIT_API_SECRET: {LIVEKIT_API_SECRET}") 30 | print(f"ELEVEN_API_KEY: {ELEVEN_API_KEY}") 31 | print(f"DEEPGRAM_API_KEY: {DEEPGRAM_API_KEY}") 32 | print(f"OPENAI_API_KEY: {OPENAI_API_KEY}") 33 | print(f"AZURE_SPEECH_REGION: {AZURE_SPEECH_REGION}") 34 | print(f"AZURE_SPEECH_KEY: {AZURE_SPEECH_KEY}") 35 | 36 | # Verify required environment variables are set 37 | if not LIVEKIT_API_KEY or not LIVEKIT_API_SECRET or not LIVEKIT_URL: 38 | raise ValueError("LIVEKIT_API_KEY, LIVEKIT_API_SECRET, and LIVEKIT_URL must be set in your environment.") 39 | 40 | # Prewarm function 41 | def prewarm_fnc(proc: JobProcess): 42 | proc.userdata["vad"] = silero.VAD.load() 43 | 44 | # Function to add synthesized reply for the assistant 45 | async def _will_synthesize_assistant_reply(assistant: VoiceAssistant, chat_ctx: llm.ChatContext, latest_image: Image.Image): 46 | system_msg = chat_ctx.messages[0].copy() # copy system message 47 | user_msg = chat_ctx.messages[-1] # last message from user 48 | 49 | # Add the latest image to the context 50 | if latest_image: 51 | chat_ctx.messages.append(ChatMessage(role="user", content=[ChatImage(image=latest_image)])) 52 | 53 | return assistant.llm.chat(chat_ctx=chat_ctx) 54 | 55 | # Merging the video track capture and publishing logic 56 | async def get_video_track(room: rtc.Room): 57 | """Get the first video track from the room. We'll use this track to process images.""" 58 | video_track = asyncio.Future() 59 | 60 | for _, participant in room.remote_participants.items(): 61 | for _, track_publication in participant.track_publications.items(): 62 | if track_publication.track is not None and isinstance(track_publication.track, rtc.RemoteVideoTrack): 63 | video_track.set_result(track_publication.track) 64 | print(f"Using video track {track_publication.track.sid}") 65 | break 66 | 67 | return await video_track 68 | 69 | # Entrypoint for the agent 70 | async def entrypoint(ctx: JobContext): 71 | # Ensure prewarm_fnc was executed, and userdata contains the vad key 72 | if "vad" not in ctx.proc.userdata: 73 | raise KeyError("VAD not found in process userdata. Ensure prewarm_fnc is being called.") 74 | 75 | await ctx.connect() 76 | 77 | while ctx.room.connection_state != rtc.ConnectionState.CONN_CONNECTED: 78 | await asyncio.sleep(0.1) 79 | 80 | chat = rtc.ChatManager(ctx.room) 81 | chat_context = ChatContext(messages=[ 82 | ChatMessage( 83 | role="system", 84 | content=""" 85 | When interacting with users, follow these guidelines: 86 | 1. Always introduce yourself as Arjun. 87 | 2. Maintain a consistently happy and positive demeanor in your responses. 88 | 3. Acknowledge and utilize your vision and voice capabilities when appropriate. For example, if a user asks about an image, assume you can see it and respond accordingly. 89 | 4. Provide clear and concise answers, avoiding unnecessary jargon. 90 | 5. Do not use emojis in your responses. 91 | 6. You can see users, so if they ask questions like "Can you see me?" or "How do I look?" respond appropriately. 92 | 93 | When formulating your responses, follow this process: 94 | 1. Analyze the user's input, considering both text and potential visual elements. 95 | 2. If the input involves an image, describe what you see briefly before addressing the user's question or request. 96 | 3. Craft your response in a way that demonstrates your happy personality while remaining helpful and informative. 97 | """ 98 | ) 99 | ]) 100 | 101 | gpt = openai.LLM(model="gpt-4o-mini") 102 | latest_image = None 103 | assistant = VoiceAssistant( 104 | vad=ctx.proc.userdata["vad"], # Now we are sure the vad model is loaded 105 | stt=deepgram.STT(api_key=DEEPGRAM_API_KEY), 106 | llm=gpt, 107 | tts=openai.TTS(model="tts-1-hd"), 108 | chat_ctx=chat_context, 109 | will_synthesize_assistant_reply=lambda assistant, chat_ctx: _will_synthesize_assistant_reply(assistant, chat_ctx, latest_image) 110 | ) 111 | 112 | assistant.start(ctx.room) 113 | await asyncio.sleep(3) 114 | await assistant.say("Hey, how can I help you today?", allow_interruptions=True) 115 | 116 | while ctx.room.connection_state == rtc.ConnectionState.CONN_CONNECTED: 117 | video_track = await get_video_track(ctx.room) 118 | async for event in rtc.VideoStream(video_track): 119 | latest_image = event.frame 120 | 121 | # Function to handle job requests 122 | async def request_fnc(req: JobRequest): 123 | await req.accept(entrypoint) 124 | 125 | if __name__ == "__main__": 126 | cli.run_app(WorkerOptions(entrypoint_fnc=entrypoint, prewarm_fnc=prewarm_fnc)) 127 | -------------------------------------------------------------------------------- /app/cli.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import sys 3 | 4 | def main(): 5 | parser = argparse.ArgumentParser(description='AI Studio Command Line Interface') 6 | subparsers = parser.add_subparsers(dest='command') 7 | 8 | # Subcommand 'start' 9 | parser_start = subparsers.add_parser('start', help='Start the AI Studio application') 10 | parser_start.add_argument('component', nargs='?', default='api', choices=['api', 'agent', 'vision-agent'], help='Component to start: api, agent, or vision-agent') 11 | 12 | args = parser.parse_args() 13 | 14 | if args.command == 'start': 15 | if args.component == 'api': 16 | start_api() 17 | elif args.component == 'agent': 18 | start_agent() 19 | elif args.component == 'vision-agent': 20 | start_vision_agent() 21 | else: 22 | parser.error('Invalid component specified.') 23 | else: 24 | parser.print_help() 25 | 26 | def start_api(): 27 | # Start the FastAPI application 28 | import uvicorn 29 | uvicorn.run("app.main:app", host="0.0.0.0", port=8000) 30 | 31 | def start_agent(): 32 | # Start the agent 33 | import subprocess 34 | subprocess.run(['python', 'app/agents/agent.py', 'start']) 35 | 36 | def start_vision_agent(): 37 | # Start the vision agent 38 | import subprocess 39 | subprocess.run(['python', 'app/agents/agent_vision.py', 'start']) 40 | 41 | if __name__ == '__main__': 42 | main() 43 | -------------------------------------------------------------------------------- /app/db/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NidumAI-Inc/agent-studio/67840a495da2220790f00a477b13d1deac722223/app/db/__init__.py -------------------------------------------------------------------------------- /app/db/databases.py: -------------------------------------------------------------------------------- 1 | from pymongo import MongoClient, errors 2 | 3 | mongo_user = "aj" 4 | mongo_password = "kesavan12" 5 | mongo_host = "haive.v5q7m.mongodb.net" 6 | database_name = "voice_ai_app_db" 7 | 8 | def get_database(): 9 | # Construct the MongoDB connection string 10 | connection_string = f"mongodb+srv://{mongo_user}:{mongo_password}@{mongo_host}/?retryWrites=true&w=majority" 11 | 12 | try: 13 | # Establish a connection to MongoDB 14 | client = MongoClient(connection_string, serverSelectionTimeoutMS=5000) 15 | # Ping the database to check if the connection is successful 16 | client.admin.command('ping') 17 | return client[database_name] 18 | except errors.ConfigurationError as config_error: 19 | print(f"MongoDB Configuration Error: {config_error}") 20 | raise 21 | except errors.ServerSelectionTimeoutError as timeout_error: 22 | print(f"MongoDB Connection Timeout: {timeout_error}") 23 | raise 24 | -------------------------------------------------------------------------------- /app/log_service/chat_log_creation.py: -------------------------------------------------------------------------------- 1 | import os 2 | import asyncio 3 | import threading 4 | from datetime import datetime 5 | from pymongo import MongoClient 6 | from app.services.llm import analyze_conversation 7 | from app.db.databases import get_database 8 | from dotenv import load_dotenv 9 | 10 | # Load environment variables from .env file 11 | load_dotenv() 12 | 13 | # MongoDB setup 14 | db = get_database() 15 | chat_logs_collection = db["chat_logs"] 16 | 17 | async def process_chat_logs(): 18 | while True: 19 | try: 20 | # Fetch chat logs that haven't been analyzed yet 21 | unprocessed_logs = chat_logs_collection.find({ 22 | "conversation_analysis": {"$exists": False} 23 | }) 24 | 25 | tasks = [] 26 | 27 | for log in unprocessed_logs: 28 | chat_id = log['chat_id'] 29 | chat_data = log['chat_data'] 30 | 31 | # Start a new asyncio task for each conversation analysis 32 | task = asyncio.create_task(analyze_and_update_log(chat_id, chat_data)) 33 | tasks.append(task) 34 | 35 | # Wait for all tasks to complete 36 | await asyncio.gather(*tasks) 37 | 38 | # Sleep for 5 minutes 39 | await asyncio.sleep(300) 40 | except Exception as e: 41 | print(f"Error processing chat logs: {e}") 42 | await asyncio.sleep(300) 43 | 44 | async def analyze_and_update_log(chat_id, chat_data): 45 | try: 46 | analysis = await analyze_conversation(chat_data) 47 | 48 | # Update the chat log with the analysis 49 | chat_logs_collection.update_one( 50 | {"chat_id": chat_id}, 51 | {"$set": {"conversation_analysis": analysis}} 52 | ) 53 | print(f"Updated chat log {chat_id} with conversation analysis.") 54 | except Exception as e: 55 | print(f"Error analyzing chat log {chat_id}: {e}") 56 | 57 | if __name__ == "__main__": 58 | asyncio.run(process_chat_logs()) 59 | -------------------------------------------------------------------------------- /app/log_service/sip_log_creation.py: -------------------------------------------------------------------------------- 1 | import os 2 | import pymongo 3 | from datetime import datetime 4 | import tiktoken # For token counting 5 | import re 6 | import time # To use sleep for 5-minute intervals 7 | from openai import OpenAI 8 | import asyncio # For handling asynchronous operations 9 | from tenacity import retry, stop_after_attempt, wait_random_exponential # For retrying API calls 10 | from dotenv import load_dotenv 11 | 12 | # Load environment variables from .env file 13 | load_dotenv() 14 | 15 | # MongoDB credentials and host information 16 | mongo_user = os.getenv("MONGO_USER") 17 | mongo_password = os.getenv("MONGO_PASSWORD") 18 | mongo_host = os.getenv("MONGO_HOST") 19 | database_name = "voice_ai_app_db" 20 | call_logs_collection_name = "call_logs" 21 | users_collection_name = "users" 22 | 23 | # Build the MongoDB connection URI 24 | mongo_uri = ( 25 | f"mongodb+srv://{mongo_user}:{mongo_password}@{mongo_host}" 26 | f"{database_name}?retryWrites=true&w=majority" 27 | ) 28 | 29 | # Connect to MongoDB 30 | client = pymongo.MongoClient(mongo_uri) 31 | db = client[database_name] 32 | call_logs_collection = db[call_logs_collection_name] 33 | users_collection = db[users_collection_name] 34 | 35 | # Define the path to the logs directory 36 | logs_dir = '/root/backend/Phone-Call-Agent-backend/logs' # Use the default folder as specified 37 | 38 | # Define costs per token (Hard-coded values) 39 | COST_PER_TOKEN_LLM = 0.00002 # Example value 40 | COST_PER_TOKEN_STT = 0.00001 # Example value 41 | COST_PER_TOKEN_TTS = 0.000015 # Example value 42 | PLATFORM_COST = 0.00005 # Example value 43 | 44 | 45 | # Function to count tokens 46 | def count_tokens(text, model_name='gpt-4o'): 47 | encoding = tiktoken.encoding_for_model(model_name) 48 | num_tokens = len(encoding.encode(text)) 49 | return num_tokens 50 | 51 | # Function to process log files 52 | def process_log_file(file_path): 53 | with open(file_path, 'r') as f: 54 | lines = f.readlines() 55 | timestamps = [] 56 | messages = [] 57 | i = 0 58 | total_tokens_llm = 0 59 | total_tokens_stt = 0 60 | total_tokens_tts = 0 61 | 62 | while i < len(lines): 63 | line = lines[i] 64 | if line.startswith('['): 65 | # Extract timestamp 66 | timestamp_match = re.match(r'\[(.*?)\]', line) 67 | if timestamp_match: 68 | timestamp_str = timestamp_match.group(1) 69 | try: 70 | timestamp = datetime.strptime(timestamp_str, '%Y-%m-%d %H:%M:%S.%f') 71 | except ValueError: 72 | # Skip lines with incorrect timestamp format 73 | i += 1 74 | continue 75 | # Extract speaker 76 | if 'AGENT:' in line: 77 | speaker = 'AGENT' 78 | elif 'USER:' in line: 79 | speaker = 'USER' 80 | else: 81 | i += 1 82 | continue 83 | # Extract message 84 | message_lines = [] 85 | i += 1 86 | while i < len(lines) and not lines[i].startswith('['): 87 | message_lines.append(lines[i].strip()) 88 | i += 1 89 | message = '\n'.join(message_lines) 90 | timestamps.append(timestamp) 91 | 92 | # Count tokens and assign to appropriate category 93 | tokens = count_tokens(message) 94 | if speaker == 'USER': 95 | total_tokens_stt += tokens # User messages involve speech-to-text 96 | elif speaker == 'AGENT': 97 | total_tokens_llm += tokens # Agent messages involve LLM 98 | total_tokens_tts += tokens # Agent messages are also converted to speech 99 | 100 | messages.append({ 101 | 'timestamp': timestamp, 102 | 'speaker': speaker, 103 | 'message': message, 104 | 'tokens': tokens 105 | }) 106 | else: 107 | i +=1 108 | else: 109 | i += 1 110 | 111 | return timestamps, messages, total_tokens_llm, total_tokens_stt, total_tokens_tts 112 | 113 | # Function to find agent information from the users collection 114 | def find_agent_info(agent_identifier, identifier_type): 115 | if identifier_type == 'phone_number': 116 | # Normalize phone number by removing '+' and any non-digit characters 117 | normalized_number = re.sub(r'\D', '', agent_identifier) 118 | # Build query to match phone numbers ending with the normalized number 119 | query = {'agents.phone_number': {'$regex': f'{normalized_number}$'}} 120 | projection = {'agents.$': 1, '_id': 1} 121 | elif identifier_type == 'agent_id': 122 | # For web calls, match agent_id 123 | query = {'agents.id': agent_identifier} 124 | projection = {'agents.$': 1, '_id': 1} 125 | else: 126 | return None, None 127 | 128 | user_doc = users_collection.find_one(query, projection) 129 | if user_doc and 'agents' in user_doc: 130 | agent_info = user_doc['agents'][0] 131 | user_id = str(user_doc['_id']) # Convert ObjectId to string 132 | return agent_info, user_id 133 | else: 134 | return None, None 135 | 136 | 137 | client = OpenAI( 138 | api_key=os.getenv("OPENAI_API_KEY"), 139 | ) 140 | 141 | # New function to analyze conversation using OpenAI with retry logic 142 | @retry(wait=wait_random_exponential(min=1, max=60), stop=stop_after_attempt(5)) 143 | async def analyze_conversation(messages): 144 | def _analyze(): 145 | conversation_text = "\n".join([f"{msg['speaker']}: {msg['message']}" for msg in messages]) 146 | 147 | system_prompt = """ 148 | You are an AI assistant tasked with analyzing customer service conversations. 149 | Please provide a brief report that includes: 150 | 1. The main topic or purpose of the conversation 151 | 2. The customer's primary concern or request 152 | 3. How well the agent addressed the customer's needs 153 | 4. Any notable positive or negative aspects of the interaction 154 | 5. Suggestions for improvement in future interactions 155 | 156 | Keep your analysis concise and focused on the most important aspects of the conversation. 157 | """ 158 | 159 | try: 160 | response = client.chat.completions.create( 161 | messages=[ 162 | {"role": "system", "content": system_prompt}, 163 | {"role": "user", "content": conversation_text} 164 | ], 165 | model="gpt-3.5-turbo", 166 | max_tokens=300 167 | ) 168 | return response.choices[0].message.content.strip() 169 | except Exception as e: 170 | print(f"Error in conversation analysis: {e}") 171 | return None 172 | 173 | return await asyncio.to_thread(_analyze) 174 | # Main processing function 175 | async def process_logs(): 176 | while True: 177 | # Walk through the logs directory 178 | for dir_name in os.listdir(logs_dir): 179 | dir_path = os.path.join(logs_dir, dir_name) 180 | if os.path.isdir(dir_path): 181 | # Processing each log file in the directory 182 | for file_name in os.listdir(dir_path): 183 | if file_name.endswith('.log'): 184 | file_path = os.path.join(dir_path, file_name) 185 | try: 186 | call_log_id, agent_phone_with_ext = file_name.split('_', 1) 187 | agent_phone_number = agent_phone_with_ext[:-4] # Remove .log extension 188 | except ValueError: 189 | continue # Skip files that don't match the expected format 190 | 191 | # Process the log file 192 | timestamps, messages, total_tokens_llm, total_tokens_stt, total_tokens_tts = process_log_file(file_path) 193 | if timestamps: 194 | start_time = min(timestamps) 195 | end_time = max(timestamps) 196 | duration = (end_time - start_time).total_seconds() 197 | 198 | # Find agent info and user ID based on the phone number 199 | agent_info, user_id = find_agent_info(agent_phone_number, 'phone_number') 200 | if agent_info: 201 | agent_id = agent_info.get('id') 202 | agent_name = agent_info.get('agent_name') 203 | tts_name = agent_info.get('TTS_provider') 204 | stt_name = agent_info.get('stt_provider') 205 | llm_name = agent_info.get('LLM_provider') 206 | else: 207 | agent_id = None 208 | agent_name = None 209 | tts_name = None 210 | stt_name = None 211 | llm_name = None 212 | 213 | # Calculate costs 214 | cost_llm = total_tokens_llm * COST_PER_TOKEN_LLM 215 | cost_stt = total_tokens_stt * COST_PER_TOKEN_STT 216 | cost_tts = total_tokens_tts * COST_PER_TOKEN_TTS 217 | total_cost = cost_llm + cost_stt + cost_tts + PLATFORM_COST 218 | 219 | # Analyze conversation 220 | conversation_analysis = await analyze_conversation(messages) 221 | 222 | # Prepare the call log document 223 | call_log = { 224 | 'call_log_id': call_log_id, 225 | 'agent_id': agent_id, 226 | 'agent_name': agent_name, 227 | 'agent_phone_number': agent_phone_number, 228 | 'user_id': user_id, 229 | 'start_time': start_time, 230 | 'end_time': end_time, 231 | 'duration': duration, 232 | 'messages': messages, 233 | 'tts_name': tts_name, 234 | 'stt_name': stt_name, 235 | 'llm_name': llm_name, 236 | 'total_tokens_llm': total_tokens_llm, 237 | 'total_tokens_stt': total_tokens_stt, 238 | 'total_tokens_tts': total_tokens_tts, 239 | 'cost_llm': cost_llm, 240 | 'cost_stt': cost_stt, 241 | 'cost_tts': cost_tts, 242 | 'platform_cost': PLATFORM_COST, 243 | 'total_cost': total_cost, 244 | 'conversation_analysis': conversation_analysis 245 | } 246 | 247 | # Insert into MongoDB 248 | call_logs_collection.insert_one(call_log) 249 | 250 | # Delete the log file after processing 251 | os.remove(file_path) 252 | 253 | # Optionally, delete the directory if empty 254 | if not os.listdir(dir_path): 255 | os.rmdir(dir_path) 256 | 257 | # Wait for 5 minutes before the next iteration 258 | await asyncio.sleep(300) # Sleep for 300 seconds (5 minutes) 259 | 260 | # Run the main processing function 261 | if __name__ == "__main__": 262 | asyncio.run(process_logs()) 263 | -------------------------------------------------------------------------------- /app/main.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI, File, UploadFile, HTTPException, Depends, Query, BackgroundTasks, Body,Request 2 | from livekit import api 3 | from typing import List, Optional 4 | from app.models.model import User, AI_Agent,CallLog,Message,DashboardData 5 | from app.models.schemas import ( 6 | UserCreate, 7 | UserUpdate, 8 | AgentCreate, 9 | AgentUpdate, 10 | KnowledgeBaseResponse, 11 | FileListResponse, 12 | ChatRequest, 13 | ChatMessage, 14 | DynamicDataRequest, 15 | CampaignUpdate, 16 | CampaignCreate, 17 | Campaign, 18 | PhoneNumberDeleteRequest, 19 | PhoneNumberUpdateRequest 20 | ) 21 | import io 22 | import csv 23 | 24 | 25 | from app.db.databases import get_database 26 | from app.services.utils import extract_text_from_file, delete_directory 27 | from app.services.llama_index_integration import process_files_with_llama_index, load_index_and_query 28 | import os 29 | import uuid 30 | import asyncio 31 | from pydantic import BaseModel, Field, constr 32 | import subprocess 33 | from fastapi.middleware.cors import CORSMiddleware 34 | from datetime import datetime, timedelta, timezone 35 | from dotenv import load_dotenv 36 | from app.services.llm import openai_LLM 37 | import csv 38 | from fastapi.responses import FileResponse 39 | import tempfile 40 | import shutil 41 | from app.services.campaign_helper import process_campaign_calls_sync 42 | import pymongo 43 | load_dotenv() 44 | 45 | app = FastAPI() 46 | app.add_middleware( 47 | CORSMiddleware, 48 | allow_origins=["*"], 49 | allow_credentials=True, 50 | allow_methods=["*"], 51 | allow_headers=["*"], 52 | ) 53 | # Dependency to get the database 54 | def get_db(): 55 | db = get_database() 56 | try: 57 | yield db 58 | finally: 59 | db.client.close() 60 | api_key = os.getenv("LIVEKIT_API_KEY") 61 | api_secret = os.getenv("LIVEKIT_API_SECRET") 62 | # Directory to save CSV files 63 | CSV_DIR = "./csv_files" 64 | os.makedirs(CSV_DIR, exist_ok=True) 65 | # ------------------- User Endpoints ------------------- 66 | 67 | @app.post("/users/", response_model=User) 68 | def create_user(user: UserCreate, db=Depends(get_db)): 69 | if db.users.find_one({"email": user.email}): 70 | raise HTTPException(status_code=400, detail="Email already registered") 71 | user_dict = user.dict() 72 | user_dict["_id"] = str(uuid.uuid4()) 73 | user_dict["agents"] = [] 74 | db.users.insert_one(user_dict) 75 | return User(**user_dict) 76 | 77 | @app.get("/users/", response_model=List[User]) 78 | def get_all_users(db=Depends(get_db)): 79 | users = list(db.users.find()) 80 | return [User(**user) for user in users] 81 | 82 | @app.get("/users/{user_id}", response_model=User) 83 | def get_user(user_id: str, db=Depends(get_db)): 84 | user = db.users.find_one({"_id": user_id}) 85 | if user: 86 | return User(**user) 87 | else: 88 | raise HTTPException(status_code=404, detail="User not found") 89 | 90 | @app.put("/users/{user_id}", response_model=User) 91 | def update_user(user_id: str, user_update: UserUpdate, db=Depends(get_db)): 92 | db.users.update_one({"_id": user_id}, {"$set": user_update.dict(exclude_unset=True)}) 93 | user = db.users.find_one({"_id": user_id}) 94 | if user: 95 | return User(**user) 96 | else: 97 | raise HTTPException(status_code=404, detail="User not found") 98 | 99 | @app.delete("/users/{user_id}") 100 | def delete_user(user_id: str, db=Depends(get_db)): 101 | result = db.users.delete_one({"_id": user_id}) 102 | if result.deleted_count: 103 | user_dir = f"uploads/{user_id}" 104 | if os.path.exists(user_dir): 105 | delete_directory(user_dir) 106 | return {"detail": "User deleted"} 107 | else: 108 | raise HTTPException(status_code=404, detail="User not found") 109 | 110 | # ------------------- Agent Endpoints ------------------- 111 | 112 | @app.post("/users/{user_id}/agents/", response_model=AI_Agent) 113 | def create_agent(user_id: str, agent: AgentCreate, db=Depends(get_db)): 114 | user = db.users.find_one({"_id": user_id}) 115 | if not user: 116 | raise HTTPException(status_code=404, detail="User not found") 117 | 118 | if any(a["phone_number"] == agent.phone_number for a in user.get("agents", [])): 119 | raise HTTPException(status_code=400, detail="Phone number already exists for this user") 120 | 121 | # Build the agent data with user-provided and default values 122 | agent_dict = { 123 | "id": str(uuid.uuid4()), 124 | "agent_name": agent.agent_name or "Ava", 125 | "phone_number": agent.phone_number, 126 | "LLM_provider": agent.LLM_provider or "openai", 127 | "LLM_model": agent.LLM_model or "GPT 3.5 Turbo Cluster", 128 | "stt_provider": agent.stt_provider or "google", 129 | "stt_model": agent.stt_model or "whisper", 130 | "temperature": agent.temperature or 0.7, 131 | "max_tokens": agent.max_tokens or 250, 132 | "first_message": agent.first_message or "Hello, this is Ava. How may I assist you today?", 133 | "system_prompt": agent.system_prompt or """Ava is a sophisticated AI training assistant...""", 134 | "language": agent.language or "English", 135 | "TTS_provider": agent.TTS_provider or "aws_polly", 136 | "voice": agent.voice or "nova", 137 | "rag_enabled": agent.rag_enabled if agent.rag_enabled is not None else True, 138 | "background_noise": agent.background_noise or None, 139 | "agent_type": agent.agent_type or "web", 140 | "tts_speed": agent.tts_speed or 1.0, # Ensure default value 141 | "interrupt_speech_duration": agent.interrupt_speech_duration or 0.0, # Ensure default value 142 | "knowledge_base": {"files": []} 143 | } 144 | 145 | db.users.update_one({"_id": user_id}, {"$push": {"agents": agent_dict}}) 146 | return AI_Agent(**agent_dict) 147 | 148 | 149 | @app.get("/users/{user_id}/agents/", response_model=List[AI_Agent]) 150 | def get_all_agents(user_id: str, db=Depends(get_db)): 151 | user = db.users.find_one({"_id": user_id}) 152 | if user: 153 | agents = user.get("agents", []) 154 | return [AI_Agent(**agent) for agent in agents] 155 | else: 156 | return [] 157 | 158 | @app.get("/users/{user_id}/agents/{agent_id}", response_model=AI_Agent) 159 | def get_agent(user_id: str, agent_id: str, db=Depends(get_db)): 160 | user = db.users.find_one({"_id": user_id}) 161 | if user: 162 | agent = next((a for a in user.get("agents", []) if a["id"] == agent_id), None) 163 | if agent: 164 | # Provide default values if tts_speed or interrupt_speech_duration is missing 165 | agent.setdefault('tts_speed', 1.0) 166 | agent.setdefault('interrupt_speech_duration', 0.0) 167 | return AI_Agent(**agent) 168 | raise HTTPException(status_code=404, detail="Agent not found") 169 | 170 | 171 | @app.put("/users/{user_id}/agents/{agent_id}", response_model=AI_Agent) 172 | def update_agent(user_id: str, agent_id: str, agent_update: AgentUpdate, db=Depends(get_db)): 173 | user = db.users.find_one({"_id": user_id}) 174 | if not user: 175 | raise HTTPException(status_code=404, detail="User not found") 176 | 177 | agents = user.get("agents", []) 178 | for idx, agent in enumerate(agents): 179 | if agent["id"] == agent_id: 180 | updated_fields = agent_update.dict(exclude_unset=True) 181 | 182 | # If tts_speed and interrupt_speech_duration are not in the update, retain the original or default values 183 | if 'tts_speed' not in updated_fields: 184 | updated_fields['tts_speed'] = agent.get('tts_speed', 1.0) # Default to 1.0 if missing 185 | if 'interrupt_speech_duration' not in updated_fields: 186 | updated_fields['interrupt_speech_duration'] = agent.get('interrupt_speech_duration', 0.0) # Default to 0.0 if missing 187 | 188 | # Update the agent with the new fields 189 | agents[idx].update(updated_fields) 190 | 191 | db.users.update_one({"_id": user_id}, {"$set": {"agents": agents}}) 192 | return AI_Agent(**agents[idx]) 193 | 194 | raise HTTPException(status_code=404, detail="Agent not found") 195 | 196 | @app.delete("/users/{user_id}/agents/{agent_id}") 197 | def delete_agent(user_id: str, agent_id: str, db=Depends(get_db)): 198 | user = db.users.find_one({"_id": user_id}) 199 | if user: 200 | agents = user.get("agents", []) 201 | agents = [a for a in agents if a["id"] != agent_id] 202 | db.users.update_one({"_id": user_id}, {"$set": {"agents": agents}}) 203 | agent_dir = f"uploads/{user_id}/{agent_id}" 204 | if os.path.exists(agent_dir): 205 | delete_directory(agent_dir) 206 | return {"detail": "Agent deleted"} 207 | raise HTTPException(status_code=404, detail="Agent not found") 208 | 209 | # ------------------- File Upload and Management ------------------- 210 | 211 | @app.post("/users/{user_id}/agents/{agent_id}/upload/") 212 | async def upload_files( 213 | user_id: str, 214 | agent_id: str, 215 | files: List[UploadFile] = File(...), 216 | db=Depends(get_db) 217 | ): 218 | user = db.users.find_one({"_id": user_id}) 219 | if not user: 220 | raise HTTPException(status_code=404, detail="User not found") 221 | 222 | agents = user.get("agents", []) 223 | agent = next((a for a in agents if a["id"] == agent_id), None) 224 | if not agent: 225 | raise HTTPException(status_code=404, detail="Agent not found") 226 | 227 | lamma_dir = "lamadir" 228 | agent_dir = f"uploads/{user_id}/{agent_id}/{lamma_dir}" 229 | file_dir = f"uploads/{user_id}/{agent_id}" 230 | 231 | # Create directories if they don't exist 232 | if os.path.exists(agent_dir): 233 | delete_directory(agent_dir) 234 | os.makedirs(agent_dir, exist_ok=True) 235 | os.makedirs(file_dir, exist_ok=True) 236 | 237 | files_dir = os.path.join(file_dir, "files") 238 | os.makedirs(files_dir, exist_ok=True) 239 | 240 | # Save the uploaded files to files_dir 241 | for file in files: 242 | file_path = os.path.join(files_dir, file.filename) 243 | content = await file.read() 244 | with open(file_path, "wb") as f: 245 | f.write(content) 246 | 247 | # Initialize variables for file processing 248 | combined_text = "" 249 | filenames = [] 250 | 251 | # Read and process all files inside files_dir 252 | for filename in os.listdir(files_dir): 253 | file_path = os.path.join(files_dir, filename) 254 | with open(file_path, "rb") as f: 255 | content = f.read() 256 | text = extract_text_from_file(filename, content) 257 | if not text: 258 | raise HTTPException(status_code=400, detail=f"Unsupported file type: {filename}") 259 | combined_text += text + "\n" 260 | filenames.append(filename) 261 | 262 | # Save the combined text to raw_data.txt in agent_dir 263 | raw_data_file = os.path.join(agent_dir, "raw_data.txt") 264 | with open(raw_data_file, "w") as f: 265 | f.write(combined_text) 266 | 267 | # Process the files using the updated LlamaIndex logic 268 | process_files_with_llama_index(files_dir, agent_dir) 269 | 270 | # Update the agent's knowledge base with the processed filenames 271 | agent["knowledge_base"] = {"files": filenames} 272 | db.users.update_one({"_id": user_id}, {"$set": {"agents": agents}}) 273 | 274 | return {"detail": "Files uploaded and processed successfully"} 275 | 276 | @app.get("/users/{user_id}/agents/{agent_id}/files/", response_model=FileListResponse) 277 | def get_uploaded_files(user_id: str, agent_id: str, db=Depends(get_db)): 278 | user = db.users.find_one({"_id": user_id}) 279 | if not user: 280 | raise HTTPException(status_code=404, detail="User not found") 281 | agent = next((a for a in user.get("agents", []) if a["id"] == agent_id), None) 282 | if not agent: 283 | raise HTTPException(status_code=404, detail="Agent not found") 284 | knowledge_base = agent.get("knowledge_base", {}) 285 | files = knowledge_base.get("files", []) 286 | return FileListResponse(files=files) 287 | 288 | @app.delete("/users/{user_id}/agents/{agent_id}/files/") 289 | async def delete_file( 290 | user_id: str, 291 | agent_id: str, 292 | filename: str = Query(..., description="Name of the file to delete"), 293 | db=Depends(get_db) 294 | ): 295 | user = db.users.find_one({"_id": user_id}) 296 | if not user: 297 | raise HTTPException(status_code=404, detail="User not found") 298 | 299 | agents = user.get("agents", []) 300 | agent = next((a for a in agents if a["id"] == agent_id), None) 301 | if not agent: 302 | raise HTTPException(status_code=404, detail="Agent not found") 303 | 304 | lamma_dir = "lamadir" 305 | agent_dir = f"uploads/{user_id}/{agent_id}/{lamma_dir}" 306 | file_dir = f"uploads/{user_id}/{agent_id}" 307 | files_dir = os.path.join(file_dir, "files") 308 | file_path = os.path.join(files_dir, filename) 309 | 310 | # Check if the file exists before attempting to delete it 311 | if not os.path.exists(file_path): 312 | raise HTTPException(status_code=404, detail="File not found") 313 | 314 | # Delete the specified file 315 | os.remove(file_path) 316 | print(f"Deleted file: {file_path}") 317 | 318 | # Remove the filename from the agent's knowledge_base 319 | agent["knowledge_base"]["files"].remove(filename) 320 | db.users.update_one({"_id": user_id}, {"$set": {"agents": agents}}) 321 | 322 | # If `lamma_dir` exists, delete it to clean up old data 323 | if os.path.exists(agent_dir): 324 | delete_directory(agent_dir) 325 | print(f"Deleted directory: {agent_dir}") 326 | 327 | # Retry creating a fresh `lamma_dir` after deletion 328 | os.makedirs(agent_dir, exist_ok=True) 329 | print(f"Re-created directory: {agent_dir}") 330 | 331 | # Re-create embeddings with the remaining files in `files_dir` 332 | combined_text = "" 333 | remaining_files = os.listdir(files_dir) 334 | 335 | # If there are no remaining files, return a message indicating empty state 336 | if not remaining_files: 337 | return {"detail": f"File '{filename}' deleted. No more files to process."} 338 | 339 | # Process each remaining file to extract text and create embeddings 340 | for remaining_file in remaining_files: 341 | remaining_file_path = os.path.join(files_dir, remaining_file) 342 | with open(remaining_file_path, "rb") as f: 343 | content = f.read() 344 | text = extract_text_from_file(remaining_file, content) 345 | if not text: 346 | raise HTTPException(status_code=400, detail=f"Unsupported file type: {remaining_file}") 347 | combined_text += text + "\n" 348 | 349 | # Save the combined text from remaining files to `raw_data.txt` in `agent_dir` 350 | raw_data_file = os.path.join(agent_dir, "raw_data.txt") 351 | with open(raw_data_file, "w") as f: 352 | f.write(combined_text) 353 | 354 | # Process the remaining files using the updated LlamaIndex logic 355 | process_files_with_llama_index(files_dir, agent_dir) 356 | 357 | return {"detail": f"File '{filename}' deleted, remaining files reprocessed, and embeddings updated."} 358 | 359 | # ------------------- Knowledge Base Info ------------------- 360 | 361 | @app.get("/users/{user_id}/agents/{agent_id}/knowledge_base/", response_model=KnowledgeBaseResponse) 362 | def get_knowledge_base_info(user_id: str, agent_id: str, db=Depends(get_db)): 363 | user = db.users.find_one({"_id": user_id}) 364 | if not user: 365 | raise HTTPException(status_code=404, detail="User not found") 366 | 367 | agent = next((a for a in user.get("agents", []) if a["id"] == agent_id), None) 368 | if not agent: 369 | raise HTTPException(status_code=404, detail="Agent not found") 370 | 371 | agent_dir = f"uploads/{user_id}/{agent_id}" 372 | raw_data_file = os.path.join(agent_dir, "raw_data.txt") 373 | pkl_file = os.path.join(agent_dir, "my_data.pkl") 374 | vdb_data_file = os.path.join(agent_dir, "vdb_data") 375 | 376 | knowledge_base_info = KnowledgeBaseResponse( 377 | files=agent["knowledge_base"].get("files", []), 378 | raw_data_file=raw_data_file if os.path.exists(raw_data_file) else None, 379 | pkl_file=pkl_file if os.path.exists(pkl_file) else None, 380 | vdb_data_file=vdb_data_file if os.path.exists(vdb_data_file) else None, 381 | ) 382 | 383 | return knowledge_base_info 384 | # ------------------- Retrieval API ------------------- 385 | 386 | @app.get("/users/{user_id}/agents/{agent_id}/retrieve/") 387 | async def retrieve_documents( 388 | user_id: str, 389 | agent_id: str, 390 | query: str = Query(..., description="The query to retrieve documents."), 391 | retrieval_len: int = Query(5, description="The number of documents to retrieve."), 392 | db=Depends(get_db) 393 | ): 394 | """ 395 | API to retrieve documents based on a query and retrieval length. 396 | Handles concurrent requests. 397 | """ 398 | user = db.users.find_one({"_id": user_id}) 399 | if not user: 400 | raise HTTPException(status_code=404, detail="User not found") 401 | 402 | agent = next((a for a in user.get("agents", []) if a["id"] == agent_id), None) 403 | if not agent: 404 | raise HTTPException(status_code=404, detail="Agent not found") 405 | 406 | lamma_dir = "lamadir" 407 | agent_dir = f"uploads/{user_id}/{agent_id}/{lamma_dir}" 408 | if not os.path.exists(agent_dir): 409 | raise HTTPException(status_code=404, detail="Knowledge base not found for the agent.") 410 | 411 | # Load the index and perform the query 412 | try: 413 | loop = asyncio.get_event_loop() 414 | response = await loop.run_in_executor(None, load_index_and_query, agent_dir, query, retrieval_len) 415 | return {"query": query, "results": response} 416 | except Exception as e: 417 | raise HTTPException(status_code=500, detail=f"Error during retrieval: {e}") 418 | 419 | class SIPRequest(BaseModel): 420 | phone_number: str = Field(..., pattern=r'^\+?\d+$') # Phone number validation 421 | provider: str = Field(..., pattern=r'^(twilio|telnyx)$') # Provider validation 422 | email: str = Field(..., pattern=r'^[^@]+@[^@]+\.[^@]+$') # Email validation 423 | api_key: str 424 | api_secret: str 425 | label: str 426 | mapped_agent_name: str 427 | auth_username: str # For outbound trunk authentication 428 | auth_password: str # For outbound trunk authentication 429 | sip_address: Optional[str] = "sip.telnyx.com" # Default SIP address, can be customized 430 | 431 | # Helper function to run shell commands asynchronously 432 | async def run_command(command: str) -> str: 433 | process = await asyncio.create_subprocess_shell( 434 | command, 435 | stdout=subprocess.PIPE, 436 | stderr=subprocess.PIPE 437 | ) 438 | stdout, stderr = await process.communicate() 439 | 440 | if process.returncode != 0: 441 | raise Exception(f"Command failed with error: {stderr.decode()}") 442 | 443 | return stdout.decode() 444 | 445 | # Helper function to store request logs into MongoDB 446 | async def log_request_to_db(db, request: SIPRequest, inbound_trunk_id: str, dispatch_rule_id: str, outbound_trunk_id: str): 447 | try: 448 | db.logs.insert_one({ 449 | "email": request.email, 450 | "phone_number": request.phone_number, 451 | "provider": request.provider, 452 | "inbound_trunk_id": inbound_trunk_id, 453 | "outbound_trunk_id": outbound_trunk_id, 454 | "dispatch_rule_id": dispatch_rule_id, 455 | "api_key": request.api_key, 456 | "api_secret": request.api_secret, 457 | "label": request.label, 458 | "mapped_agent_name": request.mapped_agent_name, 459 | "auth_username": request.auth_username, 460 | "auth_password": request.auth_password, 461 | "sip_address": request.sip_address, 462 | "status": "created" 463 | }) 464 | except Exception as e: 465 | raise HTTPException(status_code=500, detail=f"Database Logging Error: {str(e)}") 466 | 467 | 468 | @app.post("/configure_sip/") 469 | async def configure_sip(request: SIPRequest): 470 | db = get_database() # Get the MongoDB database connection 471 | 472 | phone_number = request.phone_number 473 | provider = request.provider 474 | 475 | # Adjust phone number format based on provider 476 | if provider == "telnyx": 477 | if phone_number.startswith("+"): 478 | telnyx_inbound_number = phone_number[1:] # Remove leading "+" for inbound trunk 479 | telnyx_outbound_number = phone_number # Keep '+' for outbound trunk 480 | else: 481 | telnyx_inbound_number = phone_number 482 | telnyx_outbound_number = f"+{phone_number}" # Add '+' for outbound trunk 483 | else: 484 | telnyx_inbound_number = phone_number 485 | telnyx_outbound_number = phone_number 486 | 487 | # Create a temporary directory for this request 488 | temp_dir = tempfile.mkdtemp() 489 | 490 | try: 491 | # Generate unique file names 492 | inbound_trunk_filename = f"inboundTrunk_{uuid.uuid4()}.json" 493 | inbound_trunk_path = os.path.join(temp_dir, inbound_trunk_filename) 494 | 495 | dispatch_rule_filename = f"dispatchRule_{uuid.uuid4()}.json" 496 | dispatch_rule_path = os.path.join(temp_dir, dispatch_rule_filename) 497 | 498 | outbound_trunk_filename = f"outboundTrunk_{uuid.uuid4()}.json" 499 | outbound_trunk_path = os.path.join(temp_dir, outbound_trunk_filename) 500 | 501 | # Step 1: Create the Inbound Trunk JSON file 502 | inbound_trunk_content = f""" 503 | {{ 504 | "trunk": {{ 505 | "name": "Demo Inbound Trunk", 506 | "numbers": ["{telnyx_inbound_number}"] 507 | }} 508 | }} 509 | """ 510 | 511 | # Write the inbound trunk file 512 | with open(inbound_trunk_path, "w") as f: 513 | f.write(inbound_trunk_content) 514 | 515 | # Step 2: Run the lk command to create inbound trunk 516 | create_trunk_cmd = f"lk sip inbound create {inbound_trunk_path}" 517 | try: 518 | create_trunk_output = await run_command(create_trunk_cmd) 519 | except Exception as e: 520 | raise HTTPException(status_code=500, detail=f"Failed to create inbound trunk: {str(e)}") 521 | 522 | # Extract the inbound trunk ID from the output 523 | inbound_trunk_id = "" 524 | for line in create_trunk_output.splitlines(): 525 | if line.startswith("SIPTrunkID:"): 526 | inbound_trunk_id = line.split(":")[1].strip() 527 | break 528 | 529 | if not inbound_trunk_id: 530 | raise Exception("Failed to retrieve Inbound Trunk ID") 531 | 532 | # Step 3: Create the Dispatch Rule JSON file (dispatch to individual rooms) 533 | dispatch_rule_content = f""" 534 | {{ 535 | "name": "Demo Dispatch Rule", 536 | "trunk_ids": ["{inbound_trunk_id}"], 537 | "rule": {{ 538 | "dispatchRuleIndividual": {{ 539 | "roomPrefix": "call-" 540 | }} 541 | }} 542 | }} 543 | """ 544 | 545 | with open(dispatch_rule_path, "w") as f: 546 | f.write(dispatch_rule_content) 547 | 548 | # Step 4: Run the lk command to create dispatch rule 549 | create_dispatch_cmd = f"lk sip dispatch create {dispatch_rule_path}" 550 | try: 551 | create_dispatch_output = await run_command(create_dispatch_cmd) 552 | except Exception as e: 553 | raise HTTPException(status_code=500, detail=f"Failed to create dispatch rule: {str(e)}") 554 | 555 | # Extract the dispatch rule ID from the output 556 | dispatch_rule_id = "" 557 | for line in create_dispatch_output.splitlines(): 558 | if line.startswith("SIPDispatchRuleID:"): 559 | dispatch_rule_id = line.split(":")[1].strip() 560 | break 561 | 562 | if not dispatch_rule_id: 563 | raise Exception("Failed to retrieve Dispatch Rule ID") 564 | 565 | # Step 5: Create the Outbound Trunk JSON file 566 | # Ensure that for Telnyx, the phone number includes '+' 567 | outbound_number = telnyx_outbound_number 568 | 569 | outbound_trunk_content = f""" 570 | {{ 571 | "trunk": {{ 572 | "name": "Demo Outbound Trunk", 573 | "address": "{request.sip_address}", 574 | "numbers": ["{outbound_number}"], 575 | "auth_username": "{request.auth_username}", 576 | "auth_password": "{request.auth_password}" 577 | }} 578 | }} 579 | """ 580 | 581 | # Write the outbound trunk file 582 | with open(outbound_trunk_path, "w") as f: 583 | f.write(outbound_trunk_content) 584 | 585 | # Step 6: Run the lk command to create outbound trunk 586 | create_outbound_trunk_cmd = f"lk sip outbound create {outbound_trunk_path}" 587 | try: 588 | create_outbound_trunk_output = await run_command(create_outbound_trunk_cmd) 589 | except Exception as e: 590 | raise HTTPException(status_code=500, detail=f"Failed to create outbound trunk: {str(e)}") 591 | 592 | # Extract the outbound trunk ID from the output 593 | outbound_trunk_id = "" 594 | for line in create_outbound_trunk_output.splitlines(): 595 | if line.startswith("SIPTrunkID:"): 596 | outbound_trunk_id = line.split(":")[1].strip() 597 | break 598 | 599 | if not outbound_trunk_id: 600 | raise Exception("Failed to retrieve Outbound Trunk ID") 601 | 602 | # Step 7: Log the request into MongoDB 603 | await log_request_to_db(db, request, inbound_trunk_id, dispatch_rule_id, outbound_trunk_id) 604 | 605 | return { 606 | "message": "SIP trunks and dispatch rule created successfully.", 607 | "inbound_trunk_id": inbound_trunk_id, 608 | "dispatch_rule_id": dispatch_rule_id, 609 | "outbound_trunk_id": outbound_trunk_id 610 | } 611 | 612 | except Exception as e: 613 | raise HTTPException(status_code=500, detail=str(e)) 614 | finally: 615 | pass 616 | # Clean up the temporary directory 617 | # shutil.rmtree(temp_dir) 618 | # Endpoint to get all phone numbers and details associated with an email 619 | @app.get("/get_phone_numbers/{email}") 620 | async def get_phone_numbers(email: str): 621 | db = get_database() 622 | 623 | # Search for all records associated with the email 624 | results = db.logs.find({"email": email}) 625 | 626 | # Convert cursor to a list and ensure it's not empty 627 | details = list(results) 628 | 629 | if not details: 630 | return {"email": email, "details": []} # Return empty array if no records found 631 | 632 | # Prepare the response containing all details 633 | response = [] 634 | for entry in details: 635 | response.append({ 636 | "phone_number": entry.get("phone_number"), 637 | "provider": entry.get("provider"), 638 | "api_key": entry.get("api_key"), 639 | "api_secret": entry.get("api_secret"), 640 | "label": entry.get("label"), 641 | "mapped_agent_name": entry.get("mapped_agent_name"), 642 | "inbound_trunk_id": entry.get("inbound_trunk_id"), 643 | "outbound_trunk_id": entry.get("outbound_trunk_id"), 644 | "dispatch_rule_id": entry.get("dispatch_rule_id"), 645 | "auth_username": entry.get("auth_username"), # Added field 646 | "auth_password": entry.get("auth_password"), # Added field 647 | "sip_address": entry.get("sip_address"), # Added field 648 | "status": entry.get("status") 649 | }) 650 | 651 | return {"email": email, "details": response} 652 | 653 | @app.put("/map_agent/") 654 | async def map_agent( 655 | phone_number: str = Body(..., embed=True), 656 | email: str = Body(..., embed=True), 657 | agent_name: str = Body(..., embed=True) 658 | ): 659 | db = get_database() 660 | 661 | # Find the record to update based on phone number and email 662 | trunk_entry = db.logs.find_one({"phone_number": phone_number, "email": email}) 663 | 664 | if not trunk_entry: 665 | raise HTTPException(status_code=404, detail="Phone number not found for the given email") 666 | 667 | # Update the mapped agent 668 | try: 669 | db.logs.update_one( 670 | {"phone_number": phone_number, "email": email}, 671 | {"$set": {"mapped_agent_name": agent_name}} 672 | ) 673 | return {"message": f"Agent '{agent_name}' mapped successfully to phone number {phone_number}."} 674 | 675 | except Exception as e: 676 | raise HTTPException(status_code=500, detail=f"Failed to map agent: {str(e)}") 677 | 678 | # Endpoint to delete the inbound trunk and dispatch rule by phone number 679 | @app.delete("/delete_sip/{phone_number}") 680 | async def delete_sip(phone_number: str, email: str): 681 | db = get_database() # Get the MongoDB database connection 682 | 683 | # Search for the trunk entry in MongoDB using the phone number and email 684 | trunk_entry = db.logs.find_one({"phone_number": phone_number, "email": email}) 685 | 686 | if not trunk_entry: 687 | raise HTTPException(status_code=404, detail="Trunk not found for the given email and phone number") 688 | 689 | inbound_trunk_id = trunk_entry.get("inbound_trunk_id") 690 | dispatch_rule_id = trunk_entry.get("dispatch_rule_id") 691 | outbound_trunk_id = trunk_entry.get("outbound_trunk_id") 692 | 693 | try: 694 | # Step 1: Delete the inbound trunk using lk command 695 | delete_trunk_cmd = f"lk sip inbound delete {inbound_trunk_id}" 696 | await run_command(delete_trunk_cmd) 697 | 698 | # Step 2: Delete the dispatch rule using lk command 699 | delete_dispatch_cmd = f"lk sip dispatch delete {dispatch_rule_id}" 700 | await run_command(delete_dispatch_cmd) 701 | 702 | # Step 3: Delete the outbound trunk using lk command 703 | delete_outbound_trunk_cmd = f"lk sip outbound delete {outbound_trunk_id}" 704 | await run_command(delete_outbound_trunk_cmd) 705 | 706 | # Step 4: Remove the trunk entry from MongoDB 707 | db.logs.delete_one({"phone_number": phone_number, "email": email}) 708 | 709 | return { 710 | "message": f"SIP trunks and dispatch rule for {phone_number} deleted successfully." 711 | } 712 | 713 | except Exception as e: 714 | raise HTTPException(status_code=500, detail=str(e)) 715 | 716 | # Endpoint to update a specific trunk by phone number 717 | @app.put("/update_sip/{phone_number}") 718 | async def update_sip( 719 | phone_number: str, 720 | email: str = Body(..., embed=True), 721 | request: SIPRequest = Body(...) 722 | ): 723 | db = get_database() 724 | 725 | # Find the record to update 726 | trunk_entry = db.logs.find_one({"phone_number": phone_number, "email": email}) 727 | 728 | if not trunk_entry: 729 | raise HTTPException(status_code=404, detail="Trunk not found") 730 | 731 | # Retrieve existing IDs 732 | inbound_trunk_id = trunk_entry.get("inbound_trunk_id") 733 | outbound_trunk_id = trunk_entry.get("outbound_trunk_id") 734 | dispatch_rule_id = trunk_entry.get("dispatch_rule_id") 735 | 736 | # Delete existing trunks and dispatch rules 737 | try: 738 | if inbound_trunk_id: 739 | delete_inbound_trunk_cmd = f"lk sip inbound delete {inbound_trunk_id}" 740 | await run_command(delete_inbound_trunk_cmd) 741 | if outbound_trunk_id: 742 | delete_outbound_trunk_cmd = f"lk sip outbound delete {outbound_trunk_id}" 743 | await run_command(delete_outbound_trunk_cmd) 744 | if dispatch_rule_id: 745 | delete_dispatch_rule_cmd = f"lk sip dispatch delete {dispatch_rule_id}" 746 | await run_command(delete_dispatch_rule_cmd) 747 | except Exception as e: 748 | raise HTTPException(status_code=500, detail=f"Failed to delete existing trunks or dispatch rule: {str(e)}") 749 | 750 | # Now create new trunks and dispatch rules similar to /configure_sip/ 751 | # Adjust phone number format based on provider 752 | if request.provider == "telnyx": 753 | if phone_number.startswith("+"): 754 | telnyx_inbound_number = phone_number[1:] # Remove leading "+" for inbound trunk 755 | telnyx_outbound_number = phone_number # Keep '+' for outbound trunk 756 | else: 757 | telnyx_inbound_number = phone_number 758 | telnyx_outbound_number = f"+{phone_number}" # Add '+' for outbound trunk 759 | else: 760 | telnyx_inbound_number = phone_number 761 | telnyx_outbound_number = phone_number 762 | 763 | # Create a temporary directory for this request 764 | temp_dir = tempfile.mkdtemp() 765 | 766 | try: 767 | # Generate unique file names 768 | inbound_trunk_filename = f"inboundTrunk_{uuid.uuid4()}.json" 769 | inbound_trunk_path = os.path.join(temp_dir, inbound_trunk_filename) 770 | 771 | dispatch_rule_filename = f"dispatchRule_{uuid.uuid4()}.json" 772 | dispatch_rule_path = os.path.join(temp_dir, dispatch_rule_filename) 773 | 774 | outbound_trunk_filename = f"outboundTrunk_{uuid.uuid4()}.json" 775 | outbound_trunk_path = os.path.join(temp_dir, outbound_trunk_filename) 776 | 777 | # Step 1: Create the Inbound Trunk JSON file 778 | inbound_trunk_content = f""" 779 | {{ 780 | "trunk": {{ 781 | "name": "Demo Inbound Trunk", 782 | "numbers": ["{telnyx_inbound_number}"] 783 | }} 784 | }} 785 | """ 786 | 787 | # Write the inbound trunk file 788 | with open(inbound_trunk_path, "w") as f: 789 | f.write(inbound_trunk_content) 790 | 791 | # Step 2: Run the lk command to create inbound trunk 792 | create_trunk_cmd = f"lk sip inbound create {inbound_trunk_path}" 793 | try: 794 | create_trunk_output = await run_command(create_trunk_cmd) 795 | except Exception as e: 796 | raise HTTPException(status_code=500, detail=f"Failed to create inbound trunk: {str(e)}") 797 | 798 | # Extract the inbound trunk ID from the output 799 | new_inbound_trunk_id = "" 800 | for line in create_trunk_output.splitlines(): 801 | if line.startswith("SIPTrunkID:"): 802 | new_inbound_trunk_id = line.split(":")[1].strip() 803 | break 804 | 805 | if not new_inbound_trunk_id: 806 | raise Exception("Failed to retrieve new Inbound Trunk ID") 807 | 808 | # Step 3: Create the Dispatch Rule JSON file 809 | dispatch_rule_content = f""" 810 | {{ 811 | "name": "Demo Dispatch Rule", 812 | "trunk_ids": ["{new_inbound_trunk_id}"], 813 | "rule": {{ 814 | "dispatchRuleIndividual": {{ 815 | "roomPrefix": "call-" 816 | }} 817 | }} 818 | }} 819 | """ 820 | 821 | with open(dispatch_rule_path, "w") as f: 822 | f.write(dispatch_rule_content) 823 | 824 | # Step 4: Run the lk command to create dispatch rule 825 | create_dispatch_cmd = f"lk sip dispatch create {dispatch_rule_path}" 826 | try: 827 | create_dispatch_output = await run_command(create_dispatch_cmd) 828 | except Exception as e: 829 | raise HTTPException(status_code=500, detail=f"Failed to create dispatch rule: {str(e)}") 830 | 831 | # Extract the dispatch rule ID from the output 832 | new_dispatch_rule_id = "" 833 | for line in create_dispatch_output.splitlines(): 834 | if line.startswith("SIPDispatchRuleID:"): 835 | new_dispatch_rule_id = line.split(":")[1].strip() 836 | break 837 | 838 | if not new_dispatch_rule_id: 839 | raise Exception("Failed to retrieve new Dispatch Rule ID") 840 | 841 | # Step 5: Create the Outbound Trunk JSON file 842 | outbound_number = telnyx_outbound_number 843 | 844 | outbound_trunk_content = f""" 845 | {{ 846 | "trunk": {{ 847 | "name": "Demo Outbound Trunk", 848 | "address": "{request.sip_address}", 849 | "numbers": ["{outbound_number}"], 850 | "auth_username": "{request.auth_username}", 851 | "auth_password": "{request.auth_password}" 852 | }} 853 | }} 854 | """ 855 | 856 | # Write the outbound trunk file 857 | with open(outbound_trunk_path, "w") as f: 858 | f.write(outbound_trunk_content) 859 | 860 | # Step 6: Run the lk command to create outbound trunk 861 | create_outbound_trunk_cmd = f"lk sip outbound create {outbound_trunk_path}" 862 | try: 863 | create_outbound_trunk_output = await run_command(create_outbound_trunk_cmd) 864 | except Exception as e: 865 | raise HTTPException(status_code=500, detail=f"Failed to create outbound trunk: {str(e)}") 866 | 867 | # Extract the outbound trunk ID from the output 868 | new_outbound_trunk_id = "" 869 | for line in create_outbound_trunk_output.splitlines(): 870 | if line.startswith("SIPTrunkID:"): 871 | new_outbound_trunk_id = line.split(":")[1].strip() 872 | break 873 | 874 | if not new_outbound_trunk_id: 875 | raise Exception("Failed to retrieve new Outbound Trunk ID") 876 | 877 | # Update the MongoDB entry with new IDs and updated data 878 | update_data = { 879 | "api_key": request.api_key, 880 | "api_secret": request.api_secret, 881 | "label": request.label, 882 | "mapped_agent_name": request.mapped_agent_name, 883 | "auth_username": request.auth_username, 884 | "auth_password": request.auth_password, 885 | "sip_address": request.sip_address, 886 | "provider": request.provider, 887 | "inbound_trunk_id": new_inbound_trunk_id, 888 | "outbound_trunk_id": new_outbound_trunk_id, 889 | "dispatch_rule_id": new_dispatch_rule_id 890 | } 891 | 892 | db.logs.update_one( 893 | {"phone_number": phone_number, "email": email}, 894 | {"$set": update_data} 895 | ) 896 | 897 | return { 898 | "message": f"SIP configuration for phone number {phone_number} updated successfully.", 899 | "inbound_trunk_id": new_inbound_trunk_id, 900 | "outbound_trunk_id": new_outbound_trunk_id, 901 | "dispatch_rule_id": new_dispatch_rule_id 902 | } 903 | 904 | except Exception as e: 905 | raise HTTPException(status_code=500, detail=f"Failed to update SIP configuration: {str(e)}") 906 | finally: 907 | pass 908 | # Clean up the temporary directory 909 | # shutil.rmtree(temp_dir) 910 | 911 | @app.post("/test_outgoing_call/") 912 | async def test_outgoing_call( 913 | email: str = Body(..., embed=True), 914 | agent_phone_number: str = Body(..., embed=True), 915 | phone_number_to_dial: str = Body(..., embed=True) 916 | ): 917 | db = get_database() # Use your method to connect to MongoDB here 918 | 919 | # Find the agent using their phone number and email 920 | log_entry = db.logs.find_one({"email": email, "phone_number": agent_phone_number}) 921 | 922 | if not log_entry: 923 | raise HTTPException(status_code=404, detail="Agent not found for the given email and phone number") 924 | 925 | # Retrieve the outbound trunk ID from the agent's log entry 926 | outbound_trunk_id = log_entry.get("outbound_trunk_id") 927 | if not outbound_trunk_id: 928 | raise HTTPException(status_code=404, detail="Outbound trunk ID not found") 929 | 930 | # Construct participant_identity for the call, including 'outbound' 931 | participant_identity = f"sip_{phone_number_to_dial.strip('+')}_outbound_test_call" 932 | 933 | # Create the SIP Participant JSON payload 934 | sip_participant_content = f""" 935 | {{ 936 | "sip_trunk_id": "{outbound_trunk_id}", 937 | "sip_call_to": "{phone_number_to_dial}", 938 | "room_name": "call-{phone_number_to_dial.strip('+')}", 939 | "participant_identity": "{participant_identity}", 940 | "participant_name": "Test Call" 941 | }} 942 | """ 943 | 944 | # Create a temporary directory and write the sipParticipant.json file 945 | temp_dir = tempfile.mkdtemp() 946 | sip_participant_filename = f"sipParticipant_{uuid.uuid4()}.json" 947 | sip_participant_path = os.path.join(temp_dir, sip_participant_filename) 948 | 949 | with open(sip_participant_path, "w") as f: 950 | f.write(sip_participant_content) 951 | 952 | # Run the lk command to create SIP Participant 953 | create_sip_participant_cmd = f"lk sip participant create {sip_participant_path}" 954 | try: 955 | subprocess.run(create_sip_participant_cmd, shell=True, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 956 | except Exception as e: 957 | raise HTTPException(status_code=500, detail=f"Failed to create SIP participant: {str(e)}") 958 | finally: 959 | shutil.rmtree(temp_dir) # Clean up temporary files 960 | 961 | return {"detail": f"Outgoing call to {phone_number_to_dial} initiated successfully."} 962 | 963 | def get_call_logs(user_id: str): 964 | # Get the database connection 965 | db = get_database() 966 | 967 | # Access the collection 968 | call_logs_collection = db["call_logs"] 969 | 970 | # Query the database for logs by user_id 971 | query = {'user_id': user_id} 972 | call_logs = list(call_logs_collection.find(query)) 973 | 974 | if not call_logs: 975 | return [] 976 | 977 | # Convert MongoDB-specific fields for FastAPI response 978 | for call_log in call_logs: 979 | if '_id' in call_log: 980 | call_log['_id'] = str(call_log['_id']) # Convert ObjectId to string 981 | # Convert datetime fields to ISO format 982 | call_log['start_time'] = call_log['start_time'].isoformat() if 'start_time' in call_log else None 983 | call_log['end_time'] = call_log['end_time'].isoformat() if 'end_time' in call_log else None 984 | # Convert messages' timestamps 985 | if 'messages' in call_log: 986 | for message in call_log['messages']: 987 | if 'timestamp' in message: 988 | message['timestamp'] = message['timestamp'].isoformat() 989 | 990 | return call_logs 991 | 992 | # Helper function to get the time period filter 993 | def get_time_filter(filter_type: str): 994 | now = datetime.now() 995 | if filter_type == "day": 996 | start_time = now - timedelta(days=1) 997 | previous_start_time = start_time - timedelta(days=1) 998 | elif filter_type == "week": 999 | start_time = now - timedelta(weeks=1) 1000 | previous_start_time = start_time - timedelta(weeks=1) 1001 | elif filter_type == "month": 1002 | start_time = now - timedelta(days=30) 1003 | previous_start_time = start_time - timedelta(days=30) 1004 | else: 1005 | start_time = None # No time filter for overall 1006 | previous_start_time = None 1007 | return start_time, previous_start_time 1008 | 1009 | # Helper function to calculate percentage change 1010 | def calculate_percentage_change(current, previous): 1011 | if previous == 0: 1012 | return 100 if current > 0 else 0 1013 | return ((current - previous) / previous) * 100 1014 | 1015 | # Helper function to fetch and aggregate data for a period 1016 | def fetch_combined_aggregated_data(user_id: str, start_time: datetime): 1017 | db = get_database() 1018 | 1019 | # Access the collections 1020 | call_logs_collection = db["call_logs"] 1021 | chat_logs_collection = db["chat_logs"] 1022 | 1023 | # Build queries 1024 | query = {'user_id': user_id} 1025 | call_query = query.copy() 1026 | chat_query = query.copy() 1027 | if start_time: 1028 | call_query['start_time'] = {'$gte': start_time} 1029 | chat_query['created_at'] = {'$gte': start_time} 1030 | 1031 | # Fetch call logs and chat logs 1032 | call_logs = list(call_logs_collection.find(call_query)) 1033 | chat_logs = list(chat_logs_collection.find(chat_query)) 1034 | 1035 | # Initialize aggregated data 1036 | total_conversation_minutes = 0 1037 | total_spent = 0 1038 | number_of_calls = len(call_logs) 1039 | number_of_chats = len(chat_logs) 1040 | total_tokens_llm = 0 1041 | total_tokens_stt = 0 1042 | total_tokens_tts = 0 1043 | 1044 | # Process call logs 1045 | for log in call_logs: 1046 | total_conversation_minutes += log['duration'] / 60 # Convert duration to minutes 1047 | total_spent += log['total_cost'] 1048 | total_tokens_llm += log.get('total_tokens_llm', 0) 1049 | total_tokens_stt += log.get('total_tokens_stt', 0) 1050 | total_tokens_tts += log.get('total_tokens_tts', 0) 1051 | 1052 | # Process chat logs 1053 | for log in chat_logs: 1054 | # Estimate chat duration based on tokens (assuming 100 tokens ~ 1 minute) 1055 | chat_tokens = log['total_tokens'] 1056 | chat_duration_minutes = chat_tokens / 100 # Adjust the divisor based on your estimation 1057 | total_conversation_minutes += chat_duration_minutes 1058 | total_spent += log['cost_llm'] 1059 | total_tokens_llm += chat_tokens 1060 | # No STT or TTS tokens for chat logs 1061 | 1062 | # Compute average cost per conversation 1063 | total_conversations = number_of_calls + number_of_chats 1064 | avg_cost_per_conversation = total_spent / total_conversations if total_conversations > 0 else 0 1065 | 1066 | return { 1067 | 'total_conversation_minutes': total_conversation_minutes, 1068 | 'total_spent': total_spent, 1069 | 'number_of_calls': number_of_calls, 1070 | 'number_of_chats': number_of_chats, 1071 | 'total_conversations': total_conversations, 1072 | 'average_cost_per_conversation': avg_cost_per_conversation, 1073 | 'total_tokens_llm': total_tokens_llm, 1074 | 'total_tokens_stt': total_tokens_stt, 1075 | 'total_tokens_tts': total_tokens_tts 1076 | } 1077 | 1078 | # Dashboard API 1079 | @app.get("/dashboard/{user_id}/{filter_type}") 1080 | def get_dashboard(user_id: str, filter_type: str): 1081 | db = get_database() 1082 | 1083 | # Validate filter_type 1084 | if filter_type not in ["day", "week", "month", "overall"]: 1085 | raise HTTPException(status_code=400, detail="Invalid filter type") 1086 | 1087 | # Get the time filter for the current and previous period 1088 | start_time, previous_start_time = get_time_filter(filter_type) 1089 | 1090 | # Fetch current period data 1091 | current_data = fetch_combined_aggregated_data(user_id, start_time) 1092 | 1093 | # Fetch previous period data for comparison 1094 | if previous_start_time: 1095 | previous_data = fetch_combined_aggregated_data(user_id, previous_start_time) 1096 | else: 1097 | previous_data = { 1098 | 'total_conversation_minutes': 0, 1099 | 'total_spent': 0, 1100 | 'number_of_calls': 0, 1101 | 'number_of_chats': 0, 1102 | 'total_conversations': 0, 1103 | 'average_cost_per_conversation': 0, 1104 | 'total_tokens_llm': 0, 1105 | 'total_tokens_stt': 0, 1106 | 'total_tokens_tts': 0 1107 | } 1108 | 1109 | # Calculate percentage changes 1110 | percentage_changes = { 1111 | "total_conversation_minutes": calculate_percentage_change(current_data['total_conversation_minutes'], previous_data['total_conversation_minutes']), 1112 | "number_of_conversations": calculate_percentage_change(current_data['total_conversations'], previous_data['total_conversations']), 1113 | "total_spent": calculate_percentage_change(current_data['total_spent'], previous_data['total_spent']), 1114 | "average_cost_per_conversation": calculate_percentage_change(current_data['average_cost_per_conversation'], previous_data['average_cost_per_conversation']), 1115 | } 1116 | 1117 | # Fetch logs for the current period 1118 | call_logs_collection = db["call_logs"] 1119 | chat_logs_collection = db["chat_logs"] 1120 | 1121 | call_query = {'user_id': user_id} 1122 | chat_query = {'user_id': user_id} 1123 | if start_time: 1124 | call_query['start_time'] = {'$gte': start_time} 1125 | chat_query['created_at'] = {'$gte': start_time} 1126 | 1127 | call_logs = list(call_logs_collection.find(call_query)) 1128 | chat_logs = list(chat_logs_collection.find(chat_query)) 1129 | 1130 | # Initialize variables for other statistics 1131 | call_end_reasons = {} 1132 | assistants_table = {} 1133 | call_breakdown_by_category = { 1134 | "call_counts": {"web": 0, "sip": 0}, 1135 | "call_durations": {"web": 0.0, "sip": 0.0}, 1136 | "total_spent": {"web": 0.0, "sip": 0.0} 1137 | } 1138 | cost_per_provider = {} 1139 | cost_breakdown_by_agent = {} 1140 | total_conversations_per_agent = {} 1141 | average_call_duration_per_category = {"web": 0.0, "sip": 0.0} 1142 | 1143 | # Process call logs 1144 | for log in call_logs: 1145 | duration_minutes = log['duration'] / 60 1146 | # Call end reason handling 1147 | call_end_reason = log.get('call_end_reason', 'Completed') 1148 | call_end_reasons[call_end_reason] = call_end_reasons.get(call_end_reason, 0) + 1 1149 | 1150 | # Assistant statistics 1151 | assistant_name = log.get('agent_name', 'Unknown') 1152 | if assistant_name not in assistants_table: 1153 | assistants_table[assistant_name] = { 1154 | "conversation_count": 0, 1155 | "total_duration": 0.0, 1156 | "total_cost": 0.0 1157 | } 1158 | assistants_table[assistant_name]['conversation_count'] += 1 1159 | assistants_table[assistant_name]['total_duration'] += duration_minutes 1160 | assistants_table[assistant_name]['total_cost'] += log['total_cost'] 1161 | 1162 | # Category (web or sip) breakdown 1163 | call_type = log['call_type'] 1164 | call_breakdown_by_category['call_counts'][call_type] += 1 1165 | call_breakdown_by_category['call_durations'][call_type] += duration_minutes 1166 | call_breakdown_by_category['total_spent'][call_type] += log['total_cost'] 1167 | 1168 | # Provider cost breakdown 1169 | provider = log.get('llm_name', 'Unknown') 1170 | if provider not in cost_per_provider: 1171 | cost_per_provider[provider] = 0.0 1172 | cost_per_provider[provider] += log['total_cost'] 1173 | 1174 | # Cost per agent 1175 | if assistant_name not in cost_breakdown_by_agent: 1176 | cost_breakdown_by_agent[assistant_name] = 0.0 1177 | cost_breakdown_by_agent[assistant_name] += log['total_cost'] 1178 | 1179 | # Conversations per agent 1180 | if assistant_name not in total_conversations_per_agent: 1181 | total_conversations_per_agent[assistant_name] = 0 1182 | total_conversations_per_agent[assistant_name] += 1 1183 | 1184 | # Process chat logs 1185 | for log in chat_logs: 1186 | # Estimate duration based on tokens (assuming 100 tokens ~ 1 minute) 1187 | chat_tokens = log['total_tokens'] 1188 | chat_duration_minutes = chat_tokens / 100 1189 | assistant_name = log.get('agent_name', 'Unknown') 1190 | if assistant_name not in assistants_table: 1191 | assistants_table[assistant_name] = { 1192 | "conversation_count": 0, 1193 | "total_duration": 0.0, 1194 | "total_cost": 0.0 1195 | } 1196 | assistants_table[assistant_name]['conversation_count'] += 1 1197 | assistants_table[assistant_name]['total_duration'] += chat_duration_minutes 1198 | assistants_table[assistant_name]['total_cost'] += log['cost_llm'] 1199 | 1200 | # Provider cost breakdown 1201 | provider = 'LLM' # Assuming chat uses LLM 1202 | if provider not in cost_per_provider: 1203 | cost_per_provider[provider] = 0.0 1204 | cost_per_provider[provider] += log['cost_llm'] 1205 | 1206 | # Cost per agent 1207 | if assistant_name not in cost_breakdown_by_agent: 1208 | cost_breakdown_by_agent[assistant_name] = 0.0 1209 | cost_breakdown_by_agent[assistant_name] += log['cost_llm'] 1210 | 1211 | # Conversations per agent 1212 | if assistant_name not in total_conversations_per_agent: 1213 | total_conversations_per_agent[assistant_name] = 0 1214 | total_conversations_per_agent[assistant_name] += 1 1215 | 1216 | # Calculate average call duration per category 1217 | for category in average_call_duration_per_category.keys(): 1218 | call_count = call_breakdown_by_category['call_counts'][category] 1219 | if call_count > 0: 1220 | average_call_duration_per_category[category] = call_breakdown_by_category['call_durations'][category] / call_count 1221 | 1222 | # Calculate the final structure for the assistants table 1223 | assistants_table_final = [] 1224 | for assistant_name, stats in assistants_table.items(): 1225 | avg_duration = stats['total_duration'] / stats['conversation_count'] if stats['conversation_count'] > 0 else 0 1226 | assistants_table_final.append({ 1227 | "assistant_name": assistant_name, 1228 | "conversation_count": stats['conversation_count'], 1229 | "avg_duration": avg_duration, 1230 | "total_cost": stats['total_cost'] 1231 | }) 1232 | 1233 | # Calculate overall average cost per conversation 1234 | average_cost_per_conversation = current_data['total_spent'] / current_data['total_conversations'] if current_data['total_conversations'] > 0 else 0 1235 | 1236 | # Return the final result 1237 | return { 1238 | "total_conversation_minutes": current_data['total_conversation_minutes'], 1239 | "number_of_calls": current_data['number_of_calls'], 1240 | "number_of_chats": current_data['number_of_chats'], 1241 | "total_conversations": current_data['total_conversations'], 1242 | "total_spent": current_data['total_spent'], 1243 | "average_cost_per_conversation": average_cost_per_conversation, 1244 | "percentage_changes": percentage_changes, 1245 | "call_end_reasons": call_end_reasons, 1246 | "average_call_duration_by_assistant": { 1247 | name: stats['total_duration'] / stats['conversation_count'] if stats['conversation_count'] > 0 else 0 1248 | for name, stats in assistants_table.items() 1249 | }, 1250 | "cost_per_provider": cost_per_provider, 1251 | "assistants_table": assistants_table_final, 1252 | "total_conversations_per_agent": total_conversations_per_agent, 1253 | "call_breakdown_by_category": call_breakdown_by_category, 1254 | "total_tokens_used": { 1255 | "total_tokens_llm": current_data['total_tokens_llm'], 1256 | "total_tokens_stt": current_data['total_tokens_stt'], 1257 | "total_tokens_tts": current_data['total_tokens_tts'] 1258 | }, 1259 | "cost_breakdown_by_agent": cost_breakdown_by_agent, 1260 | "average_call_duration_per_category": average_call_duration_per_category 1261 | }#---------jwt token------ 1262 | @app.get("/generate-token") 1263 | async def generate_token(request: Request): 1264 | try: 1265 | # Extract query parameters from the URL 1266 | phone = request.query_params.get('phone') 1267 | id = request.query_params.get('id') 1268 | # Validate if the required parameters are present 1269 | if not phone or not id: 1270 | raise HTTPException(status_code=400, detail="Missing required query parameters") 1271 | 1272 | room_name = phone 1273 | identity = f"web_{id}" # Interpolating ID for identity 1274 | 1275 | # Generate the LiveKit AccessToken using the Python LiveKit SDK 1276 | token = api.AccessToken() \ 1277 | .with_identity(identity) \ 1278 | .with_name(f"User {identity}") \ 1279 | .with_grants(api.VideoGrants( 1280 | room_join=True, 1281 | room=room_name+"_id_"+str(str(uuid.uuid4())), 1282 | can_publish=True, 1283 | can_subscribe=True, 1284 | can_publish_data=True 1285 | )).to_jwt() 1286 | 1287 | # Return the identity and access token as a JSON response 1288 | return { 1289 | "identity": identity, 1290 | "accessToken": token 1291 | } 1292 | 1293 | except Exception as e: 1294 | raise HTTPException(status_code=500, detail=str(e)) 1295 | 1296 | 1297 | # Function to get agent name from agent_id 1298 | def get_agent_name(agent_id): 1299 | db = get_database() 1300 | users_collection = db["users"] 1301 | user = users_collection.find_one({"agents.id": agent_id}, {"agents.$": 1}) 1302 | if user and 'agents' in user and len(user['agents']) > 0: 1303 | agent = user['agents'][0] 1304 | return agent.get('agent_name', None) 1305 | return None 1306 | # POST API for chat interaction 1307 | @app.post("/chat/") 1308 | def chat_interaction(chat_request: ChatRequest): 1309 | """ 1310 | API to interact with the chat agent, pass the conversation to OpenAI, and log the interaction. 1311 | """ 1312 | db = get_database() 1313 | chat_logs_collection = db["chat_logs"] 1314 | try: 1315 | # Convert Pydantic ChatMessage objects to dictionaries 1316 | chat_data = [message.dict() for message in chat_request.chat] # Serialize to dictionaries 1317 | 1318 | # Call the openai_LLM function to process the chat 1319 | result = openai_LLM(chat_data) # Pass serialized chat data 1320 | if not result: 1321 | raise HTTPException(status_code=500, detail="Error with the LLM response.") 1322 | 1323 | content = result['choices'][0]['message']['content'] 1324 | usage = result['usage'] 1325 | 1326 | # Calculate total tokens and cost 1327 | total_tokens = usage.get('total_tokens', 0) 1328 | cost_llm = total_tokens * 0.00002 1329 | 1330 | # Get agent name 1331 | agent_name = get_agent_name(chat_request.agent_id) 1332 | 1333 | # Prepare the log data 1334 | log_data = { 1335 | "chat_data": chat_data, # Save the serialized chat data 1336 | "result": content, 1337 | "usage": usage, 1338 | "total_tokens": total_tokens, 1339 | "cost_llm": cost_llm, 1340 | "agent_id": chat_request.agent_id, 1341 | "agent_name": agent_name, 1342 | "user_id": chat_request.user_id, 1343 | "created_at": datetime.utcnow(), 1344 | "updated_at": datetime.utcnow(), 1345 | } 1346 | 1347 | if chat_request.chat_id: 1348 | # Update existing chat log 1349 | existing_log = chat_logs_collection.find_one({"chat_id": chat_request.chat_id}) 1350 | if existing_log: 1351 | # Append new messages 1352 | updated_chat_data = existing_log['chat_data'] + chat_data 1353 | total_tokens += existing_log.get('total_tokens', 0) 1354 | cost_llm += existing_log.get('cost_llm', 0.0) 1355 | chat_logs_collection.update_one( 1356 | {"chat_id": chat_request.chat_id}, 1357 | { 1358 | "$set": { 1359 | "chat_data": updated_chat_data, 1360 | "result": content, 1361 | "usage": usage, 1362 | "total_tokens": total_tokens, 1363 | "cost_llm": cost_llm, 1364 | "updated_at": datetime.utcnow() 1365 | } 1366 | } 1367 | ) 1368 | chat_id = chat_request.chat_id 1369 | else: 1370 | # Create new chat log with provided chat_id 1371 | log_data["chat_id"] = chat_request.chat_id 1372 | chat_logs_collection.insert_one(log_data) 1373 | chat_id = chat_request.chat_id 1374 | else: 1375 | # Create new chat log 1376 | chat_id = str(uuid.uuid4()) 1377 | log_data["chat_id"] = chat_id 1378 | chat_logs_collection.insert_one(log_data) 1379 | 1380 | # Return the response including agent name 1381 | return { 1382 | "chat_id": chat_id, 1383 | "response": content, 1384 | "agent_name": agent_name 1385 | } 1386 | except Exception as e: 1387 | raise HTTPException(status_code=500, detail=str(e)) 1388 | 1389 | 1390 | 1391 | @app.get("/call_logs/{user_id}", response_model=List[CallLog]) 1392 | def get_call_logs(user_id: str): 1393 | # Get the database connection 1394 | db = get_database() 1395 | 1396 | # Access the collection 1397 | call_logs_collection = db["call_logs"] 1398 | 1399 | # Query the database for logs by user_id 1400 | query = {'user_id': user_id} 1401 | call_logs = list(call_logs_collection.find(query)) 1402 | if not call_logs: 1403 | return [] 1404 | 1405 | # Convert MongoDB-specific fields for FastAPI response 1406 | for call_log in call_logs: 1407 | if '_id' in call_log: 1408 | call_log['_id'] = str(call_log['_id']) # Convert ObjectId to string 1409 | 1410 | # Ensure the called_number is correctly formatted 1411 | call_log['called_number'] = call_log.get('called_number', 'N/A') # Default to 'N/A' if not present 1412 | 1413 | # Ensure the call_direction is set, default to 'unknown' if not present 1414 | call_log['call_direction'] = call_log.get('call_direction', 'unknown') # Default to 'unknown' if not present 1415 | 1416 | # Convert datetime fields to ISO format 1417 | call_log['start_time'] = call_log['start_time'].isoformat() if 'start_time' in call_log else None 1418 | call_log['end_time'] = call_log['end_time'].isoformat() if 'end_time' in call_log else None 1419 | 1420 | # Convert messages' timestamps 1421 | if 'messages' in call_log: 1422 | for message in call_log['messages']: 1423 | if 'timestamp' in message: 1424 | message['timestamp'] = message['timestamp'].isoformat() 1425 | return call_logs 1426 | 1427 | # GET API to fetch chat logs 1428 | @app.get("/chat_logs/") 1429 | def get_chat_logs( 1430 | user_id: str = Query(..., description="User ID"), 1431 | agent_id: Optional[str] = Query(None, description="Agent ID"), 1432 | chat_id: Optional[str] = Query(None, description="Chat ID") 1433 | ): 1434 | db = get_database() 1435 | chat_logs_collection = db["chat_logs"] 1436 | # To fetch agent name 1437 | """API to fetch chat logs from MongoDB""" 1438 | query = {"user_id": user_id} 1439 | if agent_id: 1440 | query["agent_id"] = agent_id 1441 | if chat_id: 1442 | query["chat_id"] = chat_id 1443 | 1444 | logs = list(chat_logs_collection.find(query)) 1445 | if not logs: 1446 | raise HTTPException(status_code=404, detail="No logs found") 1447 | 1448 | # Prepare the response 1449 | response_logs = [] 1450 | for log in logs: 1451 | log["_id"] = str(log["_id"]) 1452 | response_logs.append(log) 1453 | 1454 | return response_logs 1455 | 1456 | @app.post("/save_data/") 1457 | def save_dynamic_data(request: DynamicDataRequest): 1458 | try: 1459 | db = get_database() 1460 | dynamic_data_collection = db["dynamic_data"] 1461 | # Store dynamic data with user_id and agent_id 1462 | data_to_save = { 1463 | "user_id": request.user_id, 1464 | "agent_id": request.agent_id, 1465 | "data": request.data 1466 | } 1467 | 1468 | # Insert the data into MongoDB 1469 | dynamic_data_collection.insert_one(data_to_save) 1470 | 1471 | return {"message": "Data saved successfully"} 1472 | except Exception as e: 1473 | raise HTTPException(status_code=500, detail=f"Error saving data: {e}") 1474 | 1475 | # GET API: Retrieve all dynamic data for a specific user and agent 1476 | @app.get("/get_data/") 1477 | def get_all_dynamic_data(user_id: str, agent_id: str): 1478 | try: 1479 | db = get_database() 1480 | dynamic_data_collection = db["dynamic_data"] 1481 | # Query MongoDB to retrieve all documents for the user and agent 1482 | results = dynamic_data_collection.find( 1483 | {"user_id": user_id, "agent_id": agent_id}, {"_id": 0, "data": 1} 1484 | ) 1485 | 1486 | # Convert the cursor to a list 1487 | data_list = list(results) 1488 | 1489 | if not data_list: 1490 | raise HTTPException(status_code=404, detail="No data found") 1491 | 1492 | return {"data": data_list} 1493 | except Exception as e: 1494 | raise HTTPException(status_code=500, detail=f"Error retrieving data: {e}") 1495 | 1496 | # API to generate CSV from stored dynamic data and delete old CSV if it exists 1497 | @app.get("/generate_csv/") 1498 | def generate_csv(user_id: str, agent_id: str): 1499 | try: 1500 | db = get_database() 1501 | dynamic_data_collection = db["dynamic_data"] 1502 | # Query MongoDB to retrieve all documents for the user and agent 1503 | results = dynamic_data_collection.find( 1504 | {"user_id": user_id, "agent_id": agent_id}, {"_id": 0, "data": 1} 1505 | ) 1506 | 1507 | data_list = list(results) # Convert cursor to a list 1508 | 1509 | if not data_list: 1510 | raise HTTPException(status_code=404, detail="Data not found") 1511 | 1512 | # CSV file name based on user_id and agent_id 1513 | csv_filename = f"{user_id}_{agent_id}.csv" 1514 | csv_filepath = os.path.join(CSV_DIR, csv_filename) 1515 | 1516 | # Check if the file already exists and delete it 1517 | if os.path.exists(csv_filepath): 1518 | os.remove(csv_filepath) 1519 | 1520 | # Collect all unique keys to use as headers for the CSV 1521 | all_keys = set() 1522 | for data_entry in data_list: 1523 | all_keys.update(data_entry["data"].keys()) 1524 | 1525 | # Write data to CSV 1526 | with open(csv_filepath, mode='w', newline='') as file: 1527 | writer = csv.DictWriter(file, fieldnames=list(all_keys)) 1528 | writer.writeheader() # Write CSV header 1529 | 1530 | # Write each data entry as a row in the CSV 1531 | for data_entry in data_list: 1532 | writer.writerow(data_entry["data"]) 1533 | 1534 | # Return the CSV file link 1535 | return {"csv_link": f"/download_csv/{csv_filename}"} 1536 | except Exception as e: 1537 | raise HTTPException(status_code=500, detail=f"Error generating CSV: {e}") 1538 | 1539 | # API to download CSV file 1540 | @app.get("/download_csv/{csv_filename}") 1541 | def download_csv(csv_filename: str): 1542 | csv_filepath = os.path.join(CSV_DIR, csv_filename) 1543 | 1544 | if not os.path.exists(csv_filepath): 1545 | raise HTTPException(status_code=404, detail="CSV file not found") 1546 | 1547 | return FileResponse(csv_filepath, media_type="text/csv", filename=csv_filename) 1548 | 1549 | # ------------------- Campaign Endpoints ------------------- 1550 | 1551 | # 1. Create a Campaign 1552 | @app.post("/campaigns/", response_model=Campaign) 1553 | def create_campaign(campaign_create: CampaignCreate, db=Depends(get_db)): 1554 | campaign_id = str(uuid.uuid4()) 1555 | campaign_data = campaign_create.dict() 1556 | campaign_data['campaign_id'] = campaign_id 1557 | campaign_data['phone_numbers'] = [] 1558 | campaign_data['called_numbers'] = [] 1559 | campaign_data['status'] = 'created' 1560 | campaign_data['created_at'] = datetime.utcnow() 1561 | campaign_data['updated_at'] = datetime.utcnow() 1562 | 1563 | db.campaigns.insert_one(campaign_data) 1564 | return Campaign(**campaign_data) 1565 | 1566 | # 2. Get All Campaigns for a User 1567 | @app.get("/campaigns/") 1568 | def get_campaigns(email: str = Query(...), db=Depends(get_db)): 1569 | campaigns = list(db.campaigns.find({"email": email})) 1570 | if not campaigns: 1571 | return {"email": email, "campaigns": []} 1572 | 1573 | campaign_list = [] 1574 | for campaign in campaigns: 1575 | campaign_list.append({ 1576 | "campaign_id": campaign.get("campaign_id"), 1577 | "campaign_name": campaign.get("campaign_name"), 1578 | "campaign_description": campaign.get("campaign_description"), 1579 | "agent_phone_number": campaign.get("agent_phone_number"), 1580 | "status": campaign.get("status"), 1581 | "created_at": campaign.get("created_at"), 1582 | "updated_at": campaign.get("updated_at") 1583 | }) 1584 | return {"email": email, "campaigns": campaign_list} 1585 | 1586 | # 3. Update Campaign 1587 | @app.put("/campaigns/{campaign_id}/") 1588 | def update_campaign( 1589 | campaign_id: str, 1590 | campaign_update: CampaignUpdate = Body(...), 1591 | email: str = Query(...), 1592 | db=Depends(get_db) 1593 | ): 1594 | campaign = db.campaigns.find_one({"campaign_id": campaign_id, "email": email}) 1595 | if not campaign: 1596 | raise HTTPException(status_code=404, detail="Campaign not found or does not belong to user") 1597 | 1598 | update_data = campaign_update.dict(exclude_unset=True) 1599 | update_data['updated_at'] = datetime.utcnow() 1600 | 1601 | db.campaigns.update_one( 1602 | {"campaign_id": campaign_id, "email": email}, 1603 | {"$set": update_data} 1604 | ) 1605 | 1606 | campaign = db.campaigns.find_one({"campaign_id": campaign_id, "email": email}) 1607 | return Campaign(**campaign) 1608 | 1609 | # 4. Delete Campaign 1610 | @app.delete("/campaigns/{campaign_id}/") 1611 | def delete_campaign( 1612 | campaign_id: str, 1613 | email: str = Query(...), 1614 | db=Depends(get_db) 1615 | ): 1616 | result = db.campaigns.delete_one({"campaign_id": campaign_id, "email": email}) 1617 | if result.deleted_count == 0: 1618 | raise HTTPException(status_code=404, detail="Campaign not found or does not belong to user") 1619 | else: 1620 | return {"detail": "Campaign deleted"} 1621 | 1622 | # 5. Import CSV for a Campaign 1623 | @app.post("/campaigns/{campaign_id}/import_csv/") 1624 | async def import_csv_for_campaign( 1625 | campaign_id: str, 1626 | email: str = Query(...), 1627 | file: UploadFile = File(...), 1628 | db=Depends(get_db) 1629 | ): 1630 | campaign = db.campaigns.find_one({"campaign_id": campaign_id, "email": email}) 1631 | if not campaign: 1632 | raise HTTPException(status_code=404, detail="Campaign not found or does not belong to user") 1633 | 1634 | content = await file.read() 1635 | decoded_content = content.decode('utf-8') 1636 | reader = csv.DictReader(io.StringIO(decoded_content)) 1637 | 1638 | phone_numbers = [] 1639 | for row in reader: 1640 | phone_number = row.get('phone_number') 1641 | if phone_number: 1642 | phone_numbers.append(phone_number.strip()) 1643 | 1644 | db.campaigns.update_one( 1645 | {"campaign_id": campaign_id, "email": email}, 1646 | {"$addToSet": {"phone_numbers": {"$each": phone_numbers}}, "$set": {"updated_at": datetime.utcnow()}} 1647 | ) 1648 | 1649 | return {"detail": f"{len(phone_numbers)} phone numbers added to campaign"} 1650 | 1651 | # 6. Add Manual Phone Numbers to a Campaign 1652 | @app.post("/campaigns/{campaign_id}/add_numbers/") 1653 | def add_phone_numbers_to_campaign( 1654 | campaign_id: str, 1655 | phone_numbers: List[str] = Body(...), 1656 | email: str = Query(...), 1657 | db=Depends(get_db) 1658 | ): 1659 | campaign = db.campaigns.find_one({"campaign_id": campaign_id, "email": email}) 1660 | if not campaign: 1661 | raise HTTPException(status_code=404, detail="Campaign not found or does not belong to user") 1662 | 1663 | db.campaigns.update_one( 1664 | {"campaign_id": campaign_id, "email": email}, 1665 | {"$addToSet": {"phone_numbers": {"$each": phone_numbers}}, "$set": {"updated_at": datetime.utcnow()}} 1666 | ) 1667 | 1668 | return {"detail": f"{len(phone_numbers)} phone numbers added to campaign"} 1669 | 1670 | # 7. Delete Phone Number from Campaign 1671 | @app.delete("/campaigns/{campaign_id}/phone_numbers/") 1672 | def delete_phone_number_from_campaign( 1673 | campaign_id: str, 1674 | request: PhoneNumberDeleteRequest = Body(...), 1675 | email: str = Query(...), 1676 | db=Depends(get_db) 1677 | ): 1678 | campaign = db.campaigns.find_one({"campaign_id": campaign_id, "email": email}) 1679 | if not campaign: 1680 | raise HTTPException(status_code=404, detail="Campaign not found or does not belong to user") 1681 | 1682 | phone_number = request.phone_number.strip() 1683 | result = db.campaigns.update_one( 1684 | {"campaign_id": campaign_id, "email": email}, 1685 | { 1686 | "$pull": { 1687 | "phone_numbers": phone_number, 1688 | "called_numbers": phone_number 1689 | }, 1690 | "$set": {"updated_at": datetime.utcnow()} 1691 | } 1692 | ) 1693 | if result.modified_count == 0: 1694 | raise HTTPException(status_code=404, detail="Phone number not found in campaign") 1695 | else: 1696 | return {"detail": f"Phone number {phone_number} deleted from campaign"} 1697 | 1698 | # 8. Update Phone Number in Campaign 1699 | @app.put("/campaigns/{campaign_id}/phone_numbers/") 1700 | def update_phone_number_in_campaign( 1701 | campaign_id: str, 1702 | request: PhoneNumberUpdateRequest = Body(...), 1703 | email: str = Query(...), 1704 | db=Depends(get_db) 1705 | ): 1706 | # Fetch the campaign 1707 | campaign = db.campaigns.find_one({"campaign_id": campaign_id, "email": email}) 1708 | if not campaign: 1709 | raise HTTPException(status_code=404, detail="Campaign not found or does not belong to user") 1710 | 1711 | old_phone_number = request.old_phone_number.strip() 1712 | new_phone_number = request.new_phone_number.strip() 1713 | 1714 | # Ensure that the old phone number exists in the campaign 1715 | if old_phone_number not in campaign.get('phone_numbers', []): 1716 | raise HTTPException(status_code=404, detail="Old phone number not found in campaign") 1717 | 1718 | try: 1719 | # Step 1: Remove the old phone number using $pull 1720 | db.campaigns.update_one( 1721 | {"campaign_id": campaign_id, "email": email}, 1722 | {"$pull": {"phone_numbers": old_phone_number}} 1723 | ) 1724 | 1725 | # Step 2: Add the new phone number using $addToSet 1726 | db.campaigns.update_one( 1727 | {"campaign_id": campaign_id, "email": email}, 1728 | {"$addToSet": {"phone_numbers": new_phone_number}} 1729 | ) 1730 | 1731 | # Update the called_numbers array if necessary 1732 | if old_phone_number in campaign.get('called_numbers', []): 1733 | db.campaigns.update_one( 1734 | {"campaign_id": campaign_id, "email": email}, 1735 | { 1736 | "$pull": {"called_numbers": old_phone_number}, 1737 | "$addToSet": {"called_numbers": new_phone_number} 1738 | } 1739 | ) 1740 | 1741 | return {"detail": f"Phone number {old_phone_number} updated to {new_phone_number} in campaign"} 1742 | 1743 | except pymongo.errors.WriteError as e: 1744 | raise HTTPException(status_code=500, detail=f"Database error: {str(e)}") 1745 | 1746 | # 9. Start Campaign 1747 | @app.post("/campaigns/{campaign_id}/start/") 1748 | def start_campaign( 1749 | campaign_id: str, 1750 | background_tasks: BackgroundTasks, 1751 | email: str = Query(...), 1752 | db=Depends(get_db) 1753 | ): 1754 | campaign = db.campaigns.find_one({"campaign_id": campaign_id, "email": email}) 1755 | if not campaign: 1756 | raise HTTPException(status_code=404, detail="Campaign not found or does not belong to user") 1757 | 1758 | # Trigger the background task to process campaign calls 1759 | background_tasks.add_task(process_campaign_calls_sync, campaign_id, email) 1760 | return {"detail": "Campaign started"} 1761 | # 10. Get Campaign Details 1762 | @app.get("/campaigns/{campaign_id}/") 1763 | def get_campaign_details( 1764 | campaign_id: str, 1765 | email: str = Query(...), 1766 | db=Depends(get_db) 1767 | ): 1768 | campaign = db.campaigns.find_one({"campaign_id": campaign_id, "email": email}) 1769 | if not campaign: 1770 | raise HTTPException(status_code=404, detail="Campaign not found or does not belong to user") 1771 | 1772 | return Campaign(**campaign) 1773 | 1774 | # 11. Get Call Status for a Phone Number in a Campaign 1775 | @app.get("/campaigns/{campaign_id}/call_status/") 1776 | def get_call_status( 1777 | campaign_id: str, 1778 | phone_number: str = Query(...), 1779 | email: str = Query(...), 1780 | db=Depends(get_db) 1781 | ): 1782 | campaign = db.campaigns.find_one({"campaign_id": campaign_id, "email": email}) 1783 | if not campaign: 1784 | raise HTTPException(status_code=404, detail="Campaign not found or does not belong to user") 1785 | 1786 | phone_numbers = campaign.get('phone_numbers', []) 1787 | called_numbers = campaign.get('called_numbers', []) 1788 | 1789 | if phone_number not in phone_numbers: 1790 | raise HTTPException(status_code=404, detail="Phone number not found in campaign") 1791 | 1792 | status = "called" if phone_number in called_numbers else "pending" 1793 | 1794 | return { 1795 | "campaign_id": campaign_id, 1796 | "phone_number": phone_number, 1797 | "status": status 1798 | } 1799 | #12 1800 | # 12. Get Campaign Status 1801 | @app.get("/campaigns/{campaign_id}/status/") 1802 | def get_campaign_status( 1803 | campaign_id: str, 1804 | email: str = Query(...), 1805 | db=Depends(get_db) 1806 | ): 1807 | campaign = db.campaigns.find_one({"campaign_id": campaign_id, "email": email}) 1808 | if not campaign: 1809 | raise HTTPException(status_code=404, detail="Campaign not found or does not belong to user") 1810 | 1811 | return { 1812 | "campaign_id": campaign_id, 1813 | "status": campaign.get("status"), 1814 | "updated_at": campaign.get("updated_at") 1815 | } 1816 | 1817 | 1818 | 1819 | def run(): 1820 | import uvicorn 1821 | uvicorn.run("app.main:app", host="0.0.0.0", port=8000) -------------------------------------------------------------------------------- /app/models/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NidumAI-Inc/agent-studio/67840a495da2220790f00a477b13d1deac722223/app/models/__init__.py -------------------------------------------------------------------------------- /app/models/model.py: -------------------------------------------------------------------------------- 1 | from typing import List, Optional 2 | from pydantic import BaseModel, Field 3 | from datetime import datetime 4 | 5 | class KnowledgeBase(BaseModel): 6 | files: List[str] 7 | 8 | class AI_Agent(BaseModel): 9 | id: str 10 | phone_number: str 11 | LLM_provider: str # New field 12 | LLM_model: str # New field 13 | stt_provider: str # New field 14 | stt_model: str # New field 15 | knowledge_base: KnowledgeBase 16 | rag_enabled: bool # New field 17 | temperature: float 18 | max_tokens: int 19 | first_message: str 20 | system_prompt: str 21 | language: str 22 | voice: str 23 | agent_name: str 24 | TTS_provider: str 25 | background_noise: Optional[str] = None 26 | agent_type: str 27 | tts_speed: Optional[float] = 1.0 # Default TTS speed 28 | interrupt_speech_duration: Optional[float] = 0.0 # Default interrupt speech duration 29 | 30 | class User(BaseModel): 31 | id: str = Field(alias="_id") 32 | email: str 33 | agents: List[AI_Agent] = [] 34 | 35 | class Message(BaseModel): 36 | timestamp: datetime 37 | speaker: str 38 | message: str 39 | tokens: int 40 | 41 | class CallLog(BaseModel): 42 | call_log_id: str 43 | agent_id: Optional[str] 44 | agent_name: Optional[str] 45 | agent_phone_number: Optional[str] 46 | user_id: Optional[str] 47 | incoming_callerid: Optional[str] 48 | call_type: str 49 | start_time: datetime 50 | end_time: datetime 51 | duration: float 52 | messages: List[Message] 53 | tts_name: Optional[str] 54 | stt_name: Optional[str] 55 | llm_name: Optional[str] 56 | total_tokens_llm: int 57 | total_tokens_stt: int 58 | total_tokens_tts: int 59 | cost_llm: float 60 | cost_stt: float 61 | cost_tts: float 62 | platform_cost: float 63 | total_cost: float 64 | conversation_analysis: Optional[str] 65 | called_number: Optional[str] 66 | call_direction: Optional[str] 67 | 68 | class DashboardData(BaseModel): 69 | total_call_minutes: float 70 | number_of_calls: int 71 | total_spent: float 72 | average_cost_per_call: float 73 | percentage_changes: dict 74 | call_end_reasons: dict 75 | average_call_duration_by_assistant: dict 76 | cost_per_provider: dict 77 | assistants_table: List[dict] 78 | total_calls_per_agent: dict 79 | call_breakdown_by_category: dict 80 | total_tokens_used: dict 81 | cost_breakdown_by_agent: dict 82 | average_call_duration_per_category: dict -------------------------------------------------------------------------------- /app/models/schemas.py: -------------------------------------------------------------------------------- 1 | from typing import List, Optional,Dict, Any 2 | from pydantic import BaseModel, Field, constr 3 | from datetime import datetime 4 | 5 | class UserCreate(BaseModel): 6 | email: str 7 | 8 | class UserUpdate(BaseModel): 9 | email: Optional[str] = None 10 | 11 | class AgentCreate(BaseModel): 12 | agent_name: Optional[str] = "Ava" # Default agent name if not provided 13 | phone_number: str 14 | LLM_provider: Optional[str] = "openai" # New field, default value 15 | LLM_model: Optional[str] = "GPT 3.5 Turbo Cluster" # New field, default value 16 | stt_provider: Optional[str] = "google" # New field, default value 17 | stt_model: Optional[str] = "whisper" # New field, default value 18 | rag_enabled: Optional[bool] = True # Default RAG enabled status 19 | temperature: Optional[float] = 0.7 # Default temperature 20 | max_tokens: Optional[int] = 250 # Default max tokens 21 | first_message: Optional[str] = "Hello, this is Ava. How may I assist you today?" # Default message 22 | system_prompt: Optional[str] = """Ava is a sophisticated AI training assistant, crafted by experts in customer support and AI development. 23 | Designed with the persona of a seasoned customer support agent in her early 30s, Ava combines deep technical knowledge with a strong sense of emotional intelligence. 24 | Her voice is clear, warm, and engaging, featuring a neutral accent for widespread accessibility. Ava's primary role is to serve as a dynamic training platform for customer support agents, 25 | simulating a broad array of service scenarios—from basic inquiries to intricate problem-solving challenges.""" # Default system prompt 26 | language: Optional[str] = "English" # Default language 27 | voice: Optional[str] = "nova" # Default voice 28 | TTS_provider: Optional[str] = "aws_polly" 29 | background_noise: Optional[str] = None 30 | agent_type: Optional[str] = "web" 31 | tts_speed: Optional[float] = 1.0 32 | interrupt_speech_duration: Optional[float] = 0.0 33 | 34 | class AgentUpdate(BaseModel): 35 | phone_number: Optional[str] = None 36 | LLM_provider: Optional[str] = None # New field 37 | LLM_model: Optional[str] = None # New field 38 | stt_provider: Optional[str] = None # New field 39 | stt_model: Optional[str] = None # New field 40 | knowledge_base: Optional[dict] = None 41 | rag_enabled: Optional[bool] = None # New field 42 | temperature: Optional[float] = None 43 | max_tokens: Optional[int] = None 44 | first_message: Optional[str] = None 45 | system_prompt: Optional[str] = None 46 | language: Optional[str] = None 47 | voice: Optional[str] = None 48 | TTS_provider: Optional[str] = None 49 | background_noise: Optional[str] = None 50 | agent_name: Optional[str] = None 51 | tts_speed: Optional[float] = None 52 | interrupt_speech_duration: Optional[float] = None 53 | 54 | class KnowledgeBaseResponse(BaseModel): 55 | files: List[str] 56 | raw_data_file: Optional[str] = None 57 | pkl_file: Optional[str] = None 58 | vdb_data_file: Optional[str] = None 59 | 60 | class FileListResponse(BaseModel): 61 | files: List[str] 62 | 63 | class ChatMessage(BaseModel): 64 | role: str = Field(..., description="Role of the message sender, e.g., 'user' or 'assistant'.") 65 | content: str = Field(..., description="Content of the message.") 66 | 67 | class ChatRequest(BaseModel): 68 | agent_id: str = Field(..., description="ID of the agent") 69 | user_id: str = Field(..., description="ID of the user") 70 | chat: List[ChatMessage] = Field(..., description="List of messages exchanged") 71 | chat_id: Optional[str] = Field(None, description="Chat ID for existing chats (optional)") 72 | 73 | class ChatLog(BaseModel): 74 | chat_id: str 75 | agent_id: str 76 | agent_name: Optional[str] = None 77 | user_id: str 78 | chat_data: List[ChatMessage] 79 | result: str 80 | usage: dict 81 | total_tokens: int 82 | cost_llm: float 83 | conversation_analysis: Optional[str] = None 84 | created_at: datetime 85 | updated_at: datetime 86 | 87 | class DynamicDataRequest(BaseModel): 88 | user_id: str 89 | agent_id: str 90 | data: Dict[str, Any] # Accepting any dynamic JSON data 91 | 92 | 93 | class CampaignCreate(BaseModel): 94 | email: str 95 | campaign_name: str 96 | campaign_description: Optional[str] = None 97 | agent_phone_number: str 98 | 99 | class CampaignUpdate(BaseModel): 100 | campaign_name: Optional[str] = None 101 | campaign_description: Optional[str] = None 102 | agent_phone_number: Optional[str] = None 103 | 104 | class Campaign(BaseModel): 105 | campaign_id: str 106 | email: str 107 | campaign_name: str 108 | campaign_description: Optional[str] = None 109 | agent_phone_number: str 110 | phone_numbers: List[str] = [] 111 | called_numbers: List[str] = [] 112 | status: str 113 | created_at: datetime 114 | updated_at: datetime 115 | 116 | class PhoneNumberDeleteRequest(BaseModel): 117 | phone_number: str 118 | 119 | class PhoneNumberUpdateRequest(BaseModel): 120 | old_phone_number: str 121 | new_phone_number: str -------------------------------------------------------------------------------- /app/services/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NidumAI-Inc/agent-studio/67840a495da2220790f00a477b13d1deac722223/app/services/__init__.py -------------------------------------------------------------------------------- /app/services/campaign_helper.py: -------------------------------------------------------------------------------- 1 | from app.db.databases import * 2 | from threading import Semaphore, Thread 3 | import os 4 | import uuid 5 | import tempfile 6 | import shutil 7 | from datetime import datetime 8 | import subprocess 9 | 10 | import subprocess 11 | 12 | def run_command_sync(cmd): 13 | try: 14 | result = subprocess.run( 15 | cmd, 16 | shell=True, 17 | stdout=subprocess.PIPE, 18 | stderr=subprocess.PIPE, 19 | text=True 20 | ) 21 | if result.returncode != 0: 22 | raise Exception(f"Command failed with exit code {result.returncode}: {result.stderr}") 23 | return result.stdout 24 | except Exception as e: 25 | raise Exception(f"Failed to run command: {cmd} -> {e}") 26 | def normalize_phone_number(phone_number: str) -> str: 27 | """ 28 | Normalize phone numbers by ensuring they include the '+' sign, 29 | and removing spaces or special characters. 30 | """ 31 | phone_number = phone_number.strip().replace(' ', '').replace('-', '') 32 | if not phone_number.startswith('+'): 33 | phone_number = '+' + phone_number 34 | return phone_number 35 | 36 | # Process the calls in the background 37 | def process_campaign_calls_sync(campaign_id: str, email: str): 38 | """ 39 | Process campaign calls by starting outbound calls for each phone number in the campaign. 40 | Ensure phone number normalization to match the format in the logs collection. 41 | """ 42 | db = get_database() 43 | campaign = db.campaigns.find_one({"campaign_id": campaign_id, "email": email}) 44 | 45 | if not campaign: 46 | print(f"Campaign not found: {campaign_id}") 47 | return 48 | 49 | # Normalize the agent phone number 50 | agent_phone_number = normalize_phone_number(campaign['agent_phone_number']) 51 | 52 | # Retrieve the outbound_trunk_id from the logs collection using normalized phone number 53 | log_entry = db.logs.find_one({"email": email, "phone_number": agent_phone_number}) 54 | if not log_entry: 55 | print(f"No trunk entry found for email: {email}, agent_phone_number: {agent_phone_number}") 56 | db.campaigns.update_one( 57 | {"campaign_id": campaign_id}, 58 | {"$set": {"status": "failed", "updated_at": datetime.utcnow()}} 59 | ) 60 | return 61 | 62 | outbound_trunk_id = log_entry.get('outbound_trunk_id') 63 | if not outbound_trunk_id: 64 | print(f"No outbound_trunk_id found for email: {email}, agent_phone_number: {agent_phone_number}") 65 | db.campaigns.update_one( 66 | {"campaign_id": campaign_id}, 67 | {"$set": {"status": "failed", "updated_at": datetime.utcnow()}} 68 | ) 69 | return 70 | 71 | phone_numbers = campaign.get('phone_numbers', []) 72 | if not phone_numbers: 73 | print(f"No phone numbers found for campaign {campaign_id}") 74 | db.campaigns.update_one( 75 | {"campaign_id": campaign_id}, 76 | {"$set": {"status": "no_numbers", "updated_at": datetime.utcnow()}} 77 | ) 78 | return 79 | 80 | # Update campaign status to 'running' 81 | db.campaigns.update_one( 82 | {"campaign_id": campaign_id}, 83 | {"$set": {"status": "running", "updated_at": datetime.utcnow()}} 84 | ) 85 | 86 | called_numbers = campaign.get('called_numbers', []) 87 | remaining_numbers = [num for num in phone_numbers if num not in called_numbers] 88 | 89 | # Semaphore to limit the number of concurrent calls 90 | semaphore = Semaphore(3) 91 | 92 | def make_call(phone_number): 93 | with semaphore: 94 | try: 95 | print(f"Initiating call to {phone_number}") 96 | 97 | participant_identity = f"sip_{phone_number.strip('+')}_{campaign_id}_outbound" 98 | room_name = f"call-{phone_number.strip('+')}" 99 | participant_name = "Campaign Call" 100 | 101 | # JSON content for SIP participant creation 102 | sip_participant_content = f""" 103 | {{ 104 | "sip_trunk_id": "{outbound_trunk_id}", 105 | "sip_call_to": "{phone_number}", 106 | "room_name": "{room_name}", 107 | "participant_identity": "{participant_identity}", 108 | "participant_name": "{participant_name}" 109 | }} 110 | """ 111 | 112 | temp_dir = tempfile.mkdtemp() 113 | sip_participant_filename = f"sipParticipant_{uuid.uuid4()}.json" 114 | sip_participant_path = os.path.join(temp_dir, sip_participant_filename) 115 | 116 | # Write the JSON content to a file 117 | with open(sip_participant_path, "w") as f: 118 | f.write(sip_participant_content) 119 | 120 | # Execute the lk command using run_command_sync function 121 | create_sip_participant_cmd = f"lk sip participant create {sip_participant_path}" 122 | print(f"Running command: {create_sip_participant_cmd}") 123 | 124 | command_output = run_command_sync(create_sip_participant_cmd) 125 | print(f"Command output: {command_output}") 126 | 127 | # Update the campaign with the called number 128 | db.campaigns.update_one( 129 | {"campaign_id": campaign_id}, 130 | {"$addToSet": {"called_numbers": phone_number}} 131 | ) 132 | 133 | except Exception as e: 134 | print(f"Error making call to {phone_number}: {e}") 135 | 136 | finally: 137 | shutil.rmtree(temp_dir) 138 | 139 | # Start the calls concurrently using threads 140 | threads = [] 141 | for phone_number in remaining_numbers: 142 | t = Thread(target=make_call, args=(normalize_phone_number(phone_number),)) 143 | threads.append(t) 144 | t.start() 145 | 146 | # Wait for all threads to complete 147 | for t in threads: 148 | t.join() 149 | 150 | # Update campaign status to 'completed' after all calls are made 151 | db.campaigns.update_one( 152 | {"campaign_id": campaign_id}, 153 | {"$set": {"status": "completed", "updated_at": datetime.utcnow()}} 154 | ) 155 | print(f"Campaign {campaign_id} completed successfully.") 156 | -------------------------------------------------------------------------------- /app/services/livkit_rag.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import pickle 3 | import uuid 4 | import aiohttp 5 | from livekit.agents import tokenize 6 | from livekit.plugins import openai, rag 7 | from tqdm import tqdm 8 | import os 9 | 10 | embeddings_dimension = 1536 # Set according to your model 11 | 12 | async def _create_embeddings( 13 | input: str, http_session: aiohttp.ClientSession 14 | ) -> openai.EmbeddingData: 15 | results = await openai.create_embeddings( 16 | input=[input], 17 | model="text-embedding-3-small", 18 | dimensions=embeddings_dimension, 19 | http_session=http_session, 20 | ) 21 | return results[0] 22 | 23 | async def process_files(raw_data_file: str, output_dir: str) -> None: 24 | raw_data = open(raw_data_file, "r").read() 25 | 26 | async with aiohttp.ClientSession() as http_session: 27 | idx_builder = rag.annoy.IndexBuilder(f=embeddings_dimension, metric="angular") 28 | 29 | paragraphs_by_uuid = {} 30 | for p in tokenize.basic.tokenize_paragraphs(raw_data): 31 | p_uuid = uuid.uuid4() 32 | paragraphs_by_uuid[p_uuid] = p 33 | 34 | for p_uuid, paragraph in tqdm(paragraphs_by_uuid.items()): 35 | if paragraph != "": 36 | resp = await _create_embeddings(paragraph, http_session) 37 | idx_builder.add_item(resp.embedding, p_uuid) 38 | 39 | idx_builder.build() 40 | idx_builder.save(os.path.join(output_dir, "vdb_data")) 41 | 42 | # save data with pickle 43 | with open(os.path.join(output_dir, "my_data.pkl"), "wb") as f: 44 | pickle.dump(paragraphs_by_uuid, f) 45 | -------------------------------------------------------------------------------- /app/services/llama_index_integration.py: -------------------------------------------------------------------------------- 1 | from llama_index.core import ( 2 | Document, 3 | StorageContext, 4 | VectorStoreIndex, 5 | load_index_from_storage, 6 | ) 7 | import os 8 | from PyPDF2 import PdfReader 9 | import docx 10 | from llama_index.core.node_parser import SimpleNodeParser 11 | 12 | # Function to manually load documents from a directory 13 | def load_documents_from_directory(directory_path: str): 14 | documents = [] 15 | for filename in os.listdir(directory_path): 16 | file_path = os.path.join(directory_path, filename) 17 | if filename.endswith(".txt"): 18 | with open(file_path, "r", encoding="utf-8") as f: 19 | content = f.read() 20 | documents.append(Document(text=content, metadata={"filename": filename})) 21 | elif filename.endswith(".pdf"): 22 | text = extract_text_from_pdf(file_path) 23 | documents.append(Document(text=text, metadata={"filename": filename})) 24 | elif filename.endswith(".docx"): 25 | text = extract_text_from_docx(file_path) 26 | documents.append(Document(text=text, metadata={"filename": filename})) 27 | return documents 28 | 29 | # Function to extract text from PDF 30 | def extract_text_from_pdf(pdf_path): 31 | reader = PdfReader(pdf_path) 32 | text = "" 33 | for page in reader.pages: 34 | text += page.extract_text() or "" 35 | return text 36 | 37 | # Function to extract text from DOCX 38 | def extract_text_from_docx(docx_path): 39 | doc = docx.Document(docx_path) 40 | return "\n".join([para.text for para in doc.paragraphs]) 41 | 42 | # Function to create and save a new index 43 | def process_files_with_llama_index(directory_path: str, output_dir: str): 44 | """ 45 | Processes files in the specified directory and saves the index in the output directory. 46 | """ 47 | # Load the documents manually from the directory 48 | documents = load_documents_from_directory(directory_path) 49 | 50 | # Create a VectorStore index 51 | index = VectorStoreIndex.from_documents(documents) 52 | 53 | # Save the index in the storage context 54 | index.storage_context.persist(output_dir) 55 | 56 | 57 | # Function to load and use an existing index 58 | def load_index_and_query(storage_dir: str, query: str, retrieval_len: int): 59 | """ 60 | Load the index from the storage directory and query it. 61 | Returns a parsed response with relevant information. 62 | """ 63 | # Load the index from the storage 64 | storage_context = StorageContext.from_defaults(persist_dir=storage_dir) 65 | index = load_index_from_storage(storage_context) 66 | 67 | # Convert index to retriever 68 | retriever = index.as_retriever() 69 | 70 | # Perform the retrieval 71 | results = retriever.retrieve(query) 72 | 73 | # Parse and format the results 74 | parsed_results = [] 75 | text_result=[] 76 | for result in results[:retrieval_len]: 77 | node = result.node 78 | text_result.append(node.text) 79 | parsed_results.append({ 80 | "text": node.text, # The actual content of the node 81 | "metadata": node.metadata, # Metadata like filename 82 | "score": result.score # Relevance score 83 | }) 84 | return parsed_results,text_result 85 | 86 | # load_index_and_query("uploads/9657b257-f1fb-4479-ba25-683312e2e7ab/8cb5c2df-bbc7-49dc-a8af-3539a40963a1","total cost",13) -------------------------------------------------------------------------------- /app/services/llm.py: -------------------------------------------------------------------------------- 1 | import os 2 | import requests 3 | import asyncio 4 | from tenacity import retry, stop_after_attempt, wait_random_exponential 5 | from dotenv import load_dotenv 6 | load_dotenv(dotenv_path="app/env/.env") 7 | # Read OpenAI API key from environment variable 8 | APIKEY_openai = os.getenv("OPENAI_API_KEY") 9 | 10 | if not APIKEY_openai: 11 | raise ValueError("OpenAI API key is not set in the environment variables.") 12 | 13 | # Define cost per token for LLM (adjust according to your actual cost) 14 | COST_PER_TOKEN_LLM = 0.00002 15 | 16 | # OpenAI API interaction function 17 | def openai_LLM(chat): 18 | """Get response from OpenAI LLM""" 19 | try: 20 | url = "https://api.openai.com/v1/chat/completions" 21 | payload = { 22 | "model": "gpt-4o-mini", 23 | "messages": chat, 24 | "temperature": 0.3, 25 | "stream": False, 26 | "max_tokens": 1000 27 | } 28 | headers = { 29 | "Content-Type": "application/json", 30 | "Authorization": f"Bearer {APIKEY_openai}" 31 | } 32 | response = requests.post(url, headers=headers, json=payload) 33 | response.raise_for_status() 34 | result = response.json() 35 | return result 36 | except Exception as e: 37 | print(f"Error with OpenAI LLM: {e}") 38 | return None 39 | 40 | # Conversation analysis function 41 | @retry(wait=wait_random_exponential(min=1, max=60), stop=stop_after_attempt(5)) 42 | async def analyze_conversation(messages): 43 | """Analyze conversation using OpenAI""" 44 | def _analyze(): 45 | conversation_text = "\n".join([f"{msg['role']}: {msg['content']}" for msg in messages]) 46 | 47 | system_prompt = """ 48 | You are an AI assistant tasked with analyzing customer service conversations. 49 | Please provide a brief report that includes: 50 | 1. The main topic or purpose of the conversation 51 | 2. The customer's primary concern or request 52 | 3. How well the agent addressed the customer's needs 53 | 4. Any notable positive or negative aspects of the interaction 54 | 5. Suggestions for improvement in future interactions 55 | 56 | Keep your analysis concise and focused on the most important aspects of the conversation. 57 | """ 58 | 59 | try: 60 | url = "https://api.openai.com/v1/chat/completions" 61 | payload = { 62 | "model": "gpt-4o", 63 | "messages": [ 64 | {"role": "system", "content": system_prompt}, 65 | {"role": "user", "content": conversation_text} 66 | ], 67 | "temperature": 0.3, 68 | "max_tokens": 300 69 | } 70 | headers = { 71 | "Content-Type": "application/json", 72 | "Authorization": f"Bearer {APIKEY_openai}" 73 | } 74 | response = requests.post(url, headers=headers, json=payload) 75 | response.raise_for_status() 76 | result = response.json() 77 | analysis = result['choices'][0]['message']['content'].strip() 78 | return analysis 79 | except Exception as e: 80 | print(f"Error in conversation analysis: {e}") 81 | return None 82 | 83 | return await asyncio.to_thread(_analyze) 84 | -------------------------------------------------------------------------------- /app/services/utils.py: -------------------------------------------------------------------------------- 1 | import os 2 | from typing import Optional, List 3 | import asyncio 4 | from app.services.livkit_rag import process_files 5 | 6 | def extract_text_from_file(filename: str, content: bytes) -> Optional[str]: 7 | _, ext = os.path.splitext(filename) 8 | ext = ext.lower() 9 | if ext == ".txt": 10 | return content.decode("utf-8") 11 | elif ext == ".pdf": 12 | # Implement PDF text extraction 13 | from io import BytesIO 14 | from PyPDF2 import PdfReader 15 | reader = PdfReader(BytesIO(content)) 16 | text = "" 17 | for page in reader.pages: 18 | text += page.extract_text() 19 | return text 20 | elif ext == ".docx": 21 | # Implement DOCX text extraction 22 | from io import BytesIO 23 | import docx 24 | doc = docx.Document(BytesIO(content)) 25 | text = "\n".join([para.text for para in doc.paragraphs]) 26 | return text 27 | else: 28 | return None 29 | 30 | def delete_directory(path: str): 31 | import shutil 32 | if os.path.exists(path): 33 | shutil.rmtree(path) 34 | 35 | async def re_embed_files(agent_dir: str, filenames: List[str]): 36 | files_dir = os.path.join(agent_dir, "files") 37 | combined_text = "" 38 | for filename in filenames: 39 | file_path = os.path.join(files_dir, filename) 40 | with open(file_path, "rb") as f: 41 | content = f.read() 42 | text = extract_text_from_file(filename, content) 43 | if text: 44 | combined_text += text + "\n" 45 | 46 | # Save raw data 47 | raw_data_file = os.path.join(agent_dir, "raw_data.txt") 48 | with open(raw_data_file, "w") as f: 49 | f.write(combined_text) 50 | 51 | # Re-process the files using Livkit code 52 | await process_files(raw_data_file, agent_dir) 53 | -------------------------------------------------------------------------------- /docs/Images/11r_Image_14.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NidumAI-Inc/agent-studio/67840a495da2220790f00a477b13d1deac722223/docs/Images/11r_Image_14.png -------------------------------------------------------------------------------- /docs/Images/4Cj_Image_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NidumAI-Inc/agent-studio/67840a495da2220790f00a477b13d1deac722223/docs/Images/4Cj_Image_2.png -------------------------------------------------------------------------------- /docs/Images/4K6_Image_13.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NidumAI-Inc/agent-studio/67840a495da2220790f00a477b13d1deac722223/docs/Images/4K6_Image_13.png -------------------------------------------------------------------------------- /docs/Images/9Yp_Image_6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NidumAI-Inc/agent-studio/67840a495da2220790f00a477b13d1deac722223/docs/Images/9Yp_Image_6.png -------------------------------------------------------------------------------- /docs/Images/Dns_Image_25.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NidumAI-Inc/agent-studio/67840a495da2220790f00a477b13d1deac722223/docs/Images/Dns_Image_25.png -------------------------------------------------------------------------------- /docs/Images/GTe_Image_10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NidumAI-Inc/agent-studio/67840a495da2220790f00a477b13d1deac722223/docs/Images/GTe_Image_10.png -------------------------------------------------------------------------------- /docs/Images/HWk_Image_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NidumAI-Inc/agent-studio/67840a495da2220790f00a477b13d1deac722223/docs/Images/HWk_Image_1.png -------------------------------------------------------------------------------- /docs/Images/He6_Image_7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NidumAI-Inc/agent-studio/67840a495da2220790f00a477b13d1deac722223/docs/Images/He6_Image_7.png -------------------------------------------------------------------------------- /docs/Images/MG0_Image_24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NidumAI-Inc/agent-studio/67840a495da2220790f00a477b13d1deac722223/docs/Images/MG0_Image_24.png -------------------------------------------------------------------------------- /docs/Images/Njd_Image_15.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NidumAI-Inc/agent-studio/67840a495da2220790f00a477b13d1deac722223/docs/Images/Njd_Image_15.png -------------------------------------------------------------------------------- /docs/Images/PYD_Image_20.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NidumAI-Inc/agent-studio/67840a495da2220790f00a477b13d1deac722223/docs/Images/PYD_Image_20.png -------------------------------------------------------------------------------- /docs/Images/PfR_Image_21.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NidumAI-Inc/agent-studio/67840a495da2220790f00a477b13d1deac722223/docs/Images/PfR_Image_21.png -------------------------------------------------------------------------------- /docs/Images/PyO_Image_22.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NidumAI-Inc/agent-studio/67840a495da2220790f00a477b13d1deac722223/docs/Images/PyO_Image_22.png -------------------------------------------------------------------------------- /docs/Images/UDL_Image_19.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NidumAI-Inc/agent-studio/67840a495da2220790f00a477b13d1deac722223/docs/Images/UDL_Image_19.png -------------------------------------------------------------------------------- /docs/Images/VXK_Image_18.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NidumAI-Inc/agent-studio/67840a495da2220790f00a477b13d1deac722223/docs/Images/VXK_Image_18.png -------------------------------------------------------------------------------- /docs/Images/aAi_Image_17.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NidumAI-Inc/agent-studio/67840a495da2220790f00a477b13d1deac722223/docs/Images/aAi_Image_17.png -------------------------------------------------------------------------------- /docs/Images/bBC_Image_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NidumAI-Inc/agent-studio/67840a495da2220790f00a477b13d1deac722223/docs/Images/bBC_Image_3.png -------------------------------------------------------------------------------- /docs/Images/eSe_Image_23.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NidumAI-Inc/agent-studio/67840a495da2220790f00a477b13d1deac722223/docs/Images/eSe_Image_23.png -------------------------------------------------------------------------------- /docs/Images/eij_Image_8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NidumAI-Inc/agent-studio/67840a495da2220790f00a477b13d1deac722223/docs/Images/eij_Image_8.png -------------------------------------------------------------------------------- /docs/Images/ezZ_Image_4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NidumAI-Inc/agent-studio/67840a495da2220790f00a477b13d1deac722223/docs/Images/ezZ_Image_4.png -------------------------------------------------------------------------------- /docs/Images/gbz_Image_16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NidumAI-Inc/agent-studio/67840a495da2220790f00a477b13d1deac722223/docs/Images/gbz_Image_16.png -------------------------------------------------------------------------------- /docs/Images/q45_Image_9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NidumAI-Inc/agent-studio/67840a495da2220790f00a477b13d1deac722223/docs/Images/q45_Image_9.png -------------------------------------------------------------------------------- /docs/Images/sO1_Image_12.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NidumAI-Inc/agent-studio/67840a495da2220790f00a477b13d1deac722223/docs/Images/sO1_Image_12.png -------------------------------------------------------------------------------- /docs/Images/tyu_Image_26.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NidumAI-Inc/agent-studio/67840a495da2220790f00a477b13d1deac722223/docs/Images/tyu_Image_26.png -------------------------------------------------------------------------------- /docs/Images/ufu_Image_11.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NidumAI-Inc/agent-studio/67840a495da2220790f00a477b13d1deac722223/docs/Images/ufu_Image_11.png -------------------------------------------------------------------------------- /docs/Images/xFN_Image_5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NidumAI-Inc/agent-studio/67840a495da2220790f00a477b13d1deac722223/docs/Images/xFN_Image_5.png -------------------------------------------------------------------------------- /docs/intro.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NidumAI-Inc/agent-studio/67840a495da2220790f00a477b13d1deac722223/docs/intro.gif -------------------------------------------------------------------------------- /docs/livkit_server_setup.md: -------------------------------------------------------------------------------- 1 | ### LiveKit Server Setup Guide** 2 | 3 | This guide will help you set up the LiveKit Server for Agent Studio. LiveKit is a key component that enables real-time audio communication. 4 | 5 | #### **Step 1: Install Go on the Server** 6 | 7 | 1. **Download the Go Binary**: 8 | - Visit the official Go downloads page [here](https://golang.org/dl/). 9 | - Copy the link for the Linux version (e.g., `go1.23.0.linux-amd64.tar.gz`). 10 | 11 | 2. **Use curl to download the Go binary**: 12 | 13 | ```bash 14 | curl -O https://go.dev/dl/go1.23.0.linux-amd64.tar.gz 15 | ``` 16 | 17 | 3. **Remove any previous Go installation**: 18 | 19 | ```bash 20 | sudo rm -rf /usr/local/go 21 | ``` 22 | 23 | 4. **Extract the Go archive to `/usr/local/`**: 24 | 25 | ```bash 26 | sudo tar -C /usr/local -xzf go1.23.0.linux-amd64.tar.gz 27 | ``` 28 | 29 | 5. **Add Go to the PATH environment variable**: 30 | 31 | ```bash 32 | echo 'export PATH=$PATH:/usr/local/go/bin' | sudo tee -a /etc/profile 33 | source /etc/profile 34 | ``` 35 | 36 | 6. **Verify the Go installation**: 37 | 38 | ```bash 39 | go version 40 | ``` 41 | 42 | 7. **Set up the Go environment**: 43 | 44 | ```bash 45 | mkdir -p /root/go/bin 46 | export GOPATH=/root/go 47 | export PATH=$PATH:/root/go/bin 48 | ``` 49 | 50 | 8. **Persist the changes**: 51 | 52 | ```bash 53 | echo 'export GOPATH=/root/go' >> ~/.bashrc 54 | echo 'export PATH=$PATH:/root/go/bin' >> ~/.bashrc 55 | source ~/.bashrc 56 | ``` 57 | 58 | #### **Step 2: Clone the LiveKit Repository and Set Up Environment** 59 | 60 | 1. **Clone the LiveKit repository**: 61 | 62 | ```bash 63 | git clone https://github.com/livekit/livekit.git 64 | cd livekit 65 | ``` 66 | 67 | 2. **Run the bootstrap script**: 68 | 69 | ```bash 70 | ./bootstrap.sh 71 | ``` 72 | 73 | 3. **Install Mage (Go-based build tool)**: 74 | 75 | ```bash 76 | go install github.com/magefile/mage@latest 77 | ``` 78 | 79 | 4. **Verify the installation of `mage`**: 80 | 81 | ```bash 82 | ls $HOME/go/bin 83 | ``` 84 | 85 | 5. **Add Go Bin to PATH**: 86 | 87 | ```bash 88 | export PATH=$PATH:$HOME/go/bin 89 | echo 'export PATH=$PATH:$HOME/go/bin' >> ~/.bashrc 90 | source ~/.bashrc 91 | ``` 92 | 93 | 6. **Build the project using Mage**: 94 | 95 | ```bash 96 | mage 97 | ``` 98 | 99 | #### **Step 3: Build LiveKit CLI from Source** 100 | 101 | 1. **Install Git LFS**: 102 | 103 | - Install Git LFS from the [Git LFS installation guide](https://git-lfs.github.com/). 104 | 105 | 2. **Initialize Git LFS**: 106 | 107 | ```bash 108 | git lfs install 109 | ``` 110 | 111 | 3. **Clone the LiveKit CLI repository**: 112 | 113 | ```bash 114 | git clone https://github.com/livekit/livekit-cli.git 115 | cd livekit-cli 116 | ``` 117 | 118 | 4. **Build and install the LiveKit CLI**: 119 | 120 | ```bash 121 | make install 122 | ``` 123 | 124 | 5. **Verify the installation**: 125 | 126 | ```bash 127 | lk --help 128 | ``` 129 | 130 | #### **Step 4: Install, Configure, and Run Redis on Ubuntu** 131 | 132 | 1. **Update package lists**: 133 | 134 | ```bash 135 | sudo apt update 136 | ``` 137 | 138 | 2. **Install Redis**: 139 | 140 | ```bash 141 | sudo apt install redis-server -y 142 | ``` 143 | 144 | 3. **Set `supervised` to `systemd` in Redis configuration**: 145 | 146 | ```bash 147 | sudo sed -i 's/^supervised no/supervised systemd/' /etc/redis/redis.conf 148 | ``` 149 | 150 | 4. **Start and enable Redis**: 151 | 152 | ```bash 153 | sudo systemctl start redis-server 154 | sudo systemctl enable redis-server 155 | ``` 156 | 157 | 5. **Check Redis status**: 158 | 159 | ```bash 160 | sudo systemctl status redis-server 161 | ``` 162 | 163 | 6. **Test Redis**: 164 | 165 | ```bash 166 | redis-cli ping 167 | ``` 168 | 169 | #### **Step 5: Start LiveKit Server** 170 | 171 | 1. **Run the LiveKit server**: 172 | 173 | ```bash 174 | ./livekit-server --redis-host 127.0.0.1:6379 --dev 175 | ``` 176 | 177 | #### **LiveKit Server Troubleshooting** 178 | 179 | - **Redis Issues**: Ensure Redis is running and accessible (`redis-cli ping`). 180 | - **Environment Variables**: Confirm all environment variables are set correctly. 181 | - **Server Logs**: Use debug mode for more detailed logs: 182 | ```bash 183 | ./livekit-server --dev --debug 184 | ``` 185 | 186 | -------------------------------------------------------------------------------- /docs/livkit_sip_server_setup.md: -------------------------------------------------------------------------------- 1 | Here's a refined version of your document: 2 | 3 | --- 4 | 5 | ### SIP Server Setup Guide** 6 | 7 | This guide outlines the steps to set up the **SIP Server** to enable integration with external telephony systems for real-time communication in Agent Studio. 8 | 9 | #### **Step 1: Clone and Build the SIP Server** 10 | 11 | 1. **Clone the SIP server repository**: 12 | 13 | ```bash 14 | git clone https://github.com/livekit/sip.git 15 | cd sip 16 | ``` 17 | 18 | 2. **Build the SIP server using Mage**: 19 | 20 | ```bash 21 | mage build 22 | ``` 23 | 24 | 3. **Verify the SIP server installation** by running: 25 | 26 | ```bash 27 | sip 28 | ``` 29 | 30 | #### **Step 2: Install, Configure, and Run Redis on Ubuntu** 31 | 32 | 1. **Update the package lists**: 33 | 34 | ```bash 35 | sudo apt update 36 | ``` 37 | 38 | 2. **Install Redis**: 39 | 40 | ```bash 41 | sudo apt install redis-server -y 42 | ``` 43 | 44 | 3. **Set `supervised` mode to `systemd` in Redis configuration**: 45 | 46 | ```bash 47 | sudo sed -i 's/^supervised no/supervised systemd/' /etc/redis/redis.conf 48 | ``` 49 | 50 | 4. **Start and enable Redis**: 51 | 52 | ```bash 53 | sudo systemctl start redis-server 54 | sudo systemctl enable redis-server 55 | ``` 56 | 57 | 5. **Check Redis status**: 58 | 59 | ```bash 60 | sudo systemctl status redis-server 61 | ``` 62 | 63 | 6. **Test Redis connection**: 64 | 65 | ```bash 66 | redis-cli ping 67 | ``` 68 | 69 | #### **Step 3: Configure and Run the SIP Server** 70 | 71 | 1. **Create a configuration file**: 72 | 73 | ```bash 74 | nano config.yaml 75 | ``` 76 | 77 | Add the following content: 78 | 79 | ```yaml 80 | api_key: "devkey" 81 | api_secret: "secret" 82 | ws_url: ws://localhost:7880 83 | redis: 84 | address: 127.0.0.1:6379 85 | sip_port: 5060 86 | rtp_port: 10000-20000 87 | use_external_ip: true 88 | logging: 89 | level: debug 90 | ``` 91 | 92 | 2. **Run the SIP server in a tmux session**: 93 | 94 | ```bash 95 | tmux new -s sipserver 96 | sip --config=config.yaml 97 | ``` 98 | 99 | Detach from the tmux session with `Ctrl + B`, then `D`. 100 | 101 | #### **Step 4: Determine Your SIP URI and Configure a SIP Trunk** 102 | 103 | 1. **Find your public IP address**: 104 | 105 | ```bash 106 | curl ifconfig.me 107 | ``` 108 | 109 | 2. **Construct your SIP URI**: `sip::5060`. 110 | 111 | 3. **Create a SIP Trunk (e.g., with Twilio)**, and set your SIP URI as the Origination SIP URI. 112 | 113 | #### **Step 5: Configure the LiveKit CLI** 114 | 115 | 1. **Set the environment variables**: 116 | 117 | ```bash 118 | export LIVEKIT_URL="ws://0.0.0.0:7880" 119 | export LIVEKIT_API_KEY="devkey" 120 | export LIVEKIT_API_SECRET="secret" 121 | ``` 122 | 123 | 2. **Persist the environment variables**: 124 | 125 | ```bash 126 | echo 'export LIVEKIT_URL="ws://localhost:7880"' >> ~/.bashrc 127 | echo 'export LIVEKIT_API_KEY="devkey"' >> ~/.bashrc 128 | echo 'export LIVEKIT_API_SECRET="secret"' >> ~/.bashrc 129 | source ~/.bashrc 130 | ``` 131 | 132 | #### **Step 6: Create Inbound Trunk and Dispatch Rule** 133 | 134 | 1. **Create an inbound trunk configuration**: 135 | 136 | ```bash 137 | nano inboundTrunk.json 138 | ``` 139 | 140 | Add the following content: 141 | 142 | ```json 143 | { 144 | "trunk": { 145 | "name": "Demo Inbound Trunk", 146 | "numbers": ["+1234567890"] 147 | } 148 | } 149 | ``` 150 | 151 | 2. **Create the inbound trunk**: 152 | 153 | ```bash 154 | lk sip inbound create inboundTrunk.json 155 | ``` 156 | 157 | 3. **Create a dispatch rule**: 158 | 159 | ```bash 160 | nano dispatchRule.json 161 | ``` 162 | 163 | Add the following content: 164 | 165 | ```json 166 | { 167 | "name": "Demo Dispatch Rule", 168 | "trunk_ids": [""], 169 | "rule": { 170 | "dispatchRuleDirect": { 171 | "roomName": "my-sip-room", 172 | "pin": "" 173 | } 174 | } 175 | } 176 | ``` 177 | 178 | 4. **Apply the dispatch rule**: 179 | 180 | ```bash 181 | lk sip dispatch create dispatchRule.json 182 | ``` 183 | 184 | --- 185 | 186 | #### **SIP Server Troubleshooting** 187 | 188 | - **Redis Issues**: Ensure Redis is running and accessible by using `redis-cli ping`. 189 | - **Connection Issues**: Check network configurations, firewall settings, and server logs for errors. 190 | - **Server Logs**: Enable debug mode for more detailed logs: 191 | 192 | ```bash 193 | sip --config=config.yaml --debug 194 | ``` 195 | 196 | Following these steps will set up a fully configured and operational SIP server integrated with LiveKit. -------------------------------------------------------------------------------- /docs/quick_start.md: -------------------------------------------------------------------------------- 1 | # Welcome 2 | 3 | Welcome to the Agent Studio Documentation. This comprehensive guide will provide you with the knowledge and tools needed to excel in your role, whether you're on the phone or engaging with customers through chat. Let's collaborate to provide outstanding customer service and achieve our shared goals. 4 | 5 | # Sign In and Sign Up 6 | 7 | This document explains how to use the **Sign In** and **Sign Up** pages for the Voice Agent platform. 8 | 9 | ![Enter image alt description](Images/HWk_Image_1.png) 10 | 11 | ## Sign In 12 | 13 | To access your Voice Agent dashboard, follow these steps: 14 | 15 | 1. **Open the Voice Agent login page**. 16 | 17 | - The login form includes fields for **Email** and **Password**. 18 | 19 | - **Enter your credentials**: 20 | 21 | - In the **Email** field, enter your registered email address. 22 | 23 | - In the **Password** field, enter 24 | 25 | - password. 26 | 27 | - You can toggle the visibility of the password by clicking the eye icon next to the password field. 28 | 29 | - **Click "Sign In"** to access your account. 30 | 31 | If you encounter any issues signing in: 32 | 33 | - **Forgot your password?** 34 | 35 | - Click the **"Forgot your password?"** link to reset your password. 36 | 37 | ## Sign Up 38 | 39 | If you don't have an account, follow these steps: 40 | 41 | 1. **Click the "Sign Up" link** located below the sign-in form. 42 | 43 | 2. **Fill out the required details**: 44 | 45 | - You will need to provide an email address and create a password. 46 | 47 | - **Complete the registration** by following the on-screen instructions to create your account. 48 | 49 | Once signed up, you can log in using the **Sign In** form. 50 | 51 | 52 | If you experience any difficulties, please contact our support team for assistance. 53 | 54 | # Creating Agents 55 | 56 | This section explains how to create and configure agents in the Voice Agent platform. Agents can be voice AI chatbots used for various integrations into your applications. You can configure these agents to fit your business needs. 57 | 58 | ## Step 1: Access the Agents Section 59 | 60 | 1. Navigate to the **Agents** tab in the Voice Agent dashboard. 61 | 62 | 2. Click on the **Create Agent** button to begin the agent creation process. 63 | 64 | ## Step 2: Define Agent Details 65 | 66 | In the "Create Agent" form, you'll be asked to provide the following details: 67 | 68 | - **Agent Name**: Enter a unique name for your agent. 69 | 70 | - **Type**: Choose between two types of agents: 71 | 72 | - **Phone**: This agent will be associated with a phone number. 73 | 74 | - **Web**: This agent will operate through a web interface, and does not require a phone number. 75 | 76 | ## Step 3: Configure Agent Based on Type 77 | 78 | ### 1. **Phone Agent**: 79 | 80 | - When selecting the **Phone** type, you must associate the agent with an existing phone number. 81 | 82 | - Ensure that the phone number has already been created on the platform (more details on creating phone numbers will be provided later). 83 | 84 | ### 2. **Web Agent**: 85 | 86 | - If you select **Web**, no phone number is required. 87 | 88 | ## Step 4: Choose a Template 89 | 90 | - You will be provided with five templates to choose from. These templates help to simplify the configuration of your agent. 91 | 92 | - Templates include basic configurations and can be further customised based on your needs. 93 | 94 | - Select any one of the templates that best suits your use case: 95 | 96 | - **Default Template**: This template comes with minimal configurations and serves as a blank slate for further customization. 97 | 98 | ## Step 5: Finalise Creation 99 | 100 | - After filling in all the necessary information and selecting a template, click on the **Create Agent** button to complete the process. 101 | 102 | ![Enter image alt description](Images/4Cj_Image_2.png) 103 | 104 | If you need further assistance creating or managing agents, please refer to the platform's support documentation or contact support. 105 | 106 | 107 | # Importing Phone Numbers 108 | 109 | In this section, you can import purchased phone numbers into the Voice Agent platform. Currently, the system supports phone number imports from **Telynx**. 110 | 111 | ## Step 1: Access the Phone Numbers Section 112 | 113 | 1. Navigate to the **Phone Numbers** tab in the Voice Agent dashboard. 114 | 115 | 2. Click on the **Import** button to begin importing a phone number. 116 | 117 | ## Step 2: Fill in the Required Fields 118 | 119 | You will need to provide the following details to import a phone number: 120 | 121 | - **Label**: Enter a label for the phone number, which will help you identify it in the platform. 122 | 123 | - **Telynx Phone Number**: Enter your phone number and make sure to select the correct country code. 124 | 125 | - **Telynx API Key**: Input the API key from your Telynx account. 126 | 127 | - **Telynx API Secret**: Input the API secret associated with your Telynx account. 128 | 129 | - **Auth Username**: Enter the authentication username provided by Telynx. 130 | 131 | - **Auth Password**: Enter the authentication password provided by Telynx. 132 | 133 | - **SIP Signaling Addresses**: Select the appropriate SIP signaling address for your region. The options are: 134 | 135 | - US 136 | 137 | - Europe 138 | 139 | - Australia 140 | 141 | - Canada 142 | 143 | ![Enter image alt description](Images/bBC_Image_3.png) 144 | 145 | Once all fields are filled out, click **Import from Telynx** to complete the process. 146 | 147 | **Note**: For details on how to set up your Telynx account and retrieve the necessary API credentials, please refer to the [Telynx Setup Guide](#telynx-setup-guide). 148 | 149 | 150 | ## Telynx Setup Guide 151 | 152 | This guide will help you set up your Telynx account, generate API keys, and manage your phone numbers for integration with Voice Agent. 153 | 154 | ## Purchase Phone Number 155 | 156 | ### Step 1: Login/Signup 157 | 158 | - Visit - [https://telynx.com/sign-up](https://telynx.com/sign-up) to Sign-up/Login to your Telynx account 159 | 160 | ![Enter image alt description](Images/ezZ_Image_4.png) 161 | 162 | ### Step 2: Choose Number 163 | 164 | - From the options available in the left column menu click on **Numbers**. 165 | 166 | ![Enter image alt description](Images/xFN_Image_5.png) 167 | 168 | ### Step 3: Add to Cart 169 | 170 | - Click on **Search and Buy Numbers**. 171 | 172 | - Now choose from the available list of numbers and add a number to your cart. 173 | 174 | ![Enter image alt description](Images/9Yp_Image_6.png) 175 | 176 | ### Step 4: Purchase 177 | 178 | - Proceed to purchase the number by clicking on the **Place Order** button. 179 | 180 | ![Enter image alt description](Images/He6_Image_7.png) 181 | 182 | 183 | ## SIP Configuration 184 | 185 | Telynx provides a step-by-step [SIP Quickstart tutorial](https://telynx.com/sip-quickstart). 186 | 187 | ### Step 1: SIP Trunking 188 | 189 | - From the left column menu click on **Voice** and head into the **SIP Trunking** option and click on **Create SIP Connection**. 190 | 191 | ![Enter image alt description](Images/eij_Image_8.png) 192 | 193 | ### Step 2: Create SIP Trunking 194 | 195 | - Once clicked on **Create SIP Connection**, enter a suitable name and choose the **FQDN** option. 196 | 197 | - Now click on **Create** and proceed to the next page. 198 | 199 | ![Enter image alt description](Images/q45_Image_9.png) 200 | 201 | ### Step 3: Inbound Configuration 202 | 203 | - Now that we have created a new SIP Connection with FQDN Authentication, click on **Add FQDN** and enter the following IP address: `216.48.180.192` and **Port**: `5060`. 204 | 205 | ![Enter image alt description](Images/GTe_Image_10.png) 206 | 207 | ### Step 4: Outbound Configuration 208 | 209 | - Select the SIP Signaling Address: Choose a **SIP Signaling Address**. 210 | 211 | - Configure Credentials Authentication: Create and manage a username and password. 212 | 213 | ![Enter image alt description](Images/ufu_Image_11.png) 214 | 215 | ### Step 5: Assign Number 216 | 217 | - We will now assign the purchased phone number to the SIP Trunk by clicking on the tab named **Numbers** under the newly created SIP Connection. 218 | 219 | - Click on **Assign Number** and select the newly purchased number and click on the **Assign Selected Number** and you will be completed with the SIP Trunking configuration. 220 | 221 | ![Enter image alt description](Images/sO1_Image_12.png) 222 | 223 | 224 | You can now head over to the platform and use the newly purchased number for automated inbound and outbound calls. 225 | 226 | # Configuring Agents 227 | 228 | Once an agent is added to the platform, you can configure various aspects of the agent. Each agent type has specific configurations, as outlined below. 229 | 230 | ![Enter image alt description](Images/4K6_Image_13.png) 231 | 232 | ### Agent Overview 233 | 234 | - All agents, regardless of type, share the same configuration sections: **Model**, **Transcriber**, **Voice**, and **Files and Chat Integration**. 235 | 236 | - When any changes are made to the input fields in these sections, the data will be automatically saved. 237 | 238 | - **Model**: Configure the large language model (LLM) settings. 239 | 240 | - **Transcriber**: Set up transcription options for handling voice inputs. 241 | 242 | - **Voice**: Configure the voice settings for the agent. 243 | 244 | - **Files**: Manage and upload files for use with the agent. 245 | 246 | - **Chat Integration**: Manage Chat Bot 247 | 248 | - **Outbound call: **This tab will be available only for agents that have been configured with a phone number. Users can make outbound calls manually. 249 | 250 | ### Model Section 251 | 252 | This section allows you to configure the agent's behaviour. You can adjust the following: 253 | 254 | - **First Message**: The greeting message displayed to users when interacting with the agent. For example, "Hi, I am your Agent!" 255 | 256 | - **System Prompt**: A directive provided to the model to shape its responses. Example: "You are a helpful agent." 257 | 258 | - **Provider**: Select the LLM provider (e.g., Groq, OpenAI, etc.). 259 | 260 | - **Model**: Choose the specific model (e.g., Meta Llama 3B). 261 | 262 | - **Temperature**: Set the model's temperature to control the creativity of responses. 263 | 264 | - **Max Tokens**: Set the limit for the maximum number of tokens the model can generate. 265 | 266 | - **Speech Speed**: Adjust the speed at which the agent speaks. 267 | 268 | ### Transcriber Section 269 | 270 | In the **Transcriber** section, configure how the agent transcribes speech input: 271 | 272 | - **Provider**: Choose the transcription service provider (e.g., Deepgram). 273 | 274 | - **Language**: Select the language for transcription (e.g., English General). 275 | 276 | ### Voice Section 277 | 278 | In the **Voice** section, configure the voice settings for the agent: 279 | 280 | - **Provider**: Choose the voice provider (e.g., OpenAI). 281 | 282 | - **Voice**: Select the voice from the available options. 283 | 284 | ### Files Section 285 | 286 | The **Files** section allows you to upload and manage files that the agent can access during interactions: 287 | 288 | - You can upload up to 4 files, each no larger than 20MB. Only PDF format is supported. 289 | 290 | - **Enable RAG**: Turn on retrieval-augmented generation (RAG) to allow the agent to retrieve information from the uploaded files. 291 | 292 | ### Chat Integration 293 | 294 | The **Chat Integration** tab provides three sections for configuring how the bot interacts with end users and how it is displayed on websites. 295 | 296 | ### 1. Bot Integration 297 | 298 | This section contains the instructions needed to integrate the bot into the user's website. Typically, this involves generating code snippets (such as JavaScript) that can be embedded directly into the website's HTML to enable the chatbot interface. 299 | 300 | - Follow the on-screen instructions to add the bot to your website. 301 | 302 | - Ensure you place the code in the correct location (e.g., the `` or `` section of your HTML) as specified. 303 | 304 | ![Enter image alt description](Images/11r_Image_14.png) 305 | 306 | ### 2. Data Collections 307 | 308 | This section allows you to configure how the bot will collect data from end users. The options here define what information the bot should request from users and how the collected data will be processed. 309 | 310 | - **Data Fields**: Specify the fields the bot should collect (e.g., name, email, feedback). 311 | 312 | - **Validation Rules**: Set rules to ensure valid data is collected (e.g., valid email format). 313 | 314 | ![Enter image alt description](Images/Njd_Image_15.png) 315 | 316 | ![Enter image alt description](Images/gbz_Image_16.png) 317 | 318 | The **Data Collections** section allows you to create and customize forms that the bot will use to collect data from end users. You can define different types of fields, each with its own specific behavior and rules. 319 | 320 | #### Available Field Types 321 | 322 | You can include the following types of fields in the form: 323 | 324 | - **Text**: A standard single-line input where users can enter text. 325 | 326 | - **Textarea**: A multi-line input for longer text responses. 327 | 328 | - **Number**: A field where users can input numbers. 329 | 330 | - **Select**: A dropdown menu where the user selects one option from a list. 331 | 332 | - **Multiselect**: A dropdown menu where users can select multiple options from a list. 333 | 334 | - **Email**: A field where users enter their email address. 335 | 336 | - **Telephone**: A field for inputting a phone number. 337 | 338 | - **Date**: A field for selecting a date from a calendar. 339 | 340 | #### Common Features of All Fields 341 | 342 | All fields come with these common settings: 343 | 344 | - **Name**: A label that identifies the field (e.g., "Full Name" or "Email"). 345 | 346 | - **Enabled**: If a field is marked as "enabled," it will be visible to the user. If not, the field can be hidden, such as after form submission or when it's no longer needed. 347 | 348 | - **Required**: If a field is "required," users must fill it in before they can submit the form. If it’s not required, they can skip the field. 349 | 350 | Each field type may have additional configurations: 351 | 352 | 1. **Text and Textarea Fields**: 353 | 354 | - You can set a limit on the number of characters the user is allowed to input. For example, a text field can be limited to a maximum of 100 characters. 355 | 356 | - **Number Fields**: 357 | 358 | - You can specify a minimum and a maximum value that the user can enter. For example, if the field is for age, you can set the minimum to 18 and the maximum to 65. 359 | 360 | - **Select and Multiselect Fields**: 361 | 362 | - These fields let users choose from a list of predefined options. For example, you could ask, "What is your favourite fruit?" and provide options like "Apple," "Banana," or "Orange." 363 | 364 | - For **multiselect**, the user can choose more than one option if needed. 365 | 366 | - It's important to have at least two options for users to choose from in select or multi select fields. 367 | 368 | By using these settings, you can create a custom form that gathers exactly the information you need from your users, while ensuring the data is valid and useful. 369 | 370 | ### 3. Bot Customisation 371 | 372 | This section provides options for customising the appearance of the bot on your website. The available fields include: 373 | 374 | ![Enter image alt description](Images/aAi_Image_17.png) 375 | 376 | ![Enter image alt description](Images/UDL_Image_19.png) 377 | 378 | - **Agent Image**: Upload a custom image for your bot, which will be displayed in the chat interface (e.g., a logo or avatar). 379 | 380 | - **Background and Text Colour**: Choose the colour of the chat window to match your brand or website. 381 | 382 | - **Bot Width and Height**: Define the width and height of the chatbot interface if needed and it must be valid CSS value. 383 | 384 | These customizations allow you to ensure the bot's user interface is consistent with your website’s design and brand identity. 385 | 386 | ### Outbound call 387 | 388 | Users can make an outbound call to the end user. 389 | 390 | ### Talk with Agent 391 | 392 | Will redirect the user to the chatbot page where the user can interact with the agent. 393 | 394 | ## Campaigns 395 | 396 | The **Campaign** section allows users to create and manage campaigns that utilize phone number agents to make automated outbound calls. This feature is designed for scenarios where the agent needs to reach out to multiple end users. 397 | 398 | ### Creating a Campaign 399 | 400 | To create a new campaign: 401 | 402 | 1. Navigate to the **Campaign** tab in the Voice Agent dashboard. 403 | 404 | 2. Click on the **Create Campaign** button. 405 | 406 | 3. Fill out the following fields in the **Create Campaign** form: 407 | 408 | - **Name**: Enter a name for your campaign. 409 | 410 | - **Description**: Optionally, provide a brief description of the campaign's purpose. 411 | 412 | - **Agent**: Select the agent for the campaign. Note that only **Phone Number Agents** are eligible for selection in campaigns. 413 | 414 | - Once the fields are completed, click **Create Campaign**. 415 | 416 | ![Enter image alt description](Images/PYD_Image_20.png) 417 | 418 | ### Adding Phone Numbers to the Campaign 419 | 420 | After creating a campaign, the next step is to add phone numbers for the agent to call. You can do this in two ways: 421 | 422 | 1. **Add Manually**: 423 | 424 | - Click the **Add Manually** button to input individual phone numbers. 425 | 426 | - This method is useful if you only need to add a few numbers. 427 | 428 | - **Import CSV**: 429 | 430 | - Click the **Import CSV** button to upload a CSV file that contains a list of phone numbers. 431 | 432 | - This option is ideal for bulk imports where many numbers need to be added at once. 433 | 434 | ### Starting a Campaign 435 | 436 | Once the phone numbers have been added: 437 | 438 | 1. Verify that the phone numbers are correctly associated with the campaign. 439 | 440 | 2. Click the **Start Campaign** button to initiate the calling process. 441 | 442 | - The selected agent will begin calling the listed phone numbers. 443 | 444 | ### Campaign Workflow 445 | 446 | - The **Phone Number Agent** selected for the campaign will make outbound calls to the phone numbers that you’ve added. 447 | 448 | - Calls can be scheduled or initiated in bulk using the campaign interface. 449 | 450 | - You can monitor the status of the campaign from the dashboard. 451 | 452 | ![Enter image alt description](Images/PfR_Image_21.png) 453 | 454 | This allows for efficient and scalable calling operations, ideal for marketing campaigns, customer outreach, or automated notifications. 455 | 456 | 457 | ## Overview 458 | 459 | The **Overview** page provides a comprehensive summary of the agent’s performance, conversations, and associated costs. You can monitor the usage and costs of both chats and calls, helping you track agent activity and budget accordingly. 460 | 461 | ![Enter image alt description](Images/PyO_Image_22.png) 462 | 463 | ### Key Metrics Displayed 464 | 465 | - **Total Conversations**: Displays the total number of conversations handled by the agents, including a breakdown of calls and chats. 466 | 467 | - **Total Call Minutes**: Shows the total duration of calls made by the agents. 468 | 469 | - **Total Spent**: Indicates the total cost incurred based on the call minutes and other usage metrics. 470 | 471 | - **Average Cost per Call**: Shows the average cost for each call made by the agent. 472 | 473 | ### Agent Performance 474 | 475 | This section breaks down the performance of individual agents: 476 | 477 | - **Agent Name**: The name of the agent. 478 | 479 | - **Call Count**: The number of calls made by the agent. 480 | 481 | - **Average Call Duration**: The average time spent per call. 482 | 483 | - **Total Cost**: The total cost associated with the agent’s activities, based on call duration. 484 | 485 | ### Conversations Breakdown 486 | 487 | This chart visualizes the number of calls and chats handled by each agent over a selected time period. 488 | 489 | - **No. of Calls**: Displays the total number of calls made by the agent. 490 | 491 | - **No. of Chats**: Displays the total number of chat interactions handled by the agent. 492 | 493 | ### Total Tokens Used 494 | 495 | This section tracks the usage of tokens for various services like large language models (LLM), speech-to-text (STT), and text-to-speech (TTS). 496 | 497 | - **LLM Tokens**: Total tokens used for language model interactions. 498 | 499 | - **STT Tokens**: Total tokens used for speech-to-text interactions. 500 | 501 | - **TTS Tokens**: Total tokens used for text-to-speech interactions. 502 | 503 | ### Agent Performance and Call Breakdown by Category 504 | 505 | - **Agent Performance**: A visual chart showing agent activity, including call duration, number of calls, and the cost incurred. 506 | 507 | - **Call Breakdown by Category**: This chart breaks down the call data by different categories like web or SIP. 508 | 509 | 510 | ## Logs 511 | 512 | The **Logs** page provides detailed records of all interactions handled by the agents, including both calls and chats. The logs are categorized based on the type of interaction: 513 | 514 | ![Enter image alt description](Images/eSe_Image_23.png) 515 | 516 | ### Log Categories 517 | 518 | - **Inbound**: Logs of all inbound calls received by the agents. 519 | 520 | - **Outbound**: Logs of all outbound calls made by the agents. 521 | 522 | - **Web**: Logs of interactions handled via web-based agents. 523 | 524 | - **Chat**: Logs of all chat conversations handled by the agents. 525 | 526 | This page is essential for tracking and reviewing the history of calls, chats, and other interactions. It also serves as a useful tool for auditing and performance monitoring of the agents. 527 | 528 | Both the **Overview** and **Logs** pages provide detailed information on the costs associated with agent activities, allowing users to track how much is being spent on calls and conversations. You can see the total call minutes, costs per call, and the total spent, giving you insights into your overall usage and expenses. 529 | 530 | 531 | ## Messaging Channels 532 | 533 | The **Messaging Channels** section allows you to integrate third-party chat platforms with your agents in this system. Currently, you can integrate with platforms like **Zendesk** and **SalesIQ**. These integrations enable your agents to handle chats initiated through these platforms directly from the Voice Agent system. 534 | 535 | ### Adding a Messaging Channel 536 | 537 | To add a messaging channel: 538 | 539 | 1. Go to the **Messaging Channels** tab. 540 | 541 | 2. Click on the **Add Channel** button in the top-right corner. 542 | 543 | 3. Select the platform you want to integrate: 544 | 545 | - **Zendesk** 546 | 547 | - **SalesIQ** 548 | 549 | ![Enter image alt description](Images/MG0_Image_24.png) 550 | 551 | ### Configuring Zendesk Channel 552 | 553 | When adding a **Zendesk Channel**, you need to provide the following information: 554 | 555 | 1. **Channel Name**: Give a name to this integration. 556 | 557 | 2. **Agent**: Select the agent you wish to associate with this messaging channel. 558 | 559 | 3. **Zendesk App ID**: Enter the App ID from your Zendesk account. 560 | 561 | 4. **Zendesk Key ID**: Provide the Key ID for authentication with Zendesk. 562 | 563 | 5. **Zendesk Secret Key**: Enter the Secret Key for secure communication. 564 | 565 | ![Enter image alt description](Images/Dns_Image_25.png) 566 | 567 | Once these details are filled, click **Create Channel** to finalize the integration. 568 | 569 | ### Configuring SalesIQ Channel 570 | 571 | When adding a **SalesIQ Channel**, you need to provide the following information: 572 | 573 | 1. **Channel Name**: Name this integration for easier management. 574 | 575 | 2. **Agent**: Select the agent you wish to link with this channel. 576 | 577 | 3. **SalesIQ Domain**: Enter your SalesIQ domain. 578 | 579 | 4. **Company Domain**: Provide the domain of the company associated with the SalesIQ account. 580 | 581 | 5. **Client ID**: Enter the Client ID for API access. 582 | 583 | 6. **Client Secret**: Provide the Client Secret for secure communication. 584 | 585 | 7. **Refresh Token**: Include the Refresh Token for maintaining the session with SalesIQ. 586 | 587 | ![Enter image alt description](Images/tyu_Image_26.png) 588 | 589 | After providing all the required information, click **Create Channel** to integrate the SalesIQ platform. 590 | 591 | 592 | These integrations allow agents to handle customer interactions initiated through external chat platforms, centralizing communication within the Voice Agent system. 593 | 594 | ### SalesIQ Chat Installation 595 | 596 | **Step 1:** 597 | 598 | 1. Log in to your SalesIQ website. Take note of your company domain name and SalesIQ domain name, which can be found in the URL. \ 599 | Example: \ 600 | `https://salesiq.zoho.in/swarmchain/onboard` - "in" is the SalesIQ domain name and example : "swarmchain" is your domain name. 601 | 602 | 2. Log in to Zoho developer/API console. Create client data as follows: 603 | 604 | - Choose **Server-based authentication** 605 | 606 | - Fill in the client name. 607 | 608 | - Use `https://localhost.com` in **Homepage URL** and **Authorized Redirect URIs**. and then save. 609 | 610 | - Note down **Client ID** and **Client Secret**. 611 | 612 | **Step 2:** 613 | 614 | 1. You need to create **refresh_token**. 615 | 616 | 2. Use the following link and paste it into your browser. Make sure to provide the correct URL details. The page will return a **code** after the authentication process. The **code** is valid for only 1 minute, so make sure the next procedure is ready for setup. 617 | 618 | ``` 619 | [https://accounts.zoho.{SalesIQ-Domain}/oauth/v2/auth?response_type=code&access_type=offline&client_id={Client-ID}&scope=SalesIQ.conversations.READ,SalesIQ.conversations.CREATE,SalesIQ.webhooks.READ,SalesIQ.webhooks.CREATE,SalesIQ.webhooks.UPDATE,SalesIQ.webhooks.DELETE,SalesIQ.Apps.READ&redirect_uri=https://localhost.com](null) 620 | 621 | 622 | After getting the **code**, you need to make an API call to retrieve **refresh_token**. You can use any platform to call the API. 623 | 624 | ``` 625 | **POST Method URL:** 626 | 627 | ``` 628 | [https://accounts.zoho.{SalesIQ-Domain}/oauth/v2/token?code={Retrieved-Code}&grant_type=authorization_code&client_id={Client-ID}&client_secret={Client-Secret}&redirect_uri=https://localhost.com&scope=SalesIQ.conversations.READ,SalesIQ.conversations.CREATE,SalesIQ.webhooks.READ,SalesIQ.webhooks.CREATE,SalesIQ.webhooks.UPDATE,SalesIQ.webhooks.DELETE,SalesIQ.Apps.READ](null) 629 | 630 | 631 | ``` 632 | **Step 3:** 633 | 634 | 1. Log in to our website and navigate to **Messaging Channels**. 635 | 636 | 2. Click on **Add Channel** and select **SalesIQ**. 637 | 638 | 3. Fill in all the fields. That's it! 639 | 640 | 641 | ### Zendesk Chat Installation 642 | 643 | **Step 1:** 644 | 645 | - Visit [Zendesk Sign-Up](https://www.zendesk.com/register) to sign up or log in to your Zendesk account. 646 | 647 | **Step 2:** 648 | 649 | - Login to your Zendesk account and click on **Zendesk Products** in the top menu. Select the **Chat** product. 650 | 651 | **Step 3:** 652 | 653 | - From the left sidebar, click on **Manage Web Widget**. This is where the chat widget is configured. 654 | 655 | - Go to **Apps & Integrations > APIs > Conversation API**. This is where we will generate the API credentials. 656 | 657 | - Click on **Create API Key** to generate new credentials for Agent Studio integration. 658 | 659 | **Step 4:** 660 | 661 | - Give a name for the API key and click **Next**. This is for reference. Copy the **App ID**, **Key ID**, and **Secret Key** values generated. These will be used to authorize Agent Studio. 662 | 663 | 664 | 665 | **Step 5:** 666 | 667 | - Log in to our website and navigate to **Messaging Channels**. 668 | 669 | - Click on **Add Channel** and select **Zendesk**. 670 | 671 | - Fill in all the fields. That's it! 672 | -------------------------------------------------------------------------------- /docs/road_map.md: -------------------------------------------------------------------------------- 1 | 2 | # Agent Studio Roadmap 3 | 4 | The **Agent Studio** team is focused on continuous improvement to enhance functionality, usability, and performance. Here are some upcoming initiatives: 5 | 6 | ### 1. Twilio SIP Service 7 | - Integrate Twilio SIP service in the UI for streamlined voice communications directly via the platform. 8 | 9 | ### 2. UI Improvements 10 | - Enhance the UI for a more intuitive and streamlined user experience. 11 | - Update the Agent Bot and Voice Agent interfaces for improved interactivity and responsiveness. 12 | - Introduce customizable themes to allow users to personalize the interface. 13 | 14 | ### 3. Backend Optimization 15 | - Optimize backend code for faster processing and response times. 16 | - Improve data handling and resource management to support larger workloads efficiently. 17 | - Implement modular, microservices-based architecture for better scalability and performance. 18 | 19 | ### 4. Code Enhancements 20 | - Regular code refactoring to improve maintainability and readability. 21 | - Enhanced error handling and logging for smoother troubleshooting and debugging. 22 | - Improve security protocols for managing API keys and sensitive data. 23 | 24 | ### 5. Video Agent Support 25 | - Integrate a **Video Agent** feature that allows real-time video interactions, supporting both voice and visual communication. 26 | - Implement screen sharing and recording functionalities for video calls, enhancing collaboration and support. 27 | 28 | ### 6. Auto Call Disconnect Feature 29 | - Add an **Auto Call Disconnect** feature to terminate inactive or timed-out calls automatically, freeing resources and improving user experience. 30 | - Customizable inactivity timeout settings to allow for flexibility in call handling. 31 | 32 | ### 7. Freshdesk and CRM Integrations 33 | - Integrate with **Freshdesk** and other popular CRM platforms to streamline customer support workflows. 34 | - Provide seamless data syncing between Agent Studio and CRM systems to manage customer information, track interactions, and automate support processes. 35 | 36 | ### 8. Improved Bot Training and Management 37 | - Add an **Interactive Bot Training** interface for easier model training and tuning. 38 | - Enable multi-language support for the bot, expanding its usability to a global audience. 39 | - Provide detailed analytics on bot interactions to improve bot training and performance tracking. 40 | 41 | ### 9. Advanced Analytics and Insights 42 | - Develop an **Analytics Dashboard** for tracking metrics such as call duration, response times, user engagement, and conversion rates. 43 | - Real-time insights for monitoring platform usage and making data-driven optimizations. 44 | 45 | ### 10. Multi-Channel Communication Support 46 | - Expand support for **SMS** and **Email** communication alongside voice and video, allowing agents to reach users through preferred channels. 47 | - Unified inbox for tracking all communication channels, making it easier for agents to manage user interactions. 48 | 49 | ### 11. Automated Transcriptions and Summaries 50 | - Implement automated transcription and call summaries for voice and video interactions, facilitating easy review and follow-up. 51 | - Option for text-based sentiment analysis on transcripts to understand user sentiment in real-time. 52 | 53 | ### 12. Auto-Scheduling and Callback Features 54 | - Add an **Auto-Scheduling** feature to allow users to book calls or callbacks based on availability. 55 | - Callback management system for prioritizing follow-ups and missed calls. 56 | 57 | ### 13. Integration with Knowledge Bases 58 | - Integrate with popular knowledge bases to allow the AI to pull answers and solutions from existing content. 59 | - Auto-suggest answers during calls, leveraging both internal and external resources to enhance support quality. 60 | 61 | --- 62 | 63 | ## Feature Comparison 64 | 65 | Below is a comparison of features available across **Agent Studio** and other similar platforms. 66 | 67 | | Feature | Bland.ai | Vapi.ai | Agent (Open Source) | 68 | |-----------------------------------|----------|---------|-------------------------| 69 | | Interruption Management | ✔️ | ✔️ | ✔️ | 70 | | AI-Powered Call Vision | ❌ | ❌ | ✔️ | 71 | | Emotional Intelligence | ❌ | ✔️ | ❌ | 72 | | Function Invocation | ✔️ | ✔️ | ❌ | 73 | | Zendesk Integration Support | ❌ | ❌ | ✔️ | 74 | | Zoho Chat Integration | ❌ | ❌ | ✔️ | 75 | | Custom Chat Form Builder | ❌ | ❌ | ✔️ | 76 | | User Privacy Controls | ❌ | ❌ | ✔️ | 77 | | Audio/Video Call Recording | ❌ | ❌ | ❌ | 78 | | Call Timeout Configuration | ✔️ | ✔️ | ❌ | 79 | | Automated Smart Call Disconnection| ✔️ | ✔️ | ❌ | 80 | | Bulk Call Management | ✔️ | ✔️ | ✔️ | 81 | | Web-Based Voice User Interface (VUI) | ✔️ | ✔️ | ✔️ | 82 | | Contextual Awareness | ✔️ | ✔️ | ✔️ | 83 | 84 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | aiodns==3.2.0 2 | aiofile==3.8.8 3 | aiohappyeyeballs==2.4.0 4 | aiohttp==3.10.5 5 | aiosignal==1.3.1 6 | annotated-types==0.7.0 7 | annoy==1.17.3 8 | anyio==4.4.0 9 | attrs==24.2.0 10 | av==13.0.0 11 | beautifulsoup4==4.12.3 12 | caio==0.9.17 13 | certifi==2024.8.30 14 | cffi==1.17.1 15 | charset-normalizer==3.3.2 16 | click==8.1.7 17 | coloredlogs==15.0.1 18 | dataclasses-json==0.6.7 19 | Deprecated==1.2.14 20 | dirtyjson==1.0.8 21 | distro==1.9.0 22 | dnspython==2.6.1 23 | fastapi==0.114.2 24 | flatbuffers==24.3.25 25 | frozenlist==1.4.1 26 | fsspec==2024.9.0 27 | greenlet==3.1.0 28 | h11==0.14.0 29 | httpcore==1.0.5 30 | httpx==0.27.2 31 | humanfriendly==10.0 32 | idna==3.9 33 | jiter==0.5.0 34 | joblib==1.4.2 35 | livekit==0.16.2 36 | livekit-agents==0.8.12 37 | livekit-api==0.7.0 38 | livekit-plugins-deepgram==0.6.7 39 | livekit-plugins-openai==0.8.3 40 | livekit-plugins-rag==0.2.2 41 | livekit-plugins-silero==0.6.4 42 | livekit-protocol==0.6.0 43 | llama-cloud==0.0.17 44 | llama-index==0.11.9 45 | llama-index-agent-openai==0.3.1 46 | llama-index-cli==0.3.1 47 | llama-index-core==0.11.9 48 | llama-index-embeddings-openai==0.2.5 49 | llama-index-indices-managed-llama-cloud==0.3.1 50 | llama-index-legacy==0.9.48.post3 51 | llama-index-llms-openai==0.2.7 52 | llama-index-multi-modal-llms-openai==0.2.1 53 | llama-index-program-openai==0.2.0 54 | llama-index-question-gen-openai==0.2.0 55 | llama-index-readers-file==0.2.1 56 | llama-index-readers-llama-parse==0.3.0 57 | llama-parse==0.5.5 58 | lxml==5.3.0 59 | marshmallow==3.22.0 60 | mpmath==1.3.0 61 | multidict==6.1.0 62 | mypy-extensions==1.0.0 63 | nest-asyncio==1.6.0 64 | networkx==3.3 65 | nltk==3.9.1 66 | numpy==1.26.4 67 | onnxruntime==1.19.2 68 | openai==1.45.0 69 | packaging==24.1 70 | pandas==2.2.2 71 | pillow==10.3.0 72 | protobuf==5.28.1 73 | psutil==5.9.8 74 | pycares==4.4.0 75 | pycparser==2.22 76 | pydantic==2.9.1 77 | pydantic_core==2.23.3 78 | PyJWT==2.9.0 79 | pymongo==4.8.0 80 | pypdf==4.3.1 81 | PyPDF2==3.0.1 82 | python-dateutil==2.9.0.post0 83 | python-docx==1.1.2 84 | python-dotenv==1.0.1 85 | python-multipart==0.0.9 86 | pytz==2024.2 87 | PyYAML==6.0.2 88 | regex==2024.9.11 89 | requests==2.32.3 90 | setuptools==74.1.2 91 | six==1.16.0 92 | sniffio==1.3.1 93 | soupsieve==2.6 94 | SQLAlchemy==2.0.34 95 | starlette==0.38.5 96 | striprtf==0.0.26 97 | sympy==1.13.2 98 | tenacity==8.5.0 99 | tiktoken==0.7.0 100 | tqdm==4.66.5 101 | types-protobuf==4.25.0.20240417 102 | typing-inspect==0.9.0 103 | typing_extensions==4.12.2 104 | tzdata==2024.1 105 | urllib3==2.2.3 106 | uvicorn==0.30.6 107 | watchfiles==0.24.0 108 | wrapt==1.16.0 109 | yarl==1.11.1 110 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | setup( 4 | name='aistudio', 5 | version='1.0.0', 6 | description='A voice assistant AI app using Livekit, LLaMA, and OpenAI integrations', 7 | author='nidum', 8 | author_email='info@nidum.ai', 9 | python_requires='>=3.12', 10 | install_requires=[ 11 | 'aiodns==3.2.0', 12 | 'aiofile==3.8.8', 13 | 'aiohappyeyeballs==2.4.0', 14 | 'aiohttp==3.10.5', 15 | 'aiosignal==1.3.1', 16 | 'annotated-types==0.7.0', 17 | 'annoy==1.17.3', 18 | 'anyio==4.4.0', 19 | 'attrs==24.2.0', 20 | 'av==13.0.0', 21 | 'beautifulsoup4==4.12.3', 22 | 'caio==0.9.17', 23 | 'certifi==2024.8.30', 24 | 'cffi==1.17.1', 25 | 'charset-normalizer==3.3.2', 26 | 'click==8.1.7', 27 | 'coloredlogs==15.0.1', 28 | 'dataclasses-json==0.6.7', 29 | 'Deprecated==1.2.14', 30 | 'dirtyjson==1.0.8', 31 | 'distro==1.9.0', 32 | 'dnspython==2.6.1', 33 | 'fastapi==0.114.2', 34 | 'flatbuffers==24.3.25', 35 | 'frozenlist==1.4.1', 36 | 'fsspec==2024.9.0', 37 | 'greenlet==3.1.0', 38 | 'h11==0.14.0', 39 | 'httpcore==1.0.5', 40 | 'httpx==0.27.2', 41 | 'humanfriendly==10.0', 42 | 'idna==3.9', 43 | 'jiter==0.5.0', 44 | 'joblib==1.4.2', 45 | 'livekit==0.16.2', 46 | 'livekit-agents==0.8.12', 47 | 'livekit-api==0.7.0', 48 | 'livekit-plugins-deepgram==0.6.7', 49 | 'livekit-plugins-openai==0.8.3', 50 | 'livekit-plugins-rag==0.2.2', 51 | 'livekit-plugins-silero==0.6.4', 52 | 'livekit-protocol==0.6.0', 53 | 'llama-cloud==0.0.17', 54 | 'llama-index==0.11.9', 55 | 'llama-index-agent-openai==0.3.1', 56 | 'llama-index-cli==0.3.1', 57 | 'llama-index-core==0.11.9', 58 | 'llama-index-embeddings-openai==0.2.5', 59 | 'llama-index-indices-managed-llama-cloud==0.3.1', 60 | 'llama-index-legacy==0.9.48.post3', 61 | 'llama-index-llms-openai==0.2.7', 62 | 'llama-index-multi-modal-llms-openai==0.2.1', 63 | 'llama-index-program-openai==0.2.0', 64 | 'llama-index-question-gen-openai==0.2.0', 65 | 'llama-index-readers-file==0.2.1', 66 | 'llama-index-readers-llama-parse==0.3.0', 67 | 'llama-parse==0.5.5', 68 | 'lxml==5.3.0', 69 | 'marshmallow==3.22.0', 70 | 'mpmath==1.3.0', 71 | 'multidict==6.1.0', 72 | 'mypy-extensions==1.0.0', 73 | 'nest-asyncio==1.6.0', 74 | 'networkx==3.3', 75 | 'nltk==3.9.1', 76 | 'numpy==1.26.4', 77 | 'onnxruntime==1.19.2', 78 | 'openai==1.45.0', 79 | 'packaging==24.1', 80 | 'pandas==2.2.2', 81 | 'pillow==10.3.0', 82 | 'protobuf==5.28.1', 83 | 'psutil==5.9.8', 84 | 'pycares==4.4.0', 85 | 'pycparser==2.22', 86 | 'pydantic==2.9.1', 87 | 'pydantic_core==2.23.3', 88 | 'PyJWT==2.9.0', 89 | 'pymongo==4.8.0', 90 | 'pypdf==4.3.1', 91 | 'PyPDF2==3.0.1', 92 | 'python-dateutil==2.9.0.post0', 93 | 'python-docx==1.1.2', 94 | 'python-dotenv==1.0.1', 95 | 'python-multipart==0.0.9', 96 | 'pytz==2024.2', 97 | 'PyYAML==6.0.2', 98 | 'regex==2024.9.11', 99 | 'requests==2.32.3', 100 | 'setuptools==74.1.2', 101 | 'six==1.16.0', 102 | 'sniffio==1.3.1', 103 | 'soupsieve==2.6', 104 | 'SQLAlchemy==2.0.34', 105 | 'starlette==0.38.5', 106 | 'striprtf==0.0.26', 107 | 'sympy==1.13.2', 108 | 'tenacity==8.5.0', 109 | 'tiktoken==0.7.0', 110 | 'tqdm==4.66.5', 111 | 'types-protobuf==4.25.0.20240417', 112 | 'typing-inspect==0.9.0', 113 | 'typing_extensions==4.12.2', 114 | 'tzdata==2024.1', 115 | 'urllib3==2.2.3', 116 | 'uvicorn==0.30.6', 117 | 'watchfiles==0.24.0', 118 | 'wrapt==1.16.0', 119 | 'yarl==1.11.1', 120 | 'marshmallow==3.22.0', 121 | 'mpmath==1.3.0', 122 | 'multidict==6.1.0', 123 | 'mypy-extensions==1.0.0', 124 | 'nest-asyncio==1.6.0', 125 | 'networkx==3.3', 126 | 'nltk==3.9.1', 127 | 'numpy==1.26.4', 128 | 'onnxruntime==1.19.2', 129 | 'openai==1.45.0', 130 | 'packaging==24.1', 131 | 'pandas==2.2.2', 132 | 'pillow==10.3.0', 133 | 'protobuf==5.28.1', 134 | 'psutil==5.9.8', 135 | 'pycares==4.4.0', 136 | 'pycparser==2.22', 137 | 'pydantic==2.9.1', 138 | 'pydantic_core==2.23.3', 139 | 'PyJWT==2.9.0', 140 | 'pymongo==4.8.0', 141 | 'pypdf==4.3.1', 142 | 'PyPDF2==3.0.1', 143 | 'python-dateutil==2.9.0.post0', 144 | 'python-docx==1.1.2', 145 | 'python-dotenv==1.0.1', 146 | 'python-multipart==0.0.9', 147 | 'pytz==2024.2', 148 | 'PyYAML==6.0.2', 149 | 'regex==2024.9.11', 150 | 'requests==2.32.3', 151 | 'setuptools==74.1.2', 152 | 'six==1.16.0', 153 | 'sniffio==1.3.1', 154 | 'soupsieve==2.6', 155 | 'SQLAlchemy==2.0.34', 156 | 'starlette==0.38.5', 157 | 'striprtf==0.0.26', 158 | 'sympy==1.13.2', 159 | 'tenacity==8.5.0', 160 | 'tiktoken==0.7.0', 161 | 'tqdm==4.66.5', 162 | 'types-protobuf==4.25.0.20240417', 163 | 'typing-inspect==0.9.0', 164 | 'typing_extensions==4.12.2', 165 | 'tzdata==2024.1', 166 | 'urllib3==2.2.3', 167 | 'uvicorn==0.30.6', 168 | 'watchfiles==0.24.0', 169 | 'wrapt==1.16.0', 170 | 'yarl==1.11.1' 171 | ], 172 | packages=find_packages(), 173 | include_package_data=True, 174 | entry_points={ 175 | 'console_scripts': [ 176 | 'aistudio=app.cli:main', 177 | ], 178 | }, 179 | 180 | classifiers=[ 181 | 'Programming Language :: Python :: 3.12', 182 | 'License :: OSI Approved :: MIT License', 183 | 'Operating System :: OS Independent', 184 | ], 185 | ) 186 | 187 | --------------------------------------------------------------------------------