├── .github
└── workflows
│ ├── publish-pypi.yml
│ ├── publish-test.yml
│ └── test.yml
├── .gitignore
├── CHANGELOG.md
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── dev-requirements.txt
├── editor
├── .eslintrc.json
├── .prettierrc
├── css
│ └── tailwind.css
├── examples
│ ├── food_ordering.json
│ ├── movie_explorer.json
│ ├── patient_intake.json
│ ├── restaurant_reservation.json
│ └── travel_planner.json
├── favicon.png
├── favicon.svg
├── index.html
├── js
│ ├── editor
│ │ ├── canvas.js
│ │ ├── editorState.js
│ │ ├── sidePanel.js
│ │ └── toolbar.js
│ ├── main.js
│ ├── nodes
│ │ ├── baseNode.js
│ │ ├── endNode.js
│ │ ├── flowNode.js
│ │ ├── functionNode.js
│ │ ├── index.js
│ │ ├── mergeNode.js
│ │ └── startNode.js
│ ├── types.js
│ └── utils
│ │ ├── export.js
│ │ ├── helpers.js
│ │ ├── import.js
│ │ └── validation.js
├── jsdoc.json
├── package-lock.json
├── package.json
├── postcss.config.cjs
├── public
│ ├── favicon.png
│ └── favicon.svg
├── tailwind.config.cjs
├── vercel.json
└── vite.config.js
├── env.example
├── examples
├── assets
│ └── hold_music
│ │ ├── README.md
│ │ ├── hold_music.py
│ │ └── hold_music.wav
├── dynamic
│ ├── insurance_anthropic.py
│ ├── insurance_aws_bedrock.py
│ ├── insurance_gemini.py
│ ├── insurance_openai.py
│ ├── restaurant_reservation.py
│ └── warm_transfer.py
├── runner.py
└── static
│ ├── food_ordering.py
│ ├── movie_explorer_anthropic.py
│ ├── movie_explorer_gemini.py
│ ├── movie_explorer_openai.py
│ ├── patient_intake_anthropic.py
│ ├── patient_intake_aws_bedrock.py
│ ├── patient_intake_gemini.py
│ ├── patient_intake_openai.py
│ └── travel_planner.py
├── images
└── food-ordering-flow.png
├── pipecat-flows.png
├── pyproject.toml
├── requirements.txt
├── src
└── pipecat_flows
│ ├── __init__.py
│ ├── actions.py
│ ├── adapters.py
│ ├── exceptions.py
│ ├── manager.py
│ └── types.py
├── test-requirements.txt
└── tests
├── __init__.py
├── test_actions.py
├── test_adapters.py
├── test_context_strategies.py
└── test_manager.py
/.github/workflows/publish-pypi.yml:
--------------------------------------------------------------------------------
1 | name: Publish to PyPI
2 |
3 | on:
4 | workflow_dispatch:
5 | inputs:
6 | version:
7 | description: 'Version to publish (must match pyproject.toml, e.g. 0.0.4)'
8 | required: true
9 | type: string
10 |
11 | jobs:
12 | build-and-publish:
13 | runs-on: ubuntu-latest
14 | environment:
15 | name: pypi
16 | url: https://pypi.org/p/pipecat-ai-flows
17 | permissions:
18 | id-token: write # IMPORTANT: this permission is needed for trusted publishing
19 | contents: write # Needed for tagging
20 |
21 | steps:
22 | - name: Checkout code
23 | uses: actions/checkout@v4
24 |
25 | - name: Set up Python
26 | uses: actions/setup-python@v4
27 | with:
28 | python-version: '3.10'
29 |
30 | - name: Verify version matches
31 | run: |
32 | pip install tomli
33 | PROJECT_VERSION=$(python -c "import tomli; print(tomli.load(open('pyproject.toml', 'rb'))['project']['version'])")
34 | if [ "$PROJECT_VERSION" != "${{ github.event.inputs.version }}" ]; then
35 | echo "Error: Input version (${{ github.event.inputs.version }}) does not match pyproject.toml version ($PROJECT_VERSION)"
36 | exit 1
37 | fi
38 |
39 | - name: Install dependencies
40 | run: |
41 | python -m pip install --upgrade pip
42 | pip install -r requirements.txt
43 | pip install build twine
44 |
45 | - name: Build package
46 | run: python -m build
47 |
48 | - name: Create release tag
49 | run: |
50 | git config --local user.email "action@github.com"
51 | git config --local user.name "GitHub Action"
52 | git tag -a "v${{ github.event.inputs.version }}" -m "Version ${{ github.event.inputs.version }}"
53 | git push origin "v${{ github.event.inputs.version }}"
54 |
55 | - name: Publish to PyPI
56 | uses: pypa/gh-action-pypi-publish@release/v1
57 | with:
58 | verbose: true
59 | print-hash: true
60 |
--------------------------------------------------------------------------------
/.github/workflows/publish-test.yml:
--------------------------------------------------------------------------------
1 | name: Publish to TestPyPI
2 |
3 | on:
4 | workflow_dispatch:
5 | inputs:
6 | version:
7 | description: 'Version to publish (must match pyproject.toml, e.g. 0.0.4)'
8 | required: true
9 | type: string
10 |
11 | jobs:
12 | build-and-publish:
13 | runs-on: ubuntu-latest
14 | environment:
15 | name: testpypi
16 | url: https://test.pypi.org/p/pipecat-ai-flows
17 | permissions:
18 | id-token: write # IMPORTANT: this permission is needed for trusted publishing
19 |
20 | steps:
21 | - name: Checkout code
22 | uses: actions/checkout@v4
23 |
24 | - name: Set up Python
25 | uses: actions/setup-python@v4
26 | with:
27 | python-version: '3.10'
28 |
29 | - name: Verify version matches
30 | run: |
31 | pip install tomli
32 | PROJECT_VERSION=$(python -c "import tomli; print(tomli.load(open('pyproject.toml', 'rb'))['project']['version'])")
33 | if [ "$PROJECT_VERSION" != "${{ github.event.inputs.version }}" ]; then
34 | echo "Error: Input version (${{ github.event.inputs.version }}) does not match pyproject.toml version ($PROJECT_VERSION)"
35 | exit 1
36 | fi
37 |
38 | - name: Install dependencies
39 | run: |
40 | python -m pip install --upgrade pip
41 | pip install -r requirements.txt
42 | pip install build twine
43 |
44 | - name: Build package
45 | run: python -m build
46 |
47 | - name: Publish to TestPyPI
48 | uses: pypa/gh-action-pypi-publish@release/v1
49 | with:
50 | repository-url: https://test.pypi.org/legacy/
51 | verbose: true
52 | print-hash: true
53 |
--------------------------------------------------------------------------------
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
1 | name: test
2 |
3 | on:
4 | workflow_dispatch:
5 | push:
6 | branches:
7 | - main
8 | pull_request:
9 | branches:
10 | - '**'
11 |
12 | concurrency:
13 | group: build-test-${{ github.event.pull_request.number || github.ref }}
14 | cancel-in-progress: true
15 |
16 | jobs:
17 | test:
18 | name: 'Unit Tests'
19 | runs-on: ubuntu-latest
20 | steps:
21 | - name: Checkout repo
22 | uses: actions/checkout@v4
23 |
24 | - name: Set up Python
25 | id: setup_python
26 | uses: actions/setup-python@v4
27 | with:
28 | python-version: '3.10'
29 |
30 | - name: Cache virtual environment
31 | uses: actions/cache@v3
32 | with:
33 | key: venv-${{ runner.os }}-${{ steps.setup_python.outputs.python-version}}-${{ hashFiles('dev-requirements.txt') }}-${{ hashFiles('test-requirements.txt') }}
34 | path: .venv
35 |
36 | - name: Setup virtual environment
37 | run: |
38 | python -m venv .venv
39 |
40 | - name: Install dependencies
41 | run: |
42 | source .venv/bin/activate
43 | python -m pip install --upgrade pip
44 | pip install -r dev-requirements.txt -r test-requirements.txt
45 | pip uninstall -y google-generativeai google-genai google-ai-generativelanguage
46 | pip install "google-genai==0.7.0"
47 | pip install "pipecat-ai[google,openai,anthropic]"
48 | pip install -e .
49 |
50 | - name: Test with pytest
51 | run: |
52 | source .venv/bin/activate
53 | pytest tests/ --cov=pipecat_flows
54 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Dependencies
2 | node_modules/
3 | npm-debug.log*
4 | yarn-debug.log*
5 | yarn-error.log*
6 | .pnpm-debug.log*
7 |
8 | # Production build
9 | dist/
10 | build/
11 |
12 | # IDE - VSCode
13 | .vscode/
14 | *.code-workspace
15 | .history
16 |
17 | # IDE - WebStorm/IntelliJ
18 | .idea/
19 | *.iml
20 | *.iws
21 | *.ipr
22 |
23 | # IDE - Sublime Text
24 | *.sublime-workspace
25 | *.sublime-project
26 |
27 | # macOS
28 | .DS_Store
29 | .AppleDouble
30 | .LSOverride
31 | ._*
32 |
33 | # Windows
34 | Thumbs.db
35 | Thumbs.db:encryptable
36 | ehthumbs.db
37 | ehthumbs_vista.db
38 | *.lnk
39 |
40 | # Local development settings
41 | .env
42 | .env.local
43 | .env.development.local
44 | .env.test.local
45 | .env.production.local
46 |
47 | # Debug logs
48 | logs/
49 | *.log
50 | npm-debug.log*
51 |
52 | # Optional: Cache directories
53 | .cache/
54 | .npm/
55 | .eslintcache
56 |
57 | # Optional: Editor directories and files
58 | .project
59 | .settings/
60 | *.suo
61 | *.ntvs*
62 | *.njsproj
63 | *.sln
64 | *.sw?
65 |
66 | # JSDoc documentation
67 | docs/
68 |
69 |
70 | # Python
71 | __pycache__/
72 | *.py[cod]
73 | *$py.class
74 | *.so
75 | .Python
76 | develop-eggs/
77 | downloads/
78 | eggs/
79 | .eggs/
80 | lib/
81 | lib64/
82 | parts/
83 | sdist/
84 | var/
85 | wheels/
86 | *.egg-info/
87 | .installed.cfg
88 | *.egg
89 | MANIFEST
90 | .python-version
91 | *.pyc
92 |
93 | # Virtual Environment
94 | venv/
95 | ENV/
96 | env/
97 | .env/
98 | .venv/
99 | env.bak/
100 | venv.bak/
101 |
102 | # Testing
103 | .tox/
104 | .coverage
105 | .coverage.*
106 | .cache
107 | nosetests.xml
108 | coverage.xml
109 | *.cover
110 | .hypothesis/
111 | .pytest_cache/
112 | htmlcov/
113 |
114 | # mypy
115 | .mypy_cache/
116 | .dmypy.json
117 | dmypy.json
118 |
119 | # Jupyter Notebook
120 | .ipynb_checkpoints
121 |
122 | # IPython
123 | profile_default/
124 | ipython_config.py
125 |
126 | # Spyder project settings
127 | .spyderproject
128 | .spyproject
129 |
130 | # Rope project settings
131 | .ropeproject
132 |
133 | # Distribution / packaging
134 | .Python
135 | build/
136 | develop-eggs/
137 | dist/
138 | downloads/
139 | eggs/
140 | .eggs/
141 | lib/
142 | lib64/
143 | parts/
144 | sdist/
145 | var/
146 | wheels/
147 | share/python-wheels/
148 | *.egg-info/
149 | .installed.cfg
150 | *.egg
151 | MANIFEST
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | ## Contributing to Pipecat Flows
2 |
3 | We welcome contributions of all kinds! Your help is appreciated. Follow these steps to get involved:
4 |
5 | 1. **Fork this repository**: Start by forking the Pipecat Flows repository to your GitHub account.
6 |
7 | 2. **Clone the repository**: Clone your forked repository to your local machine.
8 | ```bash
9 | git clone https://github.com/your-username/pipecat-flows
10 | ```
11 | 3. **Create a branch**: For your contribution, create a new branch.
12 | ```bash
13 | git checkout -b your-branch-name
14 | ```
15 | 4. **Make your changes**: Edit or add files as necessary.
16 | 5. **Test your changes**: Ensure that your changes look correct and follow the style set in the codebase.
17 | 6. **Commit your changes**: Once you're satisfied with your changes, commit them with a meaningful message.
18 |
19 | ```bash
20 | git commit -m "Description of your changes"
21 | ```
22 |
23 | 7. **Push your changes**: Push your branch to your forked repository.
24 |
25 | ```bash
26 | git push origin your-branch-name
27 | ```
28 |
29 | 9. **Submit a Pull Request (PR)**: Open a PR from your forked repository to the main branch of this repo.
30 | > Important: Describe the changes you've made clearly!
31 |
32 | Our maintainers will review your PR, and once everything is good, your contributions will be merged!
33 |
34 | # Contributor Covenant Code of Conduct
35 |
36 | ## Our Pledge
37 |
38 | We as members, contributors, and leaders pledge to make participation in our
39 | community a harassment-free experience for everyone, regardless of age, body
40 | size, visible or invisible disability, ethnicity, sex characteristics, gender
41 | identity and expression, level of experience, education, socio-economic status,
42 | nationality, personal appearance, race, caste, color, religion, or sexual
43 | identity and orientation.
44 |
45 | We pledge to act and interact in ways that contribute to an open, welcoming,
46 | diverse, inclusive, and healthy community.
47 |
48 | ## Our Standards
49 |
50 | Examples of behavior that contributes to a positive environment for our
51 | community include:
52 |
53 | - Demonstrating empathy and kindness toward other people
54 | - Being respectful of differing opinions, viewpoints, and experiences
55 | - Giving and gracefully accepting constructive feedback
56 | - Accepting responsibility and apologizing to those affected by our mistakes,
57 | and learning from the experience
58 | - Focusing on what is best not just for us as individuals, but for the overall
59 | community
60 |
61 | Examples of unacceptable behavior include:
62 |
63 | - The use of sexualized language or imagery, and sexual attention or advances of
64 | any kind
65 | - Trolling, insulting or derogatory comments, and personal or political attacks
66 | - Public or private harassment
67 | - Publishing others' private information, such as a physical or email address,
68 | without their explicit permission
69 | - Other conduct which could reasonably be considered inappropriate in a
70 | professional setting
71 |
72 | ## Enforcement Responsibilities
73 |
74 | Community leaders are responsible for clarifying and enforcing our standards of
75 | acceptable behavior and will take appropriate and fair corrective action in
76 | response to any behavior that they deem inappropriate, threatening, offensive,
77 | or harmful.
78 |
79 | Community leaders have the right and responsibility to remove, edit, or reject
80 | comments, commits, code, wiki edits, issues, and other contributions that are
81 | not aligned to this Code of Conduct, and will communicate reasons for moderation
82 | decisions when appropriate.
83 |
84 | ## Scope
85 |
86 | This Code of Conduct applies within all community spaces, and also applies when
87 | an individual is officially representing the community in public spaces.
88 | Examples of representing our community include using an official email address,
89 | posting via an official social media account, or acting as an appointed
90 | representative at an online or offline event.
91 |
92 | ## Enforcement
93 |
94 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
95 | reported to the community leaders responsible for enforcement at pipecat-ai@daily.co.
96 | All complaints will be reviewed and investigated promptly and fairly.
97 |
98 | All community leaders are obligated to respect the privacy and security of the
99 | reporter of any incident.
100 |
101 | ## Enforcement Guidelines
102 |
103 | Community leaders will follow these Community Impact Guidelines in determining
104 | the consequences for any action they deem in violation of this Code of Conduct:
105 |
106 | ### 1. Correction
107 |
108 | **Community Impact**: Use of inappropriate language or other behavior deemed
109 | unprofessional or unwelcome in the community.
110 |
111 | **Consequence**: A private, written warning from community leaders, providing
112 | clarity around the nature of the violation and an explanation of why the
113 | behavior was inappropriate. A public apology may be requested.
114 |
115 | ### 2. Warning
116 |
117 | **Community Impact**: A violation through a single incident or series of
118 | actions.
119 |
120 | **Consequence**: A warning with consequences for continued behavior. No
121 | interaction with the people involved, including unsolicited interaction with
122 | those enforcing the Code of Conduct, for a specified period of time. This
123 | includes avoiding interactions in community spaces as well as external channels
124 | like social media. Violating these terms may lead to a temporary or permanent
125 | ban.
126 |
127 | ### 3. Temporary Ban
128 |
129 | **Community Impact**: A serious violation of community standards, including
130 | sustained inappropriate behavior.
131 |
132 | **Consequence**: A temporary ban from any sort of interaction or public
133 | communication with the community for a specified period of time. No public or
134 | private interaction with the people involved, including unsolicited interaction
135 | with those enforcing the Code of Conduct, is allowed during this period.
136 | Violating these terms may lead to a permanent ban.
137 |
138 | ### 4. Permanent Ban
139 |
140 | **Community Impact**: Demonstrating a pattern of violation of community
141 | standards, including sustained inappropriate behavior, harassment of an
142 | individual, or aggression toward or disparagement of classes of individuals.
143 |
144 | **Consequence**: A permanent ban from any sort of public interaction within the
145 | community.
146 |
147 | ## Attribution
148 |
149 | This Code of Conduct is adapted from the [Contributor Covenant][homepage],
150 | version 2.1, available at
151 | [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].
152 |
153 | Community Impact Guidelines were inspired by
154 | [Mozilla's code of conduct enforcement ladder][Mozilla CoC].
155 |
156 | For answers to common questions about this code of conduct, see the FAQ at
157 | [https://www.contributor-covenant.org/faq][FAQ]. Translations are available at
158 | [https://www.contributor-covenant.org/translations][translations].
159 |
160 | [homepage]: https://www.contributor-covenant.org
161 | [v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
162 | [Mozilla CoC]: https://github.com/mozilla/diversity
163 | [FAQ]: https://www.contributor-covenant.org/faq
164 | [translations]: https://www.contributor-covenant.org/translations
165 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | BSD 2-Clause License
2 |
3 | Copyright (c) 2024, Daily
4 |
5 | Redistribution and use in source and binary forms, with or without
6 | modification, are permitted provided that the following conditions are met:
7 |
8 | 1. Redistributions of source code must retain the above copyright notice, this
9 | list of conditions and the following disclaimer.
10 |
11 | 2. Redistributions in binary form must reproduce the above copyright notice,
12 | this list of conditions and the following disclaimer in the documentation
13 | and/or other materials provided with the distribution.
14 |
15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
16 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
17 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
18 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
19 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
20 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
21 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
22 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
23 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
24 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
25 |
--------------------------------------------------------------------------------
/dev-requirements.txt:
--------------------------------------------------------------------------------
1 | build~=1.2.1
2 | pip-tools~=7.4.1
3 | pytest~=8.3.2
4 | pytest-asyncio~=0.23.5
5 | pytest-cov~=4.1.0
6 | ruff~=0.9.1
7 | setuptools~=72.2.0
8 | python-dotenv~=1.0.1
--------------------------------------------------------------------------------
/editor/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["next", "prettier"],
3 | "rules": {
4 | "import/order": [
5 | "error",
6 | {
7 | "groups": [
8 | ["builtin", "external"],
9 | "internal",
10 | "parent",
11 | "sibling",
12 | "index",
13 | "object",
14 | "type"
15 | ],
16 | "newlines-between": "always"
17 | }
18 | ]
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/editor/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "semi": true,
3 | "tabWidth": 2,
4 | "useTabs": false,
5 | "singleQuote": false
6 | }
7 |
--------------------------------------------------------------------------------
/editor/css/tailwind.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | @layer components {
6 | .json-error-message {
7 | @apply text-error text-sm mt-1 mb-2;
8 | }
9 |
10 | .textarea-invalid {
11 | @apply textarea-error;
12 | }
13 |
14 | .textarea-unsaved {
15 | @apply border-dashed;
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/editor/examples/food_ordering.json:
--------------------------------------------------------------------------------
1 | {
2 | "initial_node": "start",
3 | "nodes": {
4 | "start": {
5 | "role_messages": [
6 | {
7 | "role": "system",
8 | "content": "You are an order-taking assistant. You must ALWAYS use the available functions to progress the conversation. This is a phone conversation and your responses will be converted to audio. Keep the conversation friendly, casual, and polite. Avoid outputting special characters and emojis."
9 | }
10 | ],
11 | "task_messages": [
12 | {
13 | "role": "system",
14 | "content": "For this step, ask the user if they want pizza or sushi, and wait for them to use a function to choose. Start off by greeting them. Be friendly and casual; you're taking an order for food over the phone."
15 | }
16 | ],
17 | "functions": [
18 | {
19 | "type": "function",
20 | "function": {
21 | "name": "choose_pizza",
22 | "description": "User wants to order pizza. Let's get that order started.",
23 | "parameters": {
24 | "type": "object",
25 | "properties": {}
26 | },
27 | "transition_to": "choose_pizza"
28 | }
29 | },
30 | {
31 | "type": "function",
32 | "function": {
33 | "name": "choose_sushi",
34 | "description": "User wants to order sushi. Let's get that order started.",
35 | "parameters": {
36 | "type": "object",
37 | "properties": {}
38 | },
39 | "transition_to": "choose_sushi"
40 | }
41 | }
42 | ]
43 | },
44 | "choose_pizza": {
45 | "task_messages": [
46 | {
47 | "role": "system",
48 | "content": "You are handling a pizza order. Use the available functions:\n\n- Use select_pizza_order when the user specifies both size AND type\n\n- Use confirm_order when the user confirms they are satisfied with their selection\n\nPricing:\n\n- Small: $10\n\n- Medium: $15\n\n- Large: $20\n\nAfter selection, confirm both the size and type, state the price, and ask if they want to confirm their order. Remember to be friendly and casual."
49 | }
50 | ],
51 | "functions": [
52 | {
53 | "type": "function",
54 | "function": {
55 | "name": "select_pizza_order",
56 | "handler": "__function__:select_pizza_order",
57 | "description": "Record the pizza order details",
58 | "parameters": {
59 | "type": "object",
60 | "properties": {
61 | "size": {
62 | "type": "string",
63 | "enum": ["small", "medium", "large"],
64 | "description": "Size of the pizza"
65 | },
66 | "type": {
67 | "type": "string",
68 | "enum": ["pepperoni", "cheese", "supreme", "vegetarian"],
69 | "description": "Type of pizza"
70 | }
71 | },
72 | "required": ["size", "type"]
73 | }
74 | }
75 | },
76 | {
77 | "type": "function",
78 | "function": {
79 | "name": "confirm_order",
80 | "description": "Proceed to order confirmation",
81 | "parameters": {
82 | "type": "object",
83 | "properties": {}
84 | },
85 | "transition_to": "confirm"
86 | }
87 | }
88 | ]
89 | },
90 | "choose_sushi": {
91 | "task_messages": [
92 | {
93 | "role": "system",
94 | "content": "You are handling a sushi order. Use the available functions:\n\n- Use select_sushi_order when the user specifies both count AND type\n\n- Use confirm_order when the user confirms they are satisfied with their selection\n\nPricing:\n\n- $8 per roll\n\nAfter selection, confirm both the count and type, state the price, and ask if they want to confirm their order. Remember to be friendly and casual."
95 | }
96 | ],
97 | "functions": [
98 | {
99 | "type": "function",
100 | "function": {
101 | "name": "select_sushi_order",
102 | "handler": "__function__:select_sushi_order",
103 | "description": "Record the sushi order details",
104 | "parameters": {
105 | "type": "object",
106 | "properties": {
107 | "count": {
108 | "type": "integer",
109 | "minimum": 1,
110 | "maximum": 10,
111 | "description": "Number of rolls to order"
112 | },
113 | "type": {
114 | "type": "string",
115 | "enum": ["california", "spicy tuna", "rainbow", "dragon"],
116 | "description": "Type of sushi roll"
117 | }
118 | },
119 | "required": ["count", "type"]
120 | }
121 | }
122 | },
123 | {
124 | "type": "function",
125 | "function": {
126 | "name": "confirm_order",
127 | "description": "Proceed to order confirmation",
128 | "parameters": {
129 | "type": "object",
130 | "properties": {}
131 | },
132 | "transition_to": "confirm"
133 | }
134 | }
135 | ]
136 | },
137 | "confirm": {
138 | "task_messages": [
139 | {
140 | "role": "system",
141 | "content": "Read back the complete order details to the user and ask for final confirmation. Use the available functions:\n\n- Use complete_order when the user confirms\n\n- Use revise_order if they want to change something\n\nBe friendly and clear when reading back the order details."
142 | }
143 | ],
144 | "functions": [
145 | {
146 | "type": "function",
147 | "function": {
148 | "name": "complete_order",
149 | "description": "User confirms the order is correct",
150 | "parameters": {
151 | "type": "object",
152 | "properties": {}
153 | },
154 | "transition_to": "end"
155 | }
156 | }
157 | ]
158 | },
159 | "end": {
160 | "task_messages": [
161 | {
162 | "role": "system",
163 | "content": "Concisely end the conversation—1-3 words is appropriate. Just say 'Bye' or something similarly short."
164 | }
165 | ],
166 | "functions": [],
167 | "post_actions": [
168 | {
169 | "type": "end_conversation"
170 | }
171 | ]
172 | }
173 | }
174 | }
175 |
--------------------------------------------------------------------------------
/editor/examples/movie_explorer.json:
--------------------------------------------------------------------------------
1 | {
2 | "initial_node": "greeting",
3 | "nodes": {
4 | "greeting": {
5 | "role_messages": [
6 | {
7 | "role": "system",
8 | "content": "You are a friendly movie expert. Your responses will be converted to audio, so avoid special characters. Always use the available functions to progress the conversation naturally."
9 | }
10 | ],
11 | "task_messages": [
12 | {
13 | "role": "system",
14 | "content": "Start by greeting the user and asking if they'd like to know about movies currently in theaters or upcoming releases. Wait for their choice before using either get_current_movies or get_upcoming_movies."
15 | }
16 | ],
17 | "functions": [
18 | {
19 | "type": "function",
20 | "function": {
21 | "name": "get_current_movies",
22 | "handler": "__function__:get_movies",
23 | "description": "Fetch movies currently playing in theaters",
24 | "parameters": {
25 | "type": "object",
26 | "properties": {}
27 | },
28 | "transition_to": "explore_movie"
29 | }
30 | },
31 | {
32 | "type": "function",
33 | "function": {
34 | "name": "get_upcoming_movies",
35 | "handler": "__function__:get_upcoming_movies",
36 | "description": "Fetch movies coming soon to theaters",
37 | "parameters": {
38 | "type": "object",
39 | "properties": {}
40 | },
41 | "transition_to": "explore_movie"
42 | }
43 | }
44 | ]
45 | },
46 | "explore_movie": {
47 | "task_messages": [
48 | {
49 | "role": "system",
50 | "content": "Help the user learn more about movies. You can:\n\n- Use get_movie_details when they express interest in a specific movie\n\n- Use get_similar_movies to show recommendations\n\n- Use get_current_movies to see what's playing now\n\n- Use get_upcoming_movies to see what's coming soon\n\n- Use end_conversation when they're done exploring\n\nAfter showing details or recommendations, ask if they'd like to explore another movie or end the conversation."
51 | }
52 | ],
53 | "functions": [
54 | {
55 | "type": "function",
56 | "function": {
57 | "name": "get_movie_details",
58 | "handler": "__function__:get_movie_details",
59 | "description": "Get details about a specific movie including cast",
60 | "parameters": {
61 | "type": "object",
62 | "properties": {
63 | "movie_id": {
64 | "type": "integer",
65 | "description": "TMDB movie ID"
66 | }
67 | },
68 | "required": ["movie_id"]
69 | }
70 | }
71 | },
72 | {
73 | "type": "function",
74 | "function": {
75 | "name": "get_similar_movies",
76 | "handler": "__function__:get_similar_movies",
77 | "description": "Get similar movies as recommendations",
78 | "parameters": {
79 | "type": "object",
80 | "properties": {
81 | "movie_id": {
82 | "type": "integer",
83 | "description": "TMDB movie ID"
84 | }
85 | },
86 | "required": ["movie_id"]
87 | }
88 | }
89 | },
90 | {
91 | "type": "function",
92 | "function": {
93 | "name": "get_current_movies",
94 | "handler": "__function__:get_movies",
95 | "description": "Show current movies in theaters",
96 | "parameters": {
97 | "type": "object",
98 | "properties": {}
99 | }
100 | }
101 | },
102 | {
103 | "type": "function",
104 | "function": {
105 | "name": "get_upcoming_movies",
106 | "handler": "__function__:get_upcoming_movies",
107 | "description": "Show movies coming soon",
108 | "parameters": {
109 | "type": "object",
110 | "properties": {}
111 | }
112 | }
113 | },
114 | {
115 | "type": "function",
116 | "function": {
117 | "name": "end_conversation",
118 | "description": "End the conversation",
119 | "parameters": {
120 | "type": "object",
121 | "properties": {}
122 | },
123 | "transition_to": "end"
124 | }
125 | }
126 | ]
127 | },
128 | "end": {
129 | "task_messages": [
130 | {
131 | "role": "system",
132 | "content": "Thank the user warmly and mention they can return anytime to discover more movies."
133 | }
134 | ],
135 | "functions": [],
136 | "post_actions": [
137 | {
138 | "type": "end_conversation"
139 | }
140 | ]
141 | }
142 | }
143 | }
144 |
--------------------------------------------------------------------------------
/editor/examples/patient_intake.json:
--------------------------------------------------------------------------------
1 | {
2 | "initial_node": "start",
3 | "nodes": {
4 | "start": {
5 | "role_messages": [
6 | {
7 | "role": "system",
8 | "content": "You are Jessica, an agent for Tri-County Health Services. You must ALWAYS use one of the available functions to progress the conversation. Be professional but friendly."
9 | }
10 | ],
11 | "task_messages": [
12 | {
13 | "role": "system",
14 | "content": "Start by introducing yourself to Chad Bailey, then ask for their date of birth, including the year. Once they provide their birthday, use verify_birthday to check it. If verified (1983-01-01), proceed to prescriptions."
15 | }
16 | ],
17 | "functions": [
18 | {
19 | "type": "function",
20 | "function": {
21 | "name": "verify_birthday",
22 | "handler": "__function__:verify_birthday",
23 | "description": "Verify the user has provided their correct birthday. Once confirmed, the next step is to recording the user's prescriptions.",
24 | "parameters": {
25 | "type": "object",
26 | "properties": {
27 | "birthday": {
28 | "type": "string",
29 | "description": "The user's birthdate (convert to YYYY-MM-DD format)"
30 | }
31 | },
32 | "required": ["birthday"]
33 | },
34 | "transition_to": "get_prescriptions"
35 | }
36 | }
37 | ]
38 | },
39 | "get_prescriptions": {
40 | "task_messages": [
41 | {
42 | "role": "system",
43 | "content": "This step is for collecting prescriptions. Ask them what prescriptions they're taking, including the dosage. After recording prescriptions (or confirming none), proceed to allergies."
44 | }
45 | ],
46 | "functions": [
47 | {
48 | "type": "function",
49 | "function": {
50 | "name": "record_prescriptions",
51 | "handler": "__function__:record_prescriptions",
52 | "description": "Record the user's prescriptions. Once confirmed, the next step is to collect allergy information.",
53 | "parameters": {
54 | "type": "object",
55 | "properties": {
56 | "prescriptions": {
57 | "type": "array",
58 | "items": {
59 | "type": "object",
60 | "properties": {
61 | "medication": {
62 | "type": "string",
63 | "description": "The medication's name"
64 | },
65 | "dosage": {
66 | "type": "string",
67 | "description": "The prescription's dosage"
68 | }
69 | },
70 | "required": ["medication", "dosage"]
71 | }
72 | }
73 | },
74 | "required": ["prescriptions"]
75 | },
76 | "transition_to": "get_allergies"
77 | }
78 | }
79 | ]
80 | },
81 | "get_allergies": {
82 | "task_messages": [
83 | {
84 | "role": "system",
85 | "content": "Collect allergy information. Ask about any allergies they have. After recording allergies (or confirming none), proceed to medical conditions."
86 | }
87 | ],
88 | "functions": [
89 | {
90 | "type": "function",
91 | "function": {
92 | "name": "record_allergies",
93 | "handler": "__function__:record_allergies",
94 | "description": "Record the user's allergies. Once confirmed, then next step is to collect medical conditions.",
95 | "parameters": {
96 | "type": "object",
97 | "properties": {
98 | "allergies": {
99 | "type": "array",
100 | "items": {
101 | "type": "object",
102 | "properties": {
103 | "name": {
104 | "type": "string",
105 | "description": "What the user is allergic to"
106 | }
107 | },
108 | "required": ["name"]
109 | }
110 | }
111 | },
112 | "required": ["allergies"]
113 | },
114 | "transition_to": "get_conditions"
115 | }
116 | }
117 | ]
118 | },
119 | "get_conditions": {
120 | "task_messages": [
121 | {
122 | "role": "system",
123 | "content": "Collect medical condition information. Ask about any medical conditions they have. After recording conditions (or confirming none), proceed to visit reasons."
124 | }
125 | ],
126 | "functions": [
127 | {
128 | "type": "function",
129 | "function": {
130 | "name": "record_conditions",
131 | "handler": "__function__:record_conditions",
132 | "description": "Record the user's medical conditions. Once confirmed, the next step is to collect visit reasons.",
133 | "parameters": {
134 | "type": "object",
135 | "properties": {
136 | "conditions": {
137 | "type": "array",
138 | "items": {
139 | "type": "object",
140 | "properties": {
141 | "name": {
142 | "type": "string",
143 | "description": "The user's medical condition"
144 | }
145 | },
146 | "required": ["name"]
147 | }
148 | }
149 | },
150 | "required": ["conditions"]
151 | },
152 | "transition_to": "get_visit_reasons"
153 | }
154 | }
155 | ]
156 | },
157 | "get_visit_reasons": {
158 | "task_messages": [
159 | {
160 | "role": "system",
161 | "content": "Collect information about the reason for their visit. Ask what brings them to the doctor today. After recording their reasons, proceed to verification."
162 | }
163 | ],
164 | "functions": [
165 | {
166 | "type": "function",
167 | "function": {
168 | "name": "record_visit_reasons",
169 | "handler": "__function__:record_visit_reasons",
170 | "description": "Record the reasons for their visit. Once confirmed, the next step is to verify all information.",
171 | "parameters": {
172 | "type": "object",
173 | "properties": {
174 | "visit_reasons": {
175 | "type": "array",
176 | "items": {
177 | "type": "object",
178 | "properties": {
179 | "name": {
180 | "type": "string",
181 | "description": "The user's reason for visiting"
182 | }
183 | },
184 | "required": ["name"]
185 | }
186 | }
187 | },
188 | "required": ["visit_reasons"]
189 | },
190 | "transition_to": "verify"
191 | }
192 | }
193 | ]
194 | },
195 | "verify": {
196 | "task_messages": [
197 | {
198 | "role": "system",
199 | "content": "Review all collected information with the patient. Follow these steps:\n\n1. Summarize their prescriptions, allergies, conditions, and visit reasons\n\n2. Ask if everything is correct\n\n3. Use the appropriate function based on their response\n\nBe thorough in reviewing all details and wait for explicit confirmation."
200 | }
201 | ],
202 | "functions": [
203 | {
204 | "type": "function",
205 | "function": {
206 | "name": "revise_information",
207 | "description": "Return to prescriptions to revise information",
208 | "parameters": {
209 | "type": "object",
210 | "properties": {}
211 | },
212 | "transition_to": "get_prescriptions"
213 | }
214 | },
215 | {
216 | "type": "function",
217 | "function": {
218 | "name": "confirm_information",
219 | "description": "Proceed with confirmed information",
220 | "parameters": {
221 | "type": "object",
222 | "properties": {}
223 | },
224 | "transition_to": "confirm"
225 | }
226 | }
227 | ]
228 | },
229 | "confirm": {
230 | "task_messages": [
231 | {
232 | "role": "system",
233 | "content": "Once confirmed, thank them, then use the complete_intake function to end the conversation."
234 | }
235 | ],
236 | "functions": [
237 | {
238 | "type": "function",
239 | "function": {
240 | "name": "complete_intake",
241 | "description": "Complete the intake process",
242 | "parameters": {
243 | "type": "object",
244 | "properties": {}
245 | },
246 | "transition_to": "end"
247 | }
248 | }
249 | ]
250 | },
251 | "end": {
252 | "task_messages": [
253 | {
254 | "role": "system",
255 | "content": "Thank them for their time and end the conversation."
256 | }
257 | ],
258 | "functions": [],
259 | "post_actions": [
260 | {
261 | "type": "end_conversation"
262 | }
263 | ]
264 | }
265 | }
266 | }
267 |
--------------------------------------------------------------------------------
/editor/examples/restaurant_reservation.json:
--------------------------------------------------------------------------------
1 | {
2 | "initial_node": "start",
3 | "nodes": {
4 | "start": {
5 | "role_messages": [
6 | {
7 | "role": "system",
8 | "content": "You are a restaurant reservation assistant for La Maison, an upscale French restaurant. You must ALWAYS use one of the available functions to progress the conversation. This is a phone conversations and your responses will be converted to audio. Avoid outputting special characters and emojis. Be causal and friendly."
9 | }
10 | ],
11 | "task_messages": [
12 | {
13 | "role": "system",
14 | "content": "Warmly greet the customer and ask how many people are in their party."
15 | }
16 | ],
17 | "functions": [
18 | {
19 | "type": "function",
20 | "function": {
21 | "name": "record_party_size",
22 | "handler": "__function__:record_party_size",
23 | "description": "Record the number of people in the party",
24 | "parameters": {
25 | "type": "object",
26 | "properties": {
27 | "size": {
28 | "type": "integer",
29 | "minimum": 1,
30 | "maximum": 12
31 | }
32 | },
33 | "required": ["size"]
34 | },
35 | "transition_to": "get_time"
36 | }
37 | }
38 | ]
39 | },
40 | "get_time": {
41 | "task_messages": [
42 | {
43 | "role": "system",
44 | "content": "Ask what time they'd like to dine. Restaurant is open 5 PM to 10 PM. After they provide a time, confirm it's within operating hours before recording. Use 24-hour format for internal recording (e.g., 17:00 for 5 PM)."
45 | }
46 | ],
47 | "functions": [
48 | {
49 | "type": "function",
50 | "function": {
51 | "name": "record_time",
52 | "handler": "__function__:record_time",
53 | "description": "Record the requested time",
54 | "parameters": {
55 | "type": "object",
56 | "properties": {
57 | "time": {
58 | "type": "string",
59 | "pattern": "^(17|18|19|20|21|22):([0-5][0-9])$",
60 | "description": "Reservation time in 24-hour format (17:00-22:00)"
61 | }
62 | },
63 | "required": ["time"]
64 | },
65 | "transition_to": "confirm"
66 | }
67 | }
68 | ]
69 | },
70 | "confirm": {
71 | "task_messages": [
72 | {
73 | "role": "system",
74 | "content": "Confirm the reservation details and end the conversation."
75 | }
76 | ],
77 | "functions": [
78 | {
79 | "type": "function",
80 | "function": {
81 | "name": "end",
82 | "description": "End the conversation",
83 | "parameters": {
84 | "type": "object",
85 | "properties": {}
86 | },
87 | "transition_to": "end"
88 | }
89 | }
90 | ]
91 | },
92 | "end": {
93 | "task_messages": [
94 | {
95 | "role": "system",
96 | "content": "Thank them and end the conversation."
97 | }
98 | ],
99 | "functions": [],
100 | "post_actions": [
101 | {
102 | "type": "end_conversation"
103 | }
104 | ]
105 | }
106 | }
107 | }
108 |
--------------------------------------------------------------------------------
/editor/examples/travel_planner.json:
--------------------------------------------------------------------------------
1 | {
2 | "initial_node": "start",
3 | "nodes": {
4 | "start": {
5 | "role_messages": [
6 | {
7 | "role": "system",
8 | "content": "You are a travel planning assistant with Summit & Sand Getaways. You must ALWAYS use one of the available functions to progress the conversation. This is a phone conversation and your responses will be converted to audio. Avoid outputting special characters and emojis."
9 | }
10 | ],
11 | "task_messages": [
12 | {
13 | "role": "system",
14 | "content": "For this step, ask if they're interested in planning a beach vacation or a mountain retreat, and wait for them to choose. Start with an enthusiastic greeting and be conversational; you're helping them plan their dream vacation."
15 | }
16 | ],
17 | "functions": [
18 | {
19 | "type": "function",
20 | "function": {
21 | "name": "choose_beach",
22 | "description": "User wants to plan a beach vacation",
23 | "parameters": {
24 | "type": "object",
25 | "properties": {}
26 | },
27 | "transition_to": "choose_beach"
28 | }
29 | },
30 | {
31 | "type": "function",
32 | "function": {
33 | "name": "choose_mountain",
34 | "description": "User wants to plan a mountain retreat",
35 | "parameters": {
36 | "type": "object",
37 | "properties": {}
38 | },
39 | "transition_to": "choose_mountain"
40 | }
41 | }
42 | ]
43 | },
44 | "choose_beach": {
45 | "task_messages": [
46 | {
47 | "role": "system",
48 | "content": "You are handling beach vacation planning. Use the available functions:\n - Use select_destination when the user chooses their preferred beach location\n - After destination is selected, dates will be collected automatically\n\nAvailable beach destinations are: 'Maui', 'Cancun', or 'Maldives'. After they choose, confirm their selection. Be enthusiastic and paint a picture of each destination."
49 | }
50 | ],
51 | "functions": [
52 | {
53 | "type": "function",
54 | "function": {
55 | "name": "select_destination",
56 | "handler": "__function__:select_destination",
57 | "description": "Record the selected beach destination",
58 | "parameters": {
59 | "type": "object",
60 | "properties": {
61 | "destination": {
62 | "type": "string",
63 | "enum": ["Maui", "Cancun", "Maldives"],
64 | "description": "Selected beach destination"
65 | }
66 | },
67 | "required": ["destination"]
68 | },
69 | "transition_to": "get_dates"
70 | }
71 | }
72 | ]
73 | },
74 | "choose_mountain": {
75 | "task_messages": [
76 | {
77 | "role": "system",
78 | "content": "You are handling mountain retreat planning. Use the available functions:\n - Use select_destination when the user chooses their preferred mountain location\n - After destination is selected, dates will be collected automatically\n\nAvailable mountain destinations are: 'Swiss Alps', 'Rocky Mountains', or 'Himalayas'. After they choose, confirm their selection. Be enthusiastic and paint a picture of each destination."
79 | }
80 | ],
81 | "functions": [
82 | {
83 | "type": "function",
84 | "function": {
85 | "name": "select_destination",
86 | "handler": "__function__:select_destination",
87 | "description": "Record the selected mountain destination",
88 | "parameters": {
89 | "type": "object",
90 | "properties": {
91 | "destination": {
92 | "type": "string",
93 | "enum": ["Swiss Alps", "Rocky Mountains", "Himalayas"],
94 | "description": "Selected mountain destination"
95 | }
96 | },
97 | "required": ["destination"]
98 | },
99 | "transition_to": "get_dates"
100 | }
101 | }
102 | ]
103 | },
104 | "get_dates": {
105 | "task_messages": [
106 | {
107 | "role": "system",
108 | "content": "Handle travel date selection. Use the available functions:\n - Use record_dates when the user specifies their travel dates (can be used multiple times if they change their mind)\n - After dates are recorded, activities will be collected automatically\n\nAsk for their preferred travel dates within the next 6 months. After recording dates, confirm the selection."
109 | }
110 | ],
111 | "functions": [
112 | {
113 | "type": "function",
114 | "function": {
115 | "name": "record_dates",
116 | "handler": "__function__:record_dates",
117 | "description": "Record the selected travel dates",
118 | "parameters": {
119 | "type": "object",
120 | "properties": {
121 | "check_in": {
122 | "type": "string",
123 | "format": "date",
124 | "description": "Check-in date (YYYY-MM-DD)"
125 | },
126 | "check_out": {
127 | "type": "string",
128 | "format": "date",
129 | "description": "Check-out date (YYYY-MM-DD)"
130 | }
131 | },
132 | "required": ["check_in", "check_out"]
133 | },
134 | "transition_to": "get_activities"
135 | }
136 | }
137 | ]
138 | },
139 | "get_activities": {
140 | "task_messages": [
141 | {
142 | "role": "system",
143 | "content": "Handle activity preferences. Use the available functions:\n - Use record_activities to save their activity preferences\n - After activities are recorded, verification will happen automatically\n\nFor beach destinations, suggest: snorkeling, surfing, sunset cruise\nFor mountain destinations, suggest: hiking, skiing, mountain biking\n\nAfter they choose, confirm their selections."
144 | }
145 | ],
146 | "functions": [
147 | {
148 | "type": "function",
149 | "function": {
150 | "name": "record_activities",
151 | "handler": "__function__:record_activities",
152 | "description": "Record selected activities",
153 | "parameters": {
154 | "type": "object",
155 | "properties": {
156 | "activities": {
157 | "type": "array",
158 | "items": {
159 | "type": "string"
160 | },
161 | "minItems": 1,
162 | "maxItems": 3,
163 | "description": "Selected activities"
164 | }
165 | },
166 | "required": ["activities"]
167 | },
168 | "transition_to": "verify_itinerary"
169 | }
170 | }
171 | ]
172 | },
173 | "verify_itinerary": {
174 | "task_messages": [
175 | {
176 | "role": "system",
177 | "content": "Review the complete itinerary with the user. Summarize their destination, dates, and chosen activities. Use revise_plan to make changes or confirm_booking if they're happy. Be thorough in reviewing all details and ask for their confirmation."
178 | }
179 | ],
180 | "functions": [
181 | {
182 | "type": "function",
183 | "function": {
184 | "name": "revise_plan",
185 | "description": "Return to date selection to revise the plan",
186 | "parameters": {
187 | "type": "object",
188 | "properties": {}
189 | },
190 | "transition_to": "get_dates"
191 | }
192 | },
193 | {
194 | "type": "function",
195 | "function": {
196 | "name": "confirm_booking",
197 | "description": "Confirm the booking and proceed to end",
198 | "parameters": {
199 | "type": "object",
200 | "properties": {}
201 | },
202 | "transition_to": "confirm_booking"
203 | }
204 | }
205 | ]
206 | },
207 | "confirm_booking": {
208 | "task_messages": [
209 | {
210 | "role": "system",
211 | "content": "The booking is confirmed. Share some relevant tips about their chosen destination, thank them warmly, and use end to complete the conversation."
212 | }
213 | ],
214 | "functions": [
215 | {
216 | "type": "function",
217 | "function": {
218 | "name": "end",
219 | "description": "End the conversation",
220 | "parameters": {
221 | "type": "object",
222 | "properties": {}
223 | },
224 | "transition_to": "end"
225 | }
226 | }
227 | ],
228 | "pre_actions": [
229 | {
230 | "type": "tts_say",
231 | "text": "Fantastic! Your dream vacation is confirmed!"
232 | }
233 | ]
234 | },
235 | "end": {
236 | "task_messages": [
237 | {
238 | "role": "system",
239 | "content": "Wish them a wonderful trip and end the conversation."
240 | }
241 | ],
242 | "functions": [],
243 | "post_actions": [
244 | {
245 | "type": "end_conversation"
246 | }
247 | ]
248 | }
249 | }
250 | }
251 |
--------------------------------------------------------------------------------
/editor/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pipecat-ai/pipecat-flows/12bb9f5b356e2905ae4bdaf5b26cd2505cd3e2ff/editor/favicon.png
--------------------------------------------------------------------------------
/editor/favicon.svg:
--------------------------------------------------------------------------------
1 |
8 |
--------------------------------------------------------------------------------
/editor/index.html:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
9 |
10 |
11 | Pipecat Flow Editor
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 | Node Editor
41 |
42 |
43 |
44 |
45 | Select a node to edit its contents
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
58 |
60 |
61 |
62 |
63 |
64 |
68 |
70 |
71 |
72 |
73 |
74 |
77 |
79 |
80 |
81 |
82 |
83 |
86 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
97 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
120 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
150 |
151 |
152 |
--------------------------------------------------------------------------------
/editor/js/editor/canvas.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2024, Daily
3 | *
4 | * SPDX-License-Identifier: BSD 2-Clause License
5 | */
6 |
7 | import { LGraphCanvas } from "litegraph.js";
8 |
9 | /**
10 | * Sets up the canvas and its event handlers
11 | * @param {LGraph} graph - The LiteGraph instance
12 | * @returns {LGraphCanvas} The configured canvas instance
13 | */
14 | export function setupCanvas(graph) {
15 | const canvas = new LGraphCanvas("#graph-canvas", graph);
16 |
17 | document.getElementById("zoom-in").onclick = () => {
18 | if (canvas.ds.scale < 2) {
19 | // Limit max zoom
20 | canvas.ds.scale *= 1.2;
21 | canvas.setDirty(true, true);
22 | }
23 | };
24 |
25 | document.getElementById("zoom-out").onclick = () => {
26 | if (canvas.ds.scale > 0.2) {
27 | // Limit min zoom
28 | canvas.ds.scale *= 0.8;
29 | canvas.setDirty(true, true);
30 | }
31 | };
32 |
33 | /**
34 | * Resizes the canvas to fit its container
35 | */
36 | function resizeCanvas() {
37 | const canvasElement = document.getElementById("graph-canvas");
38 | const container = document.getElementById("graph-container");
39 | canvasElement.width = container.offsetWidth;
40 | canvasElement.height = container.offsetHeight;
41 | }
42 |
43 | window.addEventListener("resize", resizeCanvas);
44 | resizeCanvas();
45 |
46 | return canvas;
47 | }
48 |
--------------------------------------------------------------------------------
/editor/js/editor/editorState.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2024, Daily
3 | *
4 | * SPDX-License-Identifier: BSD 2-Clause License
5 | */
6 |
7 | /**
8 | * Singleton class to manage global editor state
9 | */
10 | class EditorState {
11 | /** @type {EditorState|null} */
12 | static instance = null;
13 |
14 | /**
15 | * Creates or returns the EditorState singleton
16 | */
17 | constructor() {
18 | if (EditorState.instance) {
19 | return EditorState.instance;
20 | }
21 | /** @type {SidePanel|null} */
22 | this.sidePanel = null;
23 | EditorState.instance = this;
24 | }
25 |
26 | /**
27 | * Sets the side panel instance
28 | * @param {SidePanel} sidePanel
29 | */
30 | setSidePanel(sidePanel) {
31 | this.sidePanel = sidePanel;
32 | }
33 |
34 | /**
35 | * Updates the side panel with node data
36 | * @param {PipecatBaseNode|null} node
37 | */
38 | updateSidePanel(node) {
39 | if (this.sidePanel) {
40 | this.sidePanel.updatePanel(node);
41 | }
42 | }
43 | }
44 |
45 | export const editorState = new EditorState();
46 |
--------------------------------------------------------------------------------
/editor/js/editor/toolbar.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2024, Daily
3 | *
4 | * SPDX-License-Identifier: BSD 2-Clause License
5 | */
6 |
7 | import { generateFlowConfig } from "../utils/export.js";
8 | import { createFlowFromConfig } from "../utils/import.js";
9 | import { validateFlow } from "../utils/validation.js";
10 | import { editorState } from "../editor/editorState.js";
11 |
12 | /**
13 | * Manages the toolbar UI and actions
14 | */
15 | export class Toolbar {
16 | /**
17 | * Creates a new Toolbar instance
18 | * @param {LGraph} graph - The LiteGraph instance
19 | */
20 | constructor(graph) {
21 | this.graph = graph;
22 | this.setupButtons();
23 | }
24 |
25 | /**
26 | * Sets up toolbar button event listeners
27 | */
28 | setupButtons() {
29 | // Get modal elements
30 | const newFlowModal = document.getElementById("new-flow-modal");
31 | const cancelNewFlow = document.getElementById("cancel-new-flow");
32 | const confirmNewFlow = document.getElementById("confirm-new-flow");
33 |
34 | // New Flow button now opens the modal
35 | document.getElementById("new-flow").onclick = () => {
36 | newFlowModal.showModal();
37 | };
38 |
39 | // Cancel button closes the modal
40 | cancelNewFlow.onclick = () => {
41 | newFlowModal.close();
42 | };
43 |
44 | // Confirm button clears the graph and closes the modal
45 | confirmNewFlow.onclick = () => {
46 | this.handleNew();
47 | newFlowModal.close();
48 | };
49 |
50 | document.getElementById("import-flow").onclick = () => this.handleImport();
51 | document.getElementById("export-flow").onclick = () => this.handleExport();
52 | }
53 |
54 | /**
55 | * Handles creating a new flow
56 | */
57 | handleNew() {
58 | // Clear the graph
59 | this.graph.clear();
60 |
61 | // Reset sidebar state
62 | editorState.updateSidePanel(null);
63 | }
64 |
65 | /**
66 | * Handles importing a flow configuration
67 | */
68 | handleImport() {
69 | const input = document.createElement("input");
70 | input.type = "file";
71 | input.accept = ".json";
72 | input.onchange = (e) => {
73 | const file = e.target.files[0];
74 | const reader = new FileReader();
75 | reader.onload = (event) => {
76 | try {
77 | // Clean the input string
78 | const cleanInput = event.target.result
79 | .replace(/[\u0000-\u001F\u007F-\u009F]/g, "")
80 | .replace(/\r\n/g, "\n")
81 | .replace(/\r/g, "\n");
82 |
83 | console.debug("Cleaned input:", cleanInput);
84 |
85 | const flowConfig = JSON.parse(cleanInput);
86 | console.debug("Parsed config:", flowConfig);
87 |
88 | // Validate imported flow
89 | const validation = validateFlow(flowConfig);
90 | if (!validation.valid) {
91 | console.error("Flow validation errors:", validation.errors);
92 | if (
93 | !confirm("Imported flow has validation errors. Import anyway?")
94 | ) {
95 | return;
96 | }
97 | }
98 |
99 | createFlowFromConfig(this.graph, flowConfig);
100 | console.log("Successfully imported flow configuration");
101 | } catch (error) {
102 | console.error("Error importing flow:", error);
103 | console.error("Error details:", {
104 | message: error.message,
105 | position: error.position,
106 | stack: error.stack,
107 | });
108 | alert("Error importing flow: " + error.message);
109 | }
110 | };
111 | reader.readAsText(file);
112 | };
113 | input.click();
114 | }
115 |
116 | /**
117 | * Handles exporting the current flow
118 | */
119 | handleExport() {
120 | try {
121 | const flowConfig = generateFlowConfig(this.graph);
122 |
123 | // Validate before export
124 | const validation = validateFlow(flowConfig);
125 | if (!validation.valid) {
126 | console.error("Flow validation errors:", validation.errors);
127 | if (!confirm("Flow has validation errors. Export anyway?")) {
128 | return;
129 | }
130 | }
131 |
132 | console.log("Generated Flow Configuration:");
133 | console.log(JSON.stringify(flowConfig, null, 2));
134 |
135 | // Generate timestamp
136 | const timestamp = new Date()
137 | .toISOString()
138 | .replace(/[:.]/g, "-")
139 | .replace("T", "_")
140 | .slice(0, -5);
141 |
142 | // Create a clean JSON string
143 | const jsonString = JSON.stringify(flowConfig, null, 2);
144 |
145 | const blob = new Blob([jsonString], { type: "application/json" });
146 | const url = URL.createObjectURL(blob);
147 | const a = document.createElement("a");
148 | a.href = url;
149 | a.download = `flow_config_${timestamp}.json`;
150 | document.body.appendChild(a);
151 | a.click();
152 | document.body.removeChild(a);
153 | URL.revokeObjectURL(url);
154 | } catch (error) {
155 | console.error("Error generating flow configuration:", error);
156 | alert("Error generating flow configuration: " + error.message);
157 | }
158 | }
159 | }
160 |
--------------------------------------------------------------------------------
/editor/js/main.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2024, Daily
3 | *
4 | * SPDX-License-Identifier: BSD 2-Clause License
5 | */
6 |
7 | import { LGraph, LiteGraph } from "litegraph.js";
8 | import 'litegraph.js/css/litegraph.css';
9 |
10 | import { registerNodes } from "./nodes/index.js";
11 | import { SidePanel } from "./editor/sidePanel.js";
12 | import { Toolbar } from "./editor/toolbar.js";
13 | import { setupCanvas } from "./editor/canvas.js";
14 | import { editorState } from "./editor/editorState.js";
15 |
16 | // Clear all default node types
17 | LiteGraph.clearRegisteredTypes();
18 |
19 | /**
20 | * Initializes the flow editor
21 | */
22 | document.addEventListener("DOMContentLoaded", function () {
23 | // Initialize graph
24 | const graph = new LGraph();
25 |
26 | // Register node types
27 | registerNodes();
28 |
29 | // Setup UI components
30 | const canvas = setupCanvas(graph);
31 | const sidePanel = new SidePanel(graph);
32 | const toolbar = new Toolbar(graph);
33 |
34 | // Register side panel with editor state
35 | editorState.setSidePanel(sidePanel);
36 |
37 | // Add graph change listener
38 | graph.onAfterChange = () => {
39 | graph._nodes.forEach((node) => {
40 | if (node.onAfterGraphChange) {
41 | node.onAfterGraphChange();
42 | }
43 | });
44 | };
45 |
46 | // Handle node selection
47 | graph.onNodeSelected = (node) => {
48 | editorState.updateSidePanel(node);
49 | };
50 |
51 | graph.onNodeDeselected = () => {
52 | editorState.updateSidePanel(null);
53 | };
54 |
55 | // Start the graph
56 | graph.start();
57 | });
58 |
--------------------------------------------------------------------------------
/editor/js/nodes/baseNode.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2024, Daily
3 | *
4 | * SPDX-License-Identifier: BSD 2-Clause License
5 | */
6 |
7 | import { LGraphNode } from "litegraph.js";
8 |
9 | import { editorState } from "../editor/editorState.js";
10 | import { formatActions } from "../utils/helpers.js";
11 |
12 | /**
13 | * @typedef {Object} Message
14 | * @property {string} role - Message role (e.g., 'system', 'user', 'assistant')
15 | * @property {string} content - Message content
16 | */
17 |
18 | /**
19 | * @typedef {Object} Action
20 | * @property {string} type - Action type (e.g., 'tts_say', 'end_conversation')
21 | * @property {string} [text] - Text content for text-based actions
22 | */
23 |
24 | /**
25 | * @typedef {Object} NodeProperties
26 | * @property {Array} [role_messages] - Messages defining bot's role/personality
27 | * @property {Array} [task_messages] - Messages defining the node's task
28 | * @property {Array} [pre_actions] - Actions to execute before node processing
29 | * @property {Array} [post_actions] - Actions to execute after node processing
30 | */
31 |
32 | /**
33 | * Base class for all Pipecat nodes
34 | * @extends LGraphNode
35 | */
36 | export class PipecatBaseNode extends LGraphNode {
37 | /**
38 | * Creates a new PipecatBaseNode
39 | * @param {string} title - The display title of the node
40 | * @param {string} color - The color of the node
41 | * @param {string} [defaultContent='Enter message...'] - Default message content
42 | */
43 | constructor(title, color, defaultContent) {
44 | super();
45 | this.title = title;
46 | this.color = color;
47 | this.size = [400, 200];
48 |
49 | /** @type {NodeProperties} */
50 | this.properties = {
51 | task_messages: [
52 | {
53 | role: "system",
54 | content: defaultContent || "Enter task message...",
55 | },
56 | ],
57 | pre_actions: [],
58 | post_actions: [],
59 | };
60 |
61 | // Force minimum width
62 | this.computeSize = function () {
63 | return [400, this.size[1]];
64 | };
65 | }
66 |
67 | /**
68 | * Forces a minimum width for the node
69 | * @returns {Array} The minimum dimensions [width, height]
70 | */
71 | computeSize() {
72 | return [400, this.size[1]];
73 | }
74 |
75 | /**
76 | * Draws the node's content
77 | * @param {CanvasRenderingContext2D} ctx - The canvas rendering context
78 | */
79 | onDrawForeground(ctx) {
80 | const padding = 15;
81 | const textColor = "#ddd";
82 | const labelColor = "#aaa";
83 | let currentY = 40;
84 |
85 | /**
86 | * Draws wrapped text with a label
87 | * @param {string} text - The text to draw
88 | * @param {number} startY - Starting Y position
89 | * @param {string} label - Label for the text section
90 | * @returns {number} The Y position after drawing
91 | */
92 | const drawWrappedText = (text, startY, label) => {
93 | ctx.fillStyle = labelColor;
94 | ctx.font = "12px Arial";
95 | ctx.fillText(label, padding, startY + 5);
96 |
97 | ctx.fillStyle = textColor;
98 | ctx.font = "12px monospace";
99 |
100 | const words = text.split(" ");
101 | let line = "";
102 | let y = startY + 25;
103 | const maxWidth = this.size[0] - padding * 3;
104 |
105 | words.forEach((word) => {
106 | const testLine = line + word + " ";
107 | const metrics = ctx.measureText(testLine);
108 | if (metrics.width > maxWidth) {
109 | ctx.fillText(line, padding * 1.5, y);
110 | line = word + " ";
111 | y += 20;
112 | } else {
113 | line = testLine;
114 | }
115 | });
116 | ctx.fillText(line, padding * 1.5, y);
117 |
118 | return y + 25;
119 | };
120 |
121 | // Draw role messages if present
122 | if (this.properties.role_messages?.length > 0) {
123 | this.properties.role_messages.forEach((message) => {
124 | currentY = drawWrappedText(message.content, currentY, "Role Message");
125 | currentY += 10;
126 | });
127 | }
128 |
129 | // Draw task messages
130 | if (this.properties.task_messages) {
131 | this.properties.task_messages.forEach((message) => {
132 | currentY = drawWrappedText(message.content, currentY, "Task Message");
133 | currentY += 10;
134 | });
135 | }
136 |
137 | // Draw pre-actions
138 | if (this.properties.pre_actions?.length > 0) {
139 | currentY = drawWrappedText(
140 | formatActions(this.properties.pre_actions),
141 | currentY + 15,
142 | "Pre-actions",
143 | );
144 | }
145 |
146 | // Draw post-actions
147 | if (this.properties.post_actions?.length > 0) {
148 | currentY = drawWrappedText(
149 | formatActions(this.properties.post_actions),
150 | currentY + 15,
151 | "Post-actions",
152 | );
153 | }
154 |
155 | const desiredHeight = currentY + padding * 2;
156 | if (Math.abs(this.size[1] - desiredHeight) > 10) {
157 | this.size[1] = desiredHeight;
158 | this.setDirtyCanvas(true, true);
159 | }
160 | }
161 |
162 | /**
163 | * Handles node selection
164 | * Updates the side panel with this node's properties
165 | */
166 | onSelected() {
167 | editorState.updateSidePanel(this);
168 | }
169 |
170 | /**
171 | * Serializes the node for saving
172 | * @returns {Object} Serialized node data
173 | */
174 | serialize() {
175 | const data = super.serialize();
176 | data.properties = {
177 | role_messages: this.properties.role_messages,
178 | task_messages: this.properties.task_messages,
179 | pre_actions: this.properties.pre_actions,
180 | post_actions: this.properties.post_actions,
181 | };
182 | return data;
183 | }
184 |
185 | /**
186 | * Deserializes saved node data
187 | * @param {Object} data - The saved node data
188 | */
189 | configure(data) {
190 | super.configure(data);
191 | if (data.properties) {
192 | this.properties = {
193 | role_messages: data.properties.role_messages || [],
194 | task_messages: data.properties.task_messages || [
195 | { role: "system", content: "Enter task message..." },
196 | ],
197 | pre_actions: data.properties.pre_actions || [],
198 | post_actions: data.properties.post_actions || [],
199 | };
200 | }
201 | }
202 | }
203 |
--------------------------------------------------------------------------------
/editor/js/nodes/endNode.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2024, Daily
3 | *
4 | * SPDX-License-Identifier: BSD 2-Clause License
5 | */
6 |
7 | import { PipecatBaseNode } from "./baseNode.js";
8 | import { LiteGraph } from "litegraph.js";
9 |
10 | /**
11 | * Represents the end node of a flow
12 | * @extends PipecatBaseNode
13 | */
14 | export class PipecatEndNode extends PipecatBaseNode {
15 | /**
16 | * Creates a new end node
17 | */
18 | constructor() {
19 | super("End Node", "#e74c3c");
20 | this.addInput("In", "flow", {
21 | multipleConnections: true,
22 | linkType: LiteGraph.MULTIPLE_LINK,
23 | });
24 |
25 | // Initialize with only task messages for the end node
26 | this.properties = {
27 | task_messages: [
28 | {
29 | role: "system",
30 | content: "Enter final task message...",
31 | },
32 | ],
33 | pre_actions: [],
34 | post_actions: [],
35 | };
36 | }
37 |
38 | /**
39 | * Handles input connection
40 | * @param {number} targetSlot - Input slot index
41 | * @param {string} type - Type of connection
42 | * @param {Object} output - Output connection information
43 | * @param {LGraphNode} input_node - Node being connected
44 | * @param {number} input_slot - Slot being connected to
45 | * @returns {boolean} Whether the connection is allowed
46 | */
47 | onConnectInput(targetSlot, type, output, input_node, input_slot) {
48 | if (this.inputs[0].link == null) {
49 | this.inputs[0].link = [];
50 | }
51 | return true;
52 | }
53 |
54 | /**
55 | * Handles node connection
56 | * @param {number} slot - Input slot index
57 | * @param {LGraphNode} targetNode - Node to connect to
58 | * @param {number} targetSlot - Target node's slot index
59 | * @returns {boolean} Whether the connection was successful
60 | */
61 | connect(slot, targetNode, targetSlot) {
62 | if (this.inputs[slot].link == null) {
63 | this.inputs[slot].link = [];
64 | }
65 | return super.connect(slot, targetNode, targetSlot);
66 | }
67 |
68 | /**
69 | * Handles connection changes
70 | * @param {string} type - Type of connection change
71 | * @param {number} slot - Slot index
72 | * @param {boolean} connected - Whether connection was made or removed
73 | * @param {Object} link_info - Information about the connection
74 | */
75 | onConnectionsChange(type, slot, connected, link_info) {
76 | if (type === LiteGraph.INPUT && this.inputs[slot].link == null) {
77 | this.inputs[slot].link = [];
78 | }
79 | super.onConnectionsChange &&
80 | super.onConnectionsChange(type, slot, connected, link_info);
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/editor/js/nodes/flowNode.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2024, Daily
3 | *
4 | * SPDX-License-Identifier: BSD 2-Clause License
5 | */
6 |
7 | import { PipecatBaseNode } from "./baseNode.js";
8 |
9 | /**
10 | * Represents an intermediate flow node
11 | * @extends PipecatBaseNode
12 | */
13 | export class PipecatFlowNode extends PipecatBaseNode {
14 | /**
15 | * Creates a new flow node
16 | */
17 | constructor() {
18 | super("Flow Node", "#3498db");
19 | this.addInput("In", "flow");
20 | this.addOutput("Out", "flow");
21 |
22 | // Initialize with only task messages since role is inherited
23 | this.properties = {
24 | task_messages: [
25 | {
26 | role: "system",
27 | content: "Enter task message...",
28 | },
29 | ],
30 | pre_actions: [],
31 | post_actions: [],
32 | };
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/editor/js/nodes/functionNode.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2024, Daily
3 | *
4 | * SPDX-License-Identifier: BSD 2-Clause License
5 | */
6 |
7 | import { LGraphNode, LiteGraph } from "litegraph.js";
8 |
9 | import { editorState } from "../editor/editorState.js";
10 |
11 | /**
12 | * @typedef {Object} FunctionParameter
13 | * @property {string} type - Parameter type (e.g., 'string', 'integer')
14 | * @property {string} [description] - Parameter description
15 | * @property {Array} [enum] - Possible values for enum types
16 | * @property {number} [minimum] - Minimum value for numeric types
17 | * @property {number} [maximum] - Maximum value for numeric types
18 | */
19 |
20 | /**
21 | * @typedef {Object} FunctionDefinition
22 | * @property {string} name - Function name
23 | * @property {string} description - Function description
24 | * @property {Object} parameters - Function parameters
25 | * @property {Object.} parameters.properties - Parameter definitions
26 | * @property {Array} [parameters.required] - Required parameter names
27 | */
28 |
29 | /**
30 | * @typedef {Object} FunctionNodeProperties
31 | * @property {string} type - Always 'function'
32 | * @property {FunctionDefinition} function - The function definition
33 | * @property {boolean} isNodeFunction - Whether this is a node function
34 | */
35 |
36 | /**
37 | * Represents a function node in the flow
38 | * @extends LGraphNode
39 | */
40 | export class PipecatFunctionNode extends LGraphNode {
41 | /**
42 | * Creates a new function node
43 | */
44 | constructor() {
45 | super();
46 | this.title = "Function";
47 | this.addInput("From", "flow");
48 | this.addOutput("To", "flow", {
49 | linkType: LiteGraph.MULTIPLE_LINK,
50 | });
51 |
52 | /** @type {FunctionNodeProperties} */
53 | this.properties = {
54 | type: "function",
55 | function: {
56 | name: "function_name",
57 | description: "Function description",
58 | parameters: {
59 | type: "object",
60 | properties: {},
61 | },
62 | },
63 | isNodeFunction: false,
64 | };
65 |
66 | this.color = "#9b59b6";
67 | this.size = [400, 150];
68 | }
69 |
70 | /**
71 | * Forces a minimum width for the node
72 | * @returns {Array} The minimum dimensions [width, height]
73 | */
74 | computeSize() {
75 | return [400, this.size[1]];
76 | }
77 |
78 | /**
79 | * Updates the node status of the function based on its connections
80 | */
81 | updateNodeFunctionStatus() {
82 | const hasOutputConnection =
83 | this.outputs[0].links && this.outputs[0].links.length > 0;
84 | const newStatus = !hasOutputConnection;
85 |
86 | if (this.properties.isNodeFunction !== newStatus) {
87 | this.properties.isNodeFunction = newStatus;
88 | this.setDirtyCanvas(true, true);
89 | }
90 | }
91 |
92 | /**
93 | * Handles node connection
94 | * @param {number} slot - Output slot index
95 | * @param {LGraphNode} targetNode - Node to connect to
96 | * @param {number} targetSlot - Target node's input slot index
97 | * @returns {boolean} Whether the connection was successful
98 | */
99 | connect(slot, targetNode, targetSlot) {
100 | if (slot === 1 && this.outputs[slot].links == null) {
101 | this.outputs[slot].links = [];
102 | }
103 | const result = super.connect(slot, targetNode, targetSlot);
104 | this.updateNodeFunctionStatus();
105 | return result;
106 | }
107 |
108 | /**
109 | * Handles output disconnection
110 | * @param {number} slot - Output slot index
111 | */
112 | disconnectOutput(slot) {
113 | if (this.outputs[slot].links == null) {
114 | this.outputs[slot].links = [];
115 | }
116 | super.disconnectOutput(slot);
117 | this.updateNodeFunctionStatus();
118 | }
119 |
120 | /**
121 | * Handles input disconnection
122 | * @param {number} slot - Input slot index
123 | */
124 | disconnectInput(slot) {
125 | super.disconnectInput(slot);
126 | this.updateNodeFunctionStatus();
127 | }
128 |
129 | /**
130 | * Handles connection changes
131 | * @param {string} type - Type of connection change
132 | * @param {number} slot - Slot index
133 | * @param {boolean} connected - Whether connection was made or removed
134 | * @param {Object} link_info - Information about the connection
135 | */
136 | onConnectionsChange(type, slot, connected, link_info) {
137 | if (type === LiteGraph.OUTPUT && this.outputs[slot].links == null) {
138 | this.outputs[slot].links = [];
139 | }
140 | super.onConnectionsChange &&
141 | super.onConnectionsChange(type, slot, connected, link_info);
142 | this.updateNodeFunctionStatus();
143 | }
144 |
145 | /**
146 | * Draws the node's content
147 | * @param {CanvasRenderingContext2D} ctx - The canvas rendering context
148 | */
149 | onDrawForeground(ctx) {
150 | this.updateNodeFunctionStatus();
151 |
152 | const padding = 15;
153 | const textColor = "#ddd";
154 | const labelColor = "#aaa";
155 |
156 | this.color = this.properties.isNodeFunction ? "#e67e22" : "#9b59b6";
157 |
158 | // Draw node/edge indicator
159 | ctx.fillStyle = labelColor;
160 | ctx.font = "11px Arial";
161 | const typeText = this.properties.isNodeFunction
162 | ? "[Node Function]"
163 | : "[Edge Function]";
164 | ctx.fillText(typeText, padding, 35);
165 |
166 | // Draw function name
167 | ctx.fillStyle = textColor;
168 | ctx.font = "14px monospace";
169 | ctx.fillText(this.properties.function.name, padding, 60);
170 |
171 | // Draw description
172 | ctx.fillStyle = labelColor;
173 | ctx.font = "12px Arial";
174 | const description = this.properties.function.description;
175 |
176 | // Word wrap description
177 | const words = description.split(" ");
178 | let line = "";
179 | let y = 80;
180 | const maxWidth = this.size[0] - padding * 3;
181 |
182 | words.forEach((word) => {
183 | const testLine = line + word + " ";
184 | const metrics = ctx.measureText(testLine);
185 | if (metrics.width > maxWidth) {
186 | ctx.fillText(line, padding, y);
187 | line = word + " ";
188 | y += 20;
189 | } else {
190 | line = testLine;
191 | }
192 | });
193 | ctx.fillText(line, padding, y);
194 |
195 | // Draw parameters indicator
196 | const hasParameters =
197 | Object.keys(this.properties.function.parameters.properties).length > 0;
198 | if (hasParameters) {
199 | ctx.fillStyle = "#666";
200 | ctx.font = "11px Arial";
201 | ctx.fillText("Has Parameters ⚙️", padding, y + 25);
202 | }
203 |
204 | // Optional: Draw transition indicator
205 | if (this.properties.function.transition_to) {
206 | ctx.fillStyle = "#666";
207 | ctx.font = "11px Arial";
208 | ctx.fillText(
209 | `→ ${this.properties.function.transition_to}`,
210 | padding,
211 | y + (hasParameters ? 45 : 25),
212 | );
213 | y += 20; // Increase y for the new line
214 | }
215 |
216 | // Adjust node height
217 | const desiredHeight = y + (hasParameters ? 45 : 25);
218 | if (Math.abs(this.size[1] - desiredHeight) > 10) {
219 | this.size[1] = desiredHeight;
220 | this.setDirtyCanvas(true, true);
221 | }
222 | }
223 |
224 | /**
225 | * Handles node selection
226 | */
227 | onSelected() {
228 | editorState.updateSidePanel(this);
229 | }
230 |
231 | /**
232 | * Handles graph changes
233 | */
234 | onAfterGraphChange() {
235 | this.updateNodeFunctionStatus();
236 | }
237 | }
238 |
--------------------------------------------------------------------------------
/editor/js/nodes/index.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2024, Daily
3 | *
4 | * SPDX-License-Identifier: BSD 2-Clause License
5 | */
6 |
7 | import { LiteGraph } from "litegraph.js";
8 |
9 | import { PipecatStartNode } from "./startNode.js";
10 | import { PipecatFlowNode } from "./flowNode.js";
11 | import { PipecatEndNode } from "./endNode.js";
12 | import { PipecatFunctionNode } from "./functionNode.js";
13 | import { PipecatMergeNode } from "./mergeNode.js";
14 |
15 | /**
16 | * Registers all node types with LiteGraph
17 | */
18 | export function registerNodes() {
19 | LiteGraph.registerNodeType("nodes/Start", PipecatStartNode);
20 | LiteGraph.registerNodeType("nodes/Flow", PipecatFlowNode);
21 | LiteGraph.registerNodeType("nodes/End", PipecatEndNode);
22 | LiteGraph.registerNodeType("nodes/Function", PipecatFunctionNode);
23 | LiteGraph.registerNodeType("flow/Merge", PipecatMergeNode);
24 | }
25 |
26 | export {
27 | PipecatStartNode,
28 | PipecatFlowNode,
29 | PipecatEndNode,
30 | PipecatFunctionNode,
31 | PipecatMergeNode,
32 | };
33 |
--------------------------------------------------------------------------------
/editor/js/nodes/mergeNode.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2024, Daily
3 | *
4 | * SPDX-License-Identifier: BSD 2-Clause License
5 | */
6 |
7 | import { LGraphNode } from "litegraph.js";
8 |
9 | /**
10 | * @typedef {Object} MergeNodeSize
11 | * @property {number} width - Node width
12 | * @property {number} height - Node height
13 | */
14 |
15 | /**
16 | * Represents a merge node that combines multiple inputs into one output
17 | * @extends LGraphNode
18 | */
19 | export class PipecatMergeNode extends LGraphNode {
20 | /**
21 | * Creates a new merge node
22 | */
23 | constructor() {
24 | super();
25 | this.title = "Merge";
26 |
27 | // Start with two input slots
28 | this.addInput("In 1", "flow");
29 | this.addInput("In 2", "flow");
30 |
31 | this.addOutput("Out", "flow");
32 | this.color = "#95a5a6";
33 | /** @type {MergeNodeSize} */
34 | this.size = [140, 60];
35 |
36 | // Add buttons for managing inputs
37 | this.addWidget("button", "+ Add input", null, () => {
38 | this.addInput(`In ${this.inputs.length + 1}`, "flow");
39 | this.size[1] += 20; // Increase height to accommodate new input
40 | });
41 |
42 | this.addWidget("button", "- Remove input", null, () => {
43 | if (this.inputs.length > 2) {
44 | // Maintain minimum of 2 inputs
45 | // Disconnect any existing connection to the last input
46 | if (this.inputs[this.inputs.length - 1].link != null) {
47 | this.disconnectInput(this.inputs.length - 1);
48 | }
49 | // Remove the last input
50 | this.removeInput(this.inputs.length - 1);
51 | this.size[1] -= 20; // Decrease height
52 | }
53 | });
54 | }
55 |
56 | /**
57 | * Draws the node's content
58 | * @param {CanvasRenderingContext2D} ctx - The canvas rendering context
59 | */
60 | onDrawForeground(ctx) {
61 | const activeConnections = this.inputs.filter(
62 | (input) => input.link != null,
63 | ).length;
64 | if (activeConnections > 0) {
65 | ctx.fillStyle = "#ddd";
66 | ctx.font = "11px Arial";
67 | }
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/editor/js/nodes/startNode.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2024, Daily
3 | *
4 | * SPDX-License-Identifier: BSD 2-Clause License
5 | */
6 |
7 | import { PipecatBaseNode } from "./baseNode.js";
8 |
9 | /**
10 | * Represents the starting node of a flow
11 | * @extends PipecatBaseNode
12 | */
13 | export class PipecatStartNode extends PipecatBaseNode {
14 | /**
15 | * Creates a new start node
16 | * @param {string} [title="Start"] - Optional custom title for the node
17 | */
18 | constructor(title = "Start") {
19 | super(title, "#2ecc71");
20 | this.addOutput("Out", "flow");
21 |
22 | // Initialize with both role and task messages for the start node
23 | this.properties = {
24 | role_messages: [
25 | {
26 | role: "system",
27 | content: "Enter bot's personality/role...",
28 | },
29 | ],
30 | task_messages: [
31 | {
32 | role: "system",
33 | content: "Enter initial task...",
34 | },
35 | ],
36 | pre_actions: [],
37 | post_actions: [],
38 | };
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/editor/js/types.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @typedef {Object} SidePanel
3 | */
4 |
5 | /**
6 | * @typedef {Object} PipecatBaseNode
7 | */
8 |
9 | /**
10 | * @typedef {Object} FlowConfig
11 | * @property {string} initial_node
12 | * @property {Object.} nodes
13 | */
14 |
15 | /**
16 | * @typedef {Object} NodeConfig
17 | * @property {Array} [role_messages] - Optional messages defining bot's role/personality
18 | * @property {Array} task_messages - Required messages defining the node's task
19 | * @property {Array} functions
20 | * @property {Array} [pre_actions]
21 | * @property {Array} [post_actions]
22 | */
23 |
24 | /**
25 | * @typedef {Object} Message
26 | * @property {string} role
27 | * @property {string} content
28 | */
29 |
30 | /**
31 | * @typedef {Object} Action
32 | * @property {string} type
33 | * @property {string} [text]
34 | */
35 |
36 | /**
37 | * @typedef {Object} Function
38 | * @property {string} type
39 | * @property {FunctionDefinition} function
40 | */
41 |
42 | /**
43 | * @typedef {Object} FunctionDefinition
44 | * @property {string} name
45 | * @property {string} description
46 | * @property {Object} parameters
47 | * @property {string} [transition_to]
48 | */
49 |
--------------------------------------------------------------------------------
/editor/js/utils/export.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2024, Daily
3 | *
4 | * SPDX-License-Identifier: BSD 2-Clause License
5 | */
6 |
7 | /**
8 | * Generates a flow configuration from the graph
9 | * @param {LGraph} graphInstance - The LiteGraph instance
10 | * @returns {FlowConfig} The generated flow configuration
11 | * @throws {Error} If the graph is invalid
12 | */
13 | export function generateFlowConfig(graphInstance) {
14 | if (!graphInstance) {
15 | throw new Error("No graph instance provided");
16 | }
17 |
18 | if (!graphInstance._nodes || !Array.isArray(graphInstance._nodes)) {
19 | throw new Error("No nodes found in the graph");
20 | }
21 |
22 | const nodes = graphInstance._nodes;
23 | let startNode = nodes.find(
24 | (node) => node.constructor.name === "PipecatStartNode",
25 | );
26 |
27 | if (!startNode) {
28 | throw new Error("No start node found in the flow");
29 | }
30 |
31 | /**
32 | * Adds handler token if needed
33 | * @param {Object} functionConfig - Function configuration to process
34 | * @param {Object} sourceNode - Node containing the function
35 | */
36 | function processHandler(functionConfig, sourceNode) {
37 | if (sourceNode.properties.function.handler) {
38 | const handlerName = sourceNode.properties.function.handler;
39 | if (!handlerName.startsWith("__function__:")) {
40 | functionConfig.function.handler = `__function__:${handlerName}`;
41 | }
42 | }
43 | }
44 |
45 | /**
46 | * Finds all functions connected to a node
47 | * @param {LGraphNode} node - The node to find functions for
48 | * @returns {Array