open-swarm 0.1.1745275181__py3-none-any.whl → 0.1.1748636259__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- open_swarm-0.1.1748636259.dist-info/METADATA +188 -0
- open_swarm-0.1.1748636259.dist-info/RECORD +82 -0
- {open_swarm-0.1.1745275181.dist-info → open_swarm-0.1.1748636259.dist-info}/WHEEL +2 -1
- open_swarm-0.1.1748636259.dist-info/entry_points.txt +3 -0
- open_swarm-0.1.1748636259.dist-info/top_level.txt +1 -0
- swarm/agent/agent.py +49 -0
- swarm/auth.py +48 -113
- swarm/consumers.py +0 -19
- swarm/extensions/blueprint/__init__.py +16 -30
- swarm/{core → extensions/blueprint}/agent_utils.py +1 -1
- swarm/extensions/blueprint/blueprint_base.py +458 -0
- swarm/extensions/blueprint/blueprint_discovery.py +112 -0
- swarm/extensions/blueprint/output_utils.py +95 -0
- swarm/{core → extensions/blueprint}/spinner.py +21 -30
- swarm/extensions/cli/cli_args.py +0 -6
- swarm/extensions/cli/commands/blueprint_management.py +9 -47
- swarm/extensions/cli/commands/config_management.py +6 -5
- swarm/extensions/cli/commands/edit_config.py +7 -16
- swarm/extensions/cli/commands/list_blueprints.py +1 -1
- swarm/extensions/cli/commands/validate_env.py +4 -11
- swarm/extensions/cli/commands/validate_envvars.py +6 -6
- swarm/extensions/cli/interactive_shell.py +2 -16
- swarm/extensions/config/config_loader.py +201 -107
- swarm/{core → extensions/config}/config_manager.py +38 -50
- swarm/{core → extensions/config}/server_config.py +0 -32
- swarm/extensions/launchers/build_launchers.py +14 -0
- swarm/{core → extensions/launchers}/build_swarm_wrapper.py +0 -0
- swarm/extensions/launchers/swarm_api.py +64 -8
- swarm/extensions/launchers/swarm_cli.py +300 -8
- swarm/llm/chat_completion.py +195 -0
- swarm/serializers.py +5 -96
- swarm/settings.py +111 -99
- swarm/urls.py +74 -57
- swarm/utils/context_utils.py +4 -10
- swarm/utils/general_utils.py +0 -21
- swarm/utils/redact.py +36 -23
- swarm/views/api_views.py +39 -48
- swarm/views/chat_views.py +70 -237
- swarm/views/core_views.py +87 -80
- swarm/views/model_views.py +121 -64
- swarm/views/utils.py +441 -65
- swarm/views/web_views.py +2 -2
- open_swarm-0.1.1745275181.dist-info/METADATA +0 -874
- open_swarm-0.1.1745275181.dist-info/RECORD +0 -319
- open_swarm-0.1.1745275181.dist-info/entry_points.txt +0 -4
- swarm/blueprints/README.md +0 -68
- swarm/blueprints/blueprint_audit_status.json +0 -27
- swarm/blueprints/chatbot/README.md +0 -40
- swarm/blueprints/chatbot/blueprint_chatbot.py +0 -471
- swarm/blueprints/chatbot/metadata.json +0 -23
- swarm/blueprints/chatbot/templates/chatbot/chatbot.html +0 -33
- swarm/blueprints/chucks_angels/README.md +0 -11
- swarm/blueprints/chucks_angels/blueprint_chucks_angels.py +0 -7
- swarm/blueprints/chucks_angels/test_basic.py +0 -3
- swarm/blueprints/codey/CODEY.md +0 -15
- swarm/blueprints/codey/README.md +0 -115
- swarm/blueprints/codey/blueprint_codey.py +0 -1072
- swarm/blueprints/codey/codey_cli.py +0 -373
- swarm/blueprints/codey/instructions.md +0 -17
- swarm/blueprints/codey/metadata.json +0 -23
- swarm/blueprints/common/operation_box_utils.py +0 -83
- swarm/blueprints/digitalbutlers/README.md +0 -11
- swarm/blueprints/digitalbutlers/__init__.py +0 -1
- swarm/blueprints/digitalbutlers/blueprint_digitalbutlers.py +0 -7
- swarm/blueprints/digitalbutlers/test_basic.py +0 -3
- swarm/blueprints/divine_code/README.md +0 -3
- swarm/blueprints/divine_code/__init__.py +0 -10
- swarm/blueprints/divine_code/apps.py +0 -11
- swarm/blueprints/divine_code/blueprint_divine_code.py +0 -270
- swarm/blueprints/django_chat/apps.py +0 -6
- swarm/blueprints/django_chat/blueprint_django_chat.py +0 -268
- swarm/blueprints/django_chat/templates/django_chat/django_chat_webpage.html +0 -37
- swarm/blueprints/django_chat/urls.py +0 -8
- swarm/blueprints/django_chat/views.py +0 -32
- swarm/blueprints/echocraft/blueprint_echocraft.py +0 -384
- swarm/blueprints/flock/README.md +0 -11
- swarm/blueprints/flock/__init__.py +0 -8
- swarm/blueprints/flock/blueprint_flock.py +0 -7
- swarm/blueprints/flock/test_basic.py +0 -3
- swarm/blueprints/geese/README.md +0 -10
- swarm/blueprints/geese/__init__.py +0 -8
- swarm/blueprints/geese/blueprint_geese.py +0 -384
- swarm/blueprints/geese/geese_cli.py +0 -102
- swarm/blueprints/jeeves/README.md +0 -41
- swarm/blueprints/jeeves/blueprint_jeeves.py +0 -722
- swarm/blueprints/jeeves/jeeves_cli.py +0 -55
- swarm/blueprints/jeeves/metadata.json +0 -24
- swarm/blueprints/mcp_demo/blueprint_mcp_demo.py +0 -473
- swarm/blueprints/messenger/templates/messenger/messenger.html +0 -46
- swarm/blueprints/mission_improbable/blueprint_mission_improbable.py +0 -423
- swarm/blueprints/monkai_magic/blueprint_monkai_magic.py +0 -340
- swarm/blueprints/nebula_shellz/blueprint_nebula_shellz.py +0 -265
- swarm/blueprints/omniplex/blueprint_omniplex.py +0 -298
- swarm/blueprints/poets/blueprint_poets.py +0 -546
- swarm/blueprints/poets/poets_cli.py +0 -23
- swarm/blueprints/rue_code/README.md +0 -8
- swarm/blueprints/rue_code/blueprint_rue_code.py +0 -448
- swarm/blueprints/rue_code/rue_code_cli.py +0 -43
- swarm/blueprints/stewie/apps.py +0 -12
- swarm/blueprints/stewie/blueprint_family_ties.py +0 -349
- swarm/blueprints/stewie/models.py +0 -19
- swarm/blueprints/stewie/serializers.py +0 -10
- swarm/blueprints/stewie/settings.py +0 -17
- swarm/blueprints/stewie/urls.py +0 -11
- swarm/blueprints/stewie/views.py +0 -26
- swarm/blueprints/suggestion/blueprint_suggestion.py +0 -222
- swarm/blueprints/whinge_surf/README.md +0 -22
- swarm/blueprints/whinge_surf/__init__.py +0 -1
- swarm/blueprints/whinge_surf/blueprint_whinge_surf.py +0 -565
- swarm/blueprints/whinge_surf/whinge_surf_cli.py +0 -99
- swarm/blueprints/whiskeytango_foxtrot/__init__.py +0 -0
- swarm/blueprints/whiskeytango_foxtrot/apps.py +0 -11
- swarm/blueprints/whiskeytango_foxtrot/blueprint_whiskeytango_foxtrot.py +0 -339
- swarm/blueprints/zeus/__init__.py +0 -2
- swarm/blueprints/zeus/apps.py +0 -4
- swarm/blueprints/zeus/blueprint_zeus.py +0 -270
- swarm/blueprints/zeus/zeus_cli.py +0 -13
- swarm/cli/async_input.py +0 -65
- swarm/cli/async_input_demo.py +0 -32
- swarm/core/blueprint_base.py +0 -769
- swarm/core/blueprint_discovery.py +0 -125
- swarm/core/blueprint_runner.py +0 -59
- swarm/core/blueprint_ux.py +0 -109
- swarm/core/build_launchers.py +0 -15
- swarm/core/cli/__init__.py +0 -1
- swarm/core/cli/commands/__init__.py +0 -1
- swarm/core/cli/commands/blueprint_management.py +0 -7
- swarm/core/cli/interactive_shell.py +0 -14
- swarm/core/cli/main.py +0 -50
- swarm/core/cli/utils/__init__.py +0 -1
- swarm/core/cli/utils/discover_commands.py +0 -18
- swarm/core/config_loader.py +0 -122
- swarm/core/output_utils.py +0 -193
- swarm/core/session_logger.py +0 -42
- swarm/core/slash_commands.py +0 -89
- swarm/core/swarm_api.py +0 -68
- swarm/core/swarm_cli.py +0 -216
- swarm/core/utils/__init__.py +0 -0
- swarm/extensions/blueprint/cli_handler.py +0 -197
- swarm/extensions/blueprint/runnable_blueprint.py +0 -42
- swarm/extensions/cli/utils/__init__.py +0 -1
- swarm/extensions/cli/utils/async_input.py +0 -46
- swarm/extensions/cli/utils/prompt_user.py +0 -3
- swarm/management/__init__.py +0 -0
- swarm/management/commands/__init__.py +0 -0
- swarm/management/commands/runserver.py +0 -58
- swarm/middleware.py +0 -65
- swarm/permissions.py +0 -38
- swarm/static/contrib/fonts/fontawesome-webfont.ttf +0 -7
- swarm/static/contrib/fonts/fontawesome-webfont.woff +0 -7
- swarm/static/contrib/fonts/fontawesome-webfont.woff2 +0 -7
- swarm/static/contrib/markedjs/marked.min.js +0 -6
- swarm/static/contrib/tabler-icons/adjustments-horizontal.svg +0 -27
- swarm/static/contrib/tabler-icons/alert-triangle.svg +0 -21
- swarm/static/contrib/tabler-icons/archive.svg +0 -21
- swarm/static/contrib/tabler-icons/artboard.svg +0 -27
- swarm/static/contrib/tabler-icons/automatic-gearbox.svg +0 -23
- swarm/static/contrib/tabler-icons/box-multiple.svg +0 -19
- swarm/static/contrib/tabler-icons/carambola.svg +0 -19
- swarm/static/contrib/tabler-icons/copy.svg +0 -20
- swarm/static/contrib/tabler-icons/download.svg +0 -21
- swarm/static/contrib/tabler-icons/edit.svg +0 -21
- swarm/static/contrib/tabler-icons/filled/carambola.svg +0 -13
- swarm/static/contrib/tabler-icons/filled/paint.svg +0 -13
- swarm/static/contrib/tabler-icons/headset.svg +0 -22
- swarm/static/contrib/tabler-icons/layout-sidebar-left-collapse.svg +0 -21
- swarm/static/contrib/tabler-icons/layout-sidebar-left-expand.svg +0 -21
- swarm/static/contrib/tabler-icons/layout-sidebar-right-collapse.svg +0 -21
- swarm/static/contrib/tabler-icons/layout-sidebar-right-expand.svg +0 -21
- swarm/static/contrib/tabler-icons/message-chatbot.svg +0 -22
- swarm/static/contrib/tabler-icons/message-star.svg +0 -22
- swarm/static/contrib/tabler-icons/message-x.svg +0 -23
- swarm/static/contrib/tabler-icons/message.svg +0 -21
- swarm/static/contrib/tabler-icons/paperclip.svg +0 -18
- swarm/static/contrib/tabler-icons/playlist-add.svg +0 -22
- swarm/static/contrib/tabler-icons/robot.svg +0 -26
- swarm/static/contrib/tabler-icons/search.svg +0 -19
- swarm/static/contrib/tabler-icons/settings.svg +0 -20
- swarm/static/contrib/tabler-icons/thumb-down.svg +0 -19
- swarm/static/contrib/tabler-icons/thumb-up.svg +0 -19
- swarm/static/css/dropdown.css +0 -22
- swarm/static/htmx/htmx.min.js +0 -0
- swarm/static/js/dropdown.js +0 -23
- swarm/static/rest_mode/css/base.css +0 -470
- swarm/static/rest_mode/css/chat-history.css +0 -286
- swarm/static/rest_mode/css/chat.css +0 -251
- swarm/static/rest_mode/css/chatbot.css +0 -74
- swarm/static/rest_mode/css/chatgpt.css +0 -62
- swarm/static/rest_mode/css/colors/corporate.css +0 -74
- swarm/static/rest_mode/css/colors/pastel.css +0 -81
- swarm/static/rest_mode/css/colors/tropical.css +0 -82
- swarm/static/rest_mode/css/general.css +0 -142
- swarm/static/rest_mode/css/layout.css +0 -167
- swarm/static/rest_mode/css/layouts/messenger-layout.css +0 -17
- swarm/static/rest_mode/css/layouts/minimalist-layout.css +0 -57
- swarm/static/rest_mode/css/layouts/mobile-layout.css +0 -8
- swarm/static/rest_mode/css/messages.css +0 -84
- swarm/static/rest_mode/css/messenger.css +0 -135
- swarm/static/rest_mode/css/settings.css +0 -91
- swarm/static/rest_mode/css/simple.css +0 -44
- swarm/static/rest_mode/css/slack.css +0 -58
- swarm/static/rest_mode/css/style.css +0 -156
- swarm/static/rest_mode/css/theme.css +0 -30
- swarm/static/rest_mode/css/toast.css +0 -40
- swarm/static/rest_mode/js/auth.js +0 -9
- swarm/static/rest_mode/js/blueprint.js +0 -41
- swarm/static/rest_mode/js/blueprintUtils.js +0 -12
- swarm/static/rest_mode/js/chatLogic.js +0 -79
- swarm/static/rest_mode/js/debug.js +0 -63
- swarm/static/rest_mode/js/events.js +0 -98
- swarm/static/rest_mode/js/main.js +0 -19
- swarm/static/rest_mode/js/messages.js +0 -264
- swarm/static/rest_mode/js/messengerLogic.js +0 -355
- swarm/static/rest_mode/js/modules/apiService.js +0 -84
- swarm/static/rest_mode/js/modules/blueprintManager.js +0 -162
- swarm/static/rest_mode/js/modules/chatHistory.js +0 -110
- swarm/static/rest_mode/js/modules/debugLogger.js +0 -14
- swarm/static/rest_mode/js/modules/eventHandlers.js +0 -107
- swarm/static/rest_mode/js/modules/messageProcessor.js +0 -120
- swarm/static/rest_mode/js/modules/state.js +0 -7
- swarm/static/rest_mode/js/modules/userInteractions.js +0 -29
- swarm/static/rest_mode/js/modules/validation.js +0 -23
- swarm/static/rest_mode/js/rendering.js +0 -119
- swarm/static/rest_mode/js/settings.js +0 -130
- swarm/static/rest_mode/js/sidebar.js +0 -94
- swarm/static/rest_mode/js/simpleLogic.js +0 -37
- swarm/static/rest_mode/js/slackLogic.js +0 -66
- swarm/static/rest_mode/js/splash.js +0 -76
- swarm/static/rest_mode/js/theme.js +0 -111
- swarm/static/rest_mode/js/toast.js +0 -36
- swarm/static/rest_mode/js/ui.js +0 -265
- swarm/static/rest_mode/js/validation.js +0 -57
- swarm/static/rest_mode/svg/animated_spinner.svg +0 -12
- swarm/static/rest_mode/svg/arrow_down.svg +0 -5
- swarm/static/rest_mode/svg/arrow_left.svg +0 -5
- swarm/static/rest_mode/svg/arrow_right.svg +0 -5
- swarm/static/rest_mode/svg/arrow_up.svg +0 -5
- swarm/static/rest_mode/svg/attach.svg +0 -8
- swarm/static/rest_mode/svg/avatar.svg +0 -7
- swarm/static/rest_mode/svg/canvas.svg +0 -6
- swarm/static/rest_mode/svg/chat_history.svg +0 -4
- swarm/static/rest_mode/svg/close.svg +0 -5
- swarm/static/rest_mode/svg/copy.svg +0 -4
- swarm/static/rest_mode/svg/dark_mode.svg +0 -3
- swarm/static/rest_mode/svg/edit.svg +0 -5
- swarm/static/rest_mode/svg/layout.svg +0 -9
- swarm/static/rest_mode/svg/logo.svg +0 -29
- swarm/static/rest_mode/svg/logout.svg +0 -5
- swarm/static/rest_mode/svg/mobile.svg +0 -5
- swarm/static/rest_mode/svg/new_chat.svg +0 -4
- swarm/static/rest_mode/svg/not_visible.svg +0 -5
- swarm/static/rest_mode/svg/plus.svg +0 -7
- swarm/static/rest_mode/svg/run_code.svg +0 -6
- swarm/static/rest_mode/svg/save.svg +0 -4
- swarm/static/rest_mode/svg/search.svg +0 -6
- swarm/static/rest_mode/svg/settings.svg +0 -4
- swarm/static/rest_mode/svg/speaker.svg +0 -5
- swarm/static/rest_mode/svg/stop.svg +0 -6
- swarm/static/rest_mode/svg/thumbs_down.svg +0 -3
- swarm/static/rest_mode/svg/thumbs_up.svg +0 -3
- swarm/static/rest_mode/svg/toggle_off.svg +0 -6
- swarm/static/rest_mode/svg/toggle_on.svg +0 -6
- swarm/static/rest_mode/svg/trash.svg +0 -10
- swarm/static/rest_mode/svg/undo.svg +0 -3
- swarm/static/rest_mode/svg/visible.svg +0 -8
- swarm/static/rest_mode/svg/voice.svg +0 -10
- swarm/templates/account/login.html +0 -22
- swarm/templates/account/signup.html +0 -32
- swarm/templates/base.html +0 -30
- swarm/templates/chat.html +0 -43
- swarm/templates/index.html +0 -35
- swarm/templates/rest_mode/components/chat_sidebar.html +0 -55
- swarm/templates/rest_mode/components/header.html +0 -45
- swarm/templates/rest_mode/components/main_chat_pane.html +0 -41
- swarm/templates/rest_mode/components/settings_dialog.html +0 -97
- swarm/templates/rest_mode/components/splash_screen.html +0 -7
- swarm/templates/rest_mode/components/top_bar.html +0 -28
- swarm/templates/rest_mode/message_ui.html +0 -50
- swarm/templates/rest_mode/slackbot.html +0 -30
- swarm/templates/simple_blueprint_page.html +0 -24
- swarm/templates/websocket_partials/final_system_message.html +0 -3
- swarm/templates/websocket_partials/system_message.html +0 -4
- swarm/templates/websocket_partials/user_message.html +0 -5
- swarm/utils/ansi_box.py +0 -34
- swarm/utils/disable_tracing.py +0 -38
- swarm/utils/log_utils.py +0 -63
- swarm/utils/openai_patch.py +0 -33
- swarm/ux/ansi_box.py +0 -43
- swarm/ux/spinner.py +0 -53
- {open_swarm-0.1.1745275181.dist-info → open_swarm-0.1.1748636259.dist-info}/licenses/LICENSE +0 -0
- /swarm/{core → extensions/blueprint}/blueprint_utils.py +0 -0
- /swarm/{core → extensions/blueprint}/common_utils.py +0 -0
- /swarm/{core → extensions/config}/setup_wizard.py +0 -0
- /swarm/{blueprints/rue_code → extensions/config/utils}/__init__.py +0 -0
- /swarm/{core → extensions/config}/utils/logger.py +0 -0
- /swarm/{core → extensions/launchers}/swarm_wrapper.py +0 -0
@@ -1,12 +1,68 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
1
|
+
#!/usr/bin/env python3
|
2
|
+
import argparse
|
3
|
+
import subprocess
|
4
4
|
import sys
|
5
|
-
import
|
6
|
-
from swarm.core.swarm_api import main
|
5
|
+
from os import path, listdir, makedirs
|
7
6
|
|
8
|
-
def
|
9
|
-
|
7
|
+
def main():
|
8
|
+
parser = argparse.ArgumentParser(description="Swarm REST Launcher")
|
9
|
+
parser.add_argument("--blueprint", required=True, help="Comma-separated blueprint file paths or names for configuration purposes")
|
10
|
+
parser.add_argument("--port", type=int, default=8000, help="Port to run the REST server")
|
11
|
+
parser.add_argument("--config", default="~/.swarm/swarm_config.json", help="Configuration file path")
|
12
|
+
parser.add_argument("--daemon", action="store_true", help="Run in daemon mode and print process id")
|
13
|
+
args = parser.parse_args()
|
14
|
+
|
15
|
+
# Split blueprints by comma and strip whitespace
|
16
|
+
bp_list = [bp.strip() for bp in args.blueprint.split(",") if bp.strip()]
|
17
|
+
blueprint_paths = []
|
18
|
+
for bp_arg in bp_list:
|
19
|
+
resolved = None
|
20
|
+
if path.exists(bp_arg):
|
21
|
+
if path.isdir(bp_arg):
|
22
|
+
resolved = bp_arg
|
23
|
+
print(f"Using blueprint directory: {resolved}")
|
24
|
+
else:
|
25
|
+
resolved = bp_arg
|
26
|
+
print(f"Using blueprint file: {resolved}")
|
27
|
+
else:
|
28
|
+
managed_path = path.expanduser("~/.swarm/blueprints/" + bp_arg)
|
29
|
+
if path.isdir(managed_path):
|
30
|
+
matches = [f for f in listdir(managed_path) if f.startswith("blueprint_") and f.endswith(".py")]
|
31
|
+
if not matches:
|
32
|
+
print("Error: No blueprint file found in managed directory:", managed_path)
|
33
|
+
sys.exit(1)
|
34
|
+
resolved = path.join(managed_path, matches[0])
|
35
|
+
print(f"Using managed blueprint: {resolved}")
|
36
|
+
else:
|
37
|
+
print("Warning: Blueprint not found:", bp_arg, "- skipping.")
|
38
|
+
continue
|
39
|
+
if resolved:
|
40
|
+
blueprint_paths.append(resolved)
|
41
|
+
|
42
|
+
if not blueprint_paths:
|
43
|
+
print("Error: No valid blueprints found.")
|
44
|
+
sys.exit(1)
|
45
|
+
print("Blueprints to be configured:")
|
46
|
+
for bp in blueprint_paths:
|
47
|
+
print(" -", bp)
|
48
|
+
|
49
|
+
config_path = path.expanduser(args.config)
|
50
|
+
if not path.exists(config_path):
|
51
|
+
makedirs(path.dirname(config_path), exist_ok=True)
|
52
|
+
with open(config_path, 'w') as f:
|
53
|
+
f.write("{}")
|
54
|
+
print("Default config file created at:", config_path)
|
55
|
+
|
56
|
+
print("Launching Django server on port 0.0.0.0:{}".format(args.port))
|
57
|
+
try:
|
58
|
+
if args.daemon:
|
59
|
+
proc = subprocess.Popen(["python", "manage.py", "runserver", f"0.0.0.0:{args.port}"])
|
60
|
+
print("Running in daemon mode. Process ID:", proc.pid)
|
61
|
+
else:
|
62
|
+
subprocess.run(["python", "manage.py", "runserver", f"0.0.0.0:{args.port}"], check=True)
|
63
|
+
except subprocess.CalledProcessError as e:
|
64
|
+
print("Error launching Django server:", e)
|
65
|
+
sys.exit(1)
|
10
66
|
|
11
67
|
if __name__ == "__main__":
|
12
|
-
|
68
|
+
main()
|
@@ -1,12 +1,304 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
import sys
|
1
|
+
#!/usr/bin/env python3
|
2
|
+
import argparse
|
3
|
+
import importlib.util
|
5
4
|
import os
|
6
|
-
|
5
|
+
import sys
|
6
|
+
import subprocess
|
7
|
+
import shutil
|
8
|
+
import json
|
9
|
+
import PyInstaller.__main__
|
7
10
|
|
8
|
-
def
|
9
|
-
|
11
|
+
def resolve_env_vars(data):
|
12
|
+
if isinstance(data, dict):
|
13
|
+
return {k: resolve_env_vars(v) for k, v in data.items()}
|
14
|
+
elif isinstance(data, list):
|
15
|
+
return [resolve_env_vars(item) for item in data]
|
16
|
+
elif isinstance(data, str):
|
17
|
+
return os.path.expandvars(data)
|
18
|
+
else:
|
19
|
+
return data
|
20
|
+
|
21
|
+
MANAGED_DIR = os.path.expanduser("~/.swarm/blueprints")
|
22
|
+
BIN_DIR = os.path.expanduser("~/.swarm/bin")
|
23
|
+
|
24
|
+
def ensure_managed_dir():
|
25
|
+
if not os.path.exists(MANAGED_DIR):
|
26
|
+
os.makedirs(MANAGED_DIR, exist_ok=True)
|
27
|
+
if not os.path.exists(BIN_DIR):
|
28
|
+
os.makedirs(BIN_DIR, exist_ok=True)
|
29
|
+
|
30
|
+
def add_blueprint(source_path, blueprint_name=None):
|
31
|
+
source_path = os.path.normpath(source_path)
|
32
|
+
if not os.path.exists(source_path):
|
33
|
+
print("Error: source file/directory does not exist:", source_path)
|
34
|
+
sys.exit(1)
|
35
|
+
if os.path.isdir(source_path):
|
36
|
+
if not blueprint_name:
|
37
|
+
blueprint_name = os.path.basename(os.path.normpath(source_path))
|
38
|
+
target_dir = os.path.join(MANAGED_DIR, blueprint_name)
|
39
|
+
if os.path.exists(target_dir):
|
40
|
+
shutil.rmtree(target_dir)
|
41
|
+
os.makedirs(target_dir, exist_ok=True)
|
42
|
+
for root, dirs, files in os.walk(source_path):
|
43
|
+
rel_path = os.path.relpath(root, source_path)
|
44
|
+
dest_root = os.path.join(target_dir, rel_path) if rel_path != '.' else target_dir
|
45
|
+
os.makedirs(dest_root, exist_ok=True)
|
46
|
+
for file in files:
|
47
|
+
shutil.copy2(os.path.join(root, file), os.path.join(dest_root, file))
|
48
|
+
print(f"Blueprint '{blueprint_name}' added successfully to {target_dir}.")
|
49
|
+
else:
|
50
|
+
blueprint_file = source_path
|
51
|
+
if not blueprint_name:
|
52
|
+
base = os.path.basename(blueprint_file)
|
53
|
+
if base.startswith("blueprint_") and base.endswith(".py"):
|
54
|
+
blueprint_name = base[len("blueprint_"):-3]
|
55
|
+
else:
|
56
|
+
blueprint_name = os.path.splitext(base)[0]
|
57
|
+
target_dir = os.path.join(MANAGED_DIR, blueprint_name)
|
58
|
+
os.makedirs(target_dir, exist_ok=True)
|
59
|
+
target_file = os.path.join(target_dir, f"blueprint_{blueprint_name}.py")
|
60
|
+
shutil.copy2(blueprint_file, target_file)
|
61
|
+
print(f"Blueprint '{blueprint_name}' added successfully to {target_dir}.")
|
62
|
+
|
63
|
+
def list_blueprints():
|
64
|
+
ensure_managed_dir()
|
65
|
+
entries = os.listdir(MANAGED_DIR)
|
66
|
+
blueprints = [d for d in entries if os.path.isdir(os.path.join(MANAGED_DIR, d))]
|
67
|
+
if blueprints:
|
68
|
+
print("Registered blueprints:")
|
69
|
+
for bp in blueprints:
|
70
|
+
print(" -", bp)
|
71
|
+
else:
|
72
|
+
print("No blueprints registered.")
|
73
|
+
|
74
|
+
def delete_blueprint(blueprint_name):
|
75
|
+
target_dir = os.path.join(MANAGED_DIR, blueprint_name)
|
76
|
+
if os.path.exists(target_dir) and os.path.isdir(target_dir):
|
77
|
+
shutil.rmtree(target_dir)
|
78
|
+
print(f"Blueprint '{blueprint_name}' deleted successfully.")
|
79
|
+
else:
|
80
|
+
print(f"Error: Blueprint '{blueprint_name}' does not exist.")
|
81
|
+
sys.exit(1)
|
82
|
+
|
83
|
+
def run_blueprint(blueprint_name):
|
84
|
+
target_dir = os.path.join(MANAGED_DIR, blueprint_name)
|
85
|
+
blueprint_file = os.path.join(target_dir, f"blueprint_{blueprint_name}.py")
|
86
|
+
if not os.path.exists(blueprint_file):
|
87
|
+
print(f"Error: Blueprint file not found for '{blueprint_name}'. Install it using 'swarm-cli add <path>'.")
|
88
|
+
sys.exit(1)
|
89
|
+
spec = importlib.util.spec_from_file_location("blueprint_module", blueprint_file)
|
90
|
+
if spec is None or spec.loader is None:
|
91
|
+
print("Error: Failed to load blueprint module from:", blueprint_file)
|
92
|
+
sys.exit(1)
|
93
|
+
blueprint = importlib.util.module_from_spec(spec)
|
94
|
+
loader = spec.loader
|
95
|
+
src_path = os.path.join(os.getcwd(), "src")
|
96
|
+
if src_path not in sys.path:
|
97
|
+
sys.path.insert(0, src_path)
|
98
|
+
loader.exec_module(blueprint)
|
99
|
+
if hasattr(blueprint, "main"):
|
100
|
+
blueprint.main()
|
101
|
+
else:
|
102
|
+
print("Error: The blueprint does not have a main() function.")
|
103
|
+
sys.exit(1)
|
104
|
+
|
105
|
+
def install_blueprint(blueprint_name):
|
106
|
+
target_dir = os.path.join(MANAGED_DIR, blueprint_name)
|
107
|
+
blueprint_file = os.path.join(target_dir, f"blueprint_{blueprint_name}.py")
|
108
|
+
if not os.path.exists(blueprint_file):
|
109
|
+
print(f"Error: Blueprint '{blueprint_name}' is not registered. Add it using 'swarm-cli add <path>'.")
|
110
|
+
sys.exit(1)
|
111
|
+
cli_name = blueprint_name # Use blueprint_name as default cli_name for simplicity
|
112
|
+
try:
|
113
|
+
PyInstaller.__main__.run([
|
114
|
+
blueprint_file,
|
115
|
+
"--onefile",
|
116
|
+
"--name", cli_name,
|
117
|
+
"--distpath", BIN_DIR,
|
118
|
+
"--workpath", os.path.join(target_dir, "build"),
|
119
|
+
"--specpath", target_dir
|
120
|
+
])
|
121
|
+
except KeyboardInterrupt:
|
122
|
+
print("Installation aborted by user request.")
|
123
|
+
sys.exit(1)
|
124
|
+
print(f"Blueprint '{blueprint_name}' installed as CLI utility '{cli_name}' at: {os.path.join(BIN_DIR, cli_name)}")
|
125
|
+
|
126
|
+
def uninstall_blueprint(blueprint_name, blueprint_only=False, wrapper_only=False):
|
127
|
+
target_dir = os.path.join(MANAGED_DIR, blueprint_name)
|
128
|
+
blueprint_file = os.path.join(target_dir, f"blueprint_{blueprint_name}.py")
|
129
|
+
cli_name = blueprint_name # Default to blueprint_name for uninstall
|
130
|
+
cli_path = os.path.join(BIN_DIR, cli_name)
|
131
|
+
removed = False
|
132
|
+
|
133
|
+
if not blueprint_only and not wrapper_only: # Remove both by default
|
134
|
+
if os.path.exists(target_dir):
|
135
|
+
shutil.rmtree(target_dir)
|
136
|
+
print(f"Blueprint '{blueprint_name}' removed from {MANAGED_DIR}.")
|
137
|
+
removed = True
|
138
|
+
if os.path.exists(cli_path):
|
139
|
+
os.remove(cli_path)
|
140
|
+
print(f"Wrapper '{cli_name}' removed from {BIN_DIR}.")
|
141
|
+
removed = True
|
142
|
+
elif blueprint_only:
|
143
|
+
if os.path.exists(target_dir):
|
144
|
+
shutil.rmtree(target_dir)
|
145
|
+
print(f"Blueprint '{blueprint_name}' removed from {MANAGED_DIR}.")
|
146
|
+
removed = True
|
147
|
+
elif wrapper_only:
|
148
|
+
if os.path.exists(cli_path):
|
149
|
+
os.remove(cli_path)
|
150
|
+
print(f"Wrapper '{cli_name}' removed from {BIN_DIR}.")
|
151
|
+
removed = True
|
152
|
+
|
153
|
+
if not removed:
|
154
|
+
print(f"Error: Nothing to uninstall for '{blueprint_name}' with specified options.")
|
155
|
+
sys.exit(1)
|
156
|
+
|
157
|
+
def main():
|
158
|
+
os.environ.pop("SWARM_BLUEPRINTS", None)
|
159
|
+
parser = argparse.ArgumentParser(
|
160
|
+
description="Swarm CLI Launcher\n\nSubcommands:\n"
|
161
|
+
" add : Add a blueprint to the managed directory.\n"
|
162
|
+
" list : List registered blueprints.\n"
|
163
|
+
" delete : Delete a registered blueprint.\n"
|
164
|
+
" run : Run a blueprint by name.\n"
|
165
|
+
" install : Install a blueprint as a CLI utility with PyInstaller.\n"
|
166
|
+
" uninstall : Uninstall a blueprint and/or its CLI wrapper.\n"
|
167
|
+
" migrate : Apply Django database migrations.\n"
|
168
|
+
" config : Manage swarm configuration (LLM and MCP servers).",
|
169
|
+
formatter_class=argparse.RawTextHelpFormatter)
|
170
|
+
subparsers = parser.add_subparsers(dest="command", required=True, help="Available subcommands")
|
171
|
+
|
172
|
+
parser_add = subparsers.add_parser("add", help="Add a blueprint from a file or directory.")
|
173
|
+
parser_add.add_argument("source", help="Source blueprint file or directory.")
|
174
|
+
parser_add.add_argument("--name", help="Optional blueprint name. If not provided, inferred from filename.")
|
175
|
+
|
176
|
+
parser_list = subparsers.add_parser("list", help="List registered blueprints.")
|
177
|
+
|
178
|
+
parser_delete = subparsers.add_parser("delete", help="Delete a registered blueprint by name.")
|
179
|
+
parser_delete.add_argument("name", help="Blueprint name to delete.")
|
180
|
+
|
181
|
+
parser_run = subparsers.add_parser("run", help="Run a blueprint by name.")
|
182
|
+
parser_run.add_argument("name", help="Blueprint name to run.")
|
183
|
+
parser_run.add_argument("--config", default="~/.swarm/swarm_config.json", help="Path to configuration file.")
|
184
|
+
|
185
|
+
parser_install = subparsers.add_parser("install", help="Install a blueprint as a CLI utility with PyInstaller.")
|
186
|
+
parser_install.add_argument("name", help="Blueprint name to install as a CLI utility.")
|
187
|
+
|
188
|
+
parser_uninstall = subparsers.add_parser("uninstall", help="Uninstall a blueprint and/or its CLI wrapper.")
|
189
|
+
parser_uninstall.add_argument("name", help="Blueprint name to uninstall.")
|
190
|
+
parser_uninstall.add_argument("--blueprint-only", action="store_true", help="Remove only the blueprint directory.")
|
191
|
+
parser_uninstall.add_argument("--wrapper-only", action="store_true", help="Remove only the CLI wrapper.")
|
192
|
+
|
193
|
+
parser_migrate = subparsers.add_parser("migrate", help="Apply Django database migrations.")
|
194
|
+
|
195
|
+
parser_config = subparsers.add_parser("config", help="Manage swarm configuration (LLM and MCP servers).")
|
196
|
+
parser_config.add_argument("action", choices=["add", "list", "remove"], help="Action to perform on configuration")
|
197
|
+
parser_config.add_argument("--section", required=True, choices=["llm", "mcpServers"], help="Configuration section to manage")
|
198
|
+
parser_config.add_argument("--name", help="Name of the configuration entry (required for add and remove)")
|
199
|
+
parser_config.add_argument("--json", help="JSON string for configuration entry (required for add)")
|
200
|
+
parser_config.add_argument("--config", default="~/.swarm/swarm_config.json", help="Path to configuration file")
|
201
|
+
|
202
|
+
args = parser.parse_args()
|
203
|
+
ensure_managed_dir()
|
204
|
+
|
205
|
+
if args.command == "add":
|
206
|
+
add_blueprint(args.source, args.name)
|
207
|
+
elif args.command == "list":
|
208
|
+
list_blueprints()
|
209
|
+
elif args.command == "delete":
|
210
|
+
delete_blueprint(args.name)
|
211
|
+
elif args.command == "run":
|
212
|
+
config_path = os.path.expanduser(args.config)
|
213
|
+
if not os.path.exists(config_path):
|
214
|
+
os.makedirs(os.path.dirname(config_path), exist_ok=True)
|
215
|
+
default_config = {"llm": {}, "mcpServers": {}}
|
216
|
+
with open(config_path, 'w') as f:
|
217
|
+
json.dump(default_config, f, indent=4)
|
218
|
+
print("Default config file created at:", config_path)
|
219
|
+
run_blueprint(args.name)
|
220
|
+
elif args.command == "install":
|
221
|
+
install_blueprint(args.name)
|
222
|
+
elif args.command == "uninstall":
|
223
|
+
uninstall_blueprint(args.name, args.blueprint_only, args.wrapper_only)
|
224
|
+
elif args.command == "migrate":
|
225
|
+
try:
|
226
|
+
subprocess.run(["python", "manage.py", "migrate"], check=True)
|
227
|
+
print("Migrations applied successfully.")
|
228
|
+
except subprocess.CalledProcessError as e:
|
229
|
+
print("Error applying migrations:", e)
|
230
|
+
sys.exit(1)
|
231
|
+
elif args.command == "config":
|
232
|
+
config_path = os.path.expanduser(args.config)
|
233
|
+
if not os.path.exists(config_path):
|
234
|
+
default_conf = {"llm": {}, "mcpServers": {}}
|
235
|
+
os.makedirs(os.path.dirname(config_path), exist_ok=True)
|
236
|
+
with open(config_path, "w") as f:
|
237
|
+
json.dump(default_conf, f, indent=4)
|
238
|
+
print("Default config file created at:", config_path)
|
239
|
+
config = default_conf
|
240
|
+
else:
|
241
|
+
try:
|
242
|
+
with open(config_path, "r") as f:
|
243
|
+
config = json.load(f)
|
244
|
+
except json.JSONDecodeError:
|
245
|
+
print("Error: Invalid configuration file.")
|
246
|
+
sys.exit(1)
|
247
|
+
section = args.section
|
248
|
+
if args.action == "list":
|
249
|
+
entries = config.get(section, {})
|
250
|
+
if entries:
|
251
|
+
print(f"Entries in {section}:")
|
252
|
+
for key, value in entries.items():
|
253
|
+
print(f" - {key}: {json.dumps(value, indent=4)}")
|
254
|
+
else:
|
255
|
+
print(f"No entries found in {section}.")
|
256
|
+
elif args.action == "add":
|
257
|
+
if args.section == "mcpServers" and not args.name:
|
258
|
+
if not args.json:
|
259
|
+
print("Error: --json is required for adding an mcpServers block when --name is omitted.")
|
260
|
+
sys.exit(1)
|
261
|
+
try:
|
262
|
+
update_data = json.loads(args.json)
|
263
|
+
except json.JSONDecodeError:
|
264
|
+
print("Error: --json must be a valid JSON string.")
|
265
|
+
sys.exit(1)
|
266
|
+
if "mcpServers" not in update_data:
|
267
|
+
print("Error: JSON block must contain 'mcpServers' key for merging.")
|
268
|
+
sys.exit(1)
|
269
|
+
config.setdefault("mcpServers", {})
|
270
|
+
config["mcpServers"].update(update_data["mcpServers"])
|
271
|
+
with open(config_path, "w") as f:
|
272
|
+
json.dump(config, f, indent=4)
|
273
|
+
print("MCP servers updated in configuration.")
|
274
|
+
else:
|
275
|
+
if not args.name or not args.json:
|
276
|
+
print("Error: --name and --json are required for adding an entry.")
|
277
|
+
sys.exit(1)
|
278
|
+
try:
|
279
|
+
entry_data = json.loads(args.json)
|
280
|
+
except json.JSONDecodeError:
|
281
|
+
print("Error: --json must be a valid JSON string.")
|
282
|
+
sys.exit(1)
|
283
|
+
config.setdefault(section, {})[args.name] = entry_data
|
284
|
+
with open(config_path, "w") as f:
|
285
|
+
json.dump(config, f, indent=4)
|
286
|
+
print(f"Entry '{args.name}' added to {section} in configuration.")
|
287
|
+
elif args.action == "remove":
|
288
|
+
if not args.name:
|
289
|
+
print("Error: --name is required for removing an entry.")
|
290
|
+
sys.exit(1)
|
291
|
+
if args.name in config.get(section, {}):
|
292
|
+
del config[section][args.name]
|
293
|
+
with open(config_path, "w") as f:
|
294
|
+
json.dump(config, f, indent=4)
|
295
|
+
print(f"Entry '{args.name}' removed from {section} in configuration.")
|
296
|
+
else:
|
297
|
+
print(f"Error: Entry '{args.name}' not found in {section}.")
|
298
|
+
sys.exit(1)
|
299
|
+
else:
|
300
|
+
parser.print_help()
|
301
|
+
sys.exit(1)
|
10
302
|
|
11
303
|
if __name__ == "__main__":
|
12
|
-
|
304
|
+
main()
|
@@ -0,0 +1,195 @@
|
|
1
|
+
"""
|
2
|
+
Chat Completion Module
|
3
|
+
|
4
|
+
This module handles chat completion logic for the Swarm framework, including message preparation,
|
5
|
+
tool call repair, and interaction with the OpenAI API. Located in llm/ for LLM-specific functionality.
|
6
|
+
"""
|
7
|
+
|
8
|
+
import os
|
9
|
+
import json
|
10
|
+
import logging
|
11
|
+
from typing import List, Optional, Dict, Any, Union, AsyncGenerator # Added AsyncGenerator
|
12
|
+
from collections import defaultdict
|
13
|
+
|
14
|
+
import asyncio
|
15
|
+
from openai import AsyncOpenAI, OpenAIError
|
16
|
+
# Make sure ChatCompletionMessage is correctly imported if it's defined elsewhere
|
17
|
+
# Assuming it might be part of the base model or a common types module
|
18
|
+
# For now, let's assume it's implicitly handled or use a dict directly
|
19
|
+
# from ..types import ChatCompletionMessage, Agent # If defined in types
|
20
|
+
from ..types import Agent # Import Agent
|
21
|
+
from ..utils.redact import redact_sensitive_data
|
22
|
+
from ..utils.general_utils import serialize_datetime
|
23
|
+
from ..utils.message_utils import filter_duplicate_system_messages, update_null_content
|
24
|
+
from ..utils.context_utils import get_token_count, truncate_message_history
|
25
|
+
from ..utils.message_sequence import repair_message_payload
|
26
|
+
|
27
|
+
# Configure module-level logging
|
28
|
+
logger = logging.getLogger(__name__)
|
29
|
+
logger.setLevel(logging.DEBUG)
|
30
|
+
if not logger.handlers:
|
31
|
+
stream_handler = logging.StreamHandler()
|
32
|
+
formatter = logging.Formatter("[%(levelname)s] %(asctime)s - %(name)s - %(message)s")
|
33
|
+
stream_handler.setFormatter(formatter)
|
34
|
+
logger.addHandler(stream_handler)
|
35
|
+
|
36
|
+
|
37
|
+
async def get_chat_completion(
|
38
|
+
client: AsyncOpenAI,
|
39
|
+
agent: Agent,
|
40
|
+
history: List[Dict[str, Any]],
|
41
|
+
context_variables: dict,
|
42
|
+
current_llm_config: Dict[str, Any],
|
43
|
+
max_context_tokens: int,
|
44
|
+
max_context_messages: int,
|
45
|
+
tools: Optional[List[Dict[str, Any]]] = None, # <-- Added tools parameter
|
46
|
+
tool_choice: Optional[str] = "auto", # <-- Added tool_choice parameter
|
47
|
+
model_override: Optional[str] = None,
|
48
|
+
stream: bool = False,
|
49
|
+
debug: bool = False
|
50
|
+
) -> Union[Dict[str, Any], AsyncGenerator[Any, None]]: # Adjusted return type hint
|
51
|
+
"""
|
52
|
+
Retrieve a chat completion from the LLM for the given agent and history.
|
53
|
+
|
54
|
+
Args:
|
55
|
+
client: AsyncOpenAI client instance.
|
56
|
+
agent: The agent processing the completion.
|
57
|
+
history: List of previous messages in the conversation.
|
58
|
+
context_variables: Variables to include in the agent's context.
|
59
|
+
current_llm_config: Current LLM configuration dictionary.
|
60
|
+
max_context_tokens: Maximum token limit for context.
|
61
|
+
max_context_messages: Maximum message limit for context.
|
62
|
+
tools: Optional list of tools in OpenAI format.
|
63
|
+
tool_choice: Tool choice mode (e.g., "auto", "none").
|
64
|
+
model_override: Optional model to use instead of default.
|
65
|
+
stream: If True, stream the response; otherwise, return complete.
|
66
|
+
debug: If True, log detailed debugging information.
|
67
|
+
|
68
|
+
Returns:
|
69
|
+
Union[Dict[str, Any], AsyncGenerator[Any, None]]: The LLM's response message (as dict) or stream.
|
70
|
+
"""
|
71
|
+
if not agent:
|
72
|
+
logger.error("Cannot generate chat completion: Agent is None")
|
73
|
+
raise ValueError("Agent is required")
|
74
|
+
|
75
|
+
logger.debug(f"Generating chat completion for agent '{agent.name}'")
|
76
|
+
active_model = model_override or current_llm_config.get("model", "default")
|
77
|
+
client_kwargs = {
|
78
|
+
"api_key": current_llm_config.get("api_key"),
|
79
|
+
"base_url": current_llm_config.get("base_url")
|
80
|
+
}
|
81
|
+
client_kwargs = {k: v for k, v in client_kwargs.items() if v is not None}
|
82
|
+
redacted_kwargs = redact_sensitive_data(client_kwargs, sensitive_keys=["api_key"])
|
83
|
+
logger.debug(f"Using client with model='{active_model}', base_url='{client_kwargs.get('base_url', 'default')}', api_key={redacted_kwargs['api_key']}")
|
84
|
+
|
85
|
+
context_variables = defaultdict(str, context_variables)
|
86
|
+
instructions = agent.instructions(context_variables) if callable(agent.instructions) else agent.instructions
|
87
|
+
if not isinstance(instructions, str):
|
88
|
+
logger.warning(f"Invalid instructions type for '{agent.name}': {type(instructions)}. Converting to string.")
|
89
|
+
instructions = str(instructions)
|
90
|
+
messages = repair_message_payload([{"role": "system", "content": instructions}], debug=debug)
|
91
|
+
|
92
|
+
if not isinstance(history, list):
|
93
|
+
logger.error(f"Invalid history type for '{agent.name}': {type(history)}. Expected list.")
|
94
|
+
history = []
|
95
|
+
seen_ids = set()
|
96
|
+
for msg in history:
|
97
|
+
msg_id = msg.get("id", hash(json.dumps(msg, sort_keys=True, default=serialize_datetime)))
|
98
|
+
if msg_id not in seen_ids:
|
99
|
+
seen_ids.add(msg_id)
|
100
|
+
if "tool_calls" in msg and msg["tool_calls"] is not None and not isinstance(msg["tool_calls"], list):
|
101
|
+
logger.warning(f"Invalid tool_calls in history for '{msg.get('sender', 'unknown')}': {msg['tool_calls']}. Setting to None.")
|
102
|
+
msg["tool_calls"] = None
|
103
|
+
# Ensure content: None becomes content: "" for API compatibility
|
104
|
+
if "content" in msg and msg["content"] is None:
|
105
|
+
msg["content"] = ""
|
106
|
+
messages.append(msg)
|
107
|
+
messages = filter_duplicate_system_messages(messages)
|
108
|
+
messages = truncate_message_history(messages, active_model, max_context_tokens, max_context_messages)
|
109
|
+
messages = repair_message_payload(messages, debug=debug) # Ensure tool calls are paired post-truncation
|
110
|
+
# Final content None -> "" check after repair
|
111
|
+
messages = update_null_content(messages)
|
112
|
+
|
113
|
+
logger.debug(f"Prepared {len(messages)} messages for '{agent.name}'")
|
114
|
+
if debug:
|
115
|
+
logger.debug(f"Messages: {json.dumps(messages, indent=2, default=str)}")
|
116
|
+
|
117
|
+
create_params = {
|
118
|
+
"model": active_model,
|
119
|
+
"messages": messages,
|
120
|
+
"stream": stream,
|
121
|
+
"temperature": current_llm_config.get("temperature", 0.7),
|
122
|
+
# --- Pass tools and tool_choice ---
|
123
|
+
"tools": tools if tools else None,
|
124
|
+
"tool_choice": tool_choice if tools else None, # Only set tool_choice if tools are provided
|
125
|
+
}
|
126
|
+
if getattr(agent, "response_format", None):
|
127
|
+
create_params["response_format"] = agent.response_format
|
128
|
+
create_params = {k: v for k, v in create_params.items() if v is not None} # Clean None values
|
129
|
+
|
130
|
+
tool_info_log = f", tools_count={len(tools)}" if tools else ", tools=None"
|
131
|
+
logger.debug(f"Chat completion params: model='{active_model}', messages_count={len(messages)}, stream={stream}{tool_info_log}, tool_choice={create_params.get('tool_choice')}")
|
132
|
+
|
133
|
+
try:
|
134
|
+
logger.debug(f"Calling OpenAI API for '{agent.name}' with model='{active_model}'")
|
135
|
+
# Temporary workaround for potential env var conflicts if client doesn't isolate well
|
136
|
+
prev_openai_api_key = os.environ.pop("OPENAI_API_KEY", None)
|
137
|
+
try:
|
138
|
+
completion = await client.chat.completions.create(**create_params)
|
139
|
+
if stream:
|
140
|
+
return completion # Return stream object directly
|
141
|
+
|
142
|
+
# --- Handle Non-Streaming Response ---
|
143
|
+
if completion.choices and len(completion.choices) > 0 and completion.choices[0].message:
|
144
|
+
message_dict = completion.choices[0].message.model_dump(exclude_none=True)
|
145
|
+
log_msg = message_dict.get("content", "No content")[:50] if message_dict.get("content") else "No content"
|
146
|
+
if message_dict.get("tool_calls"): log_msg += f" (+{len(message_dict['tool_calls'])} tool calls)"
|
147
|
+
logger.debug(f"OpenAI completion received for '{agent.name}': {log_msg}...")
|
148
|
+
return message_dict # Return the message dictionary
|
149
|
+
else:
|
150
|
+
logger.warning(f"No valid message in completion for '{agent.name}'")
|
151
|
+
return {"role": "assistant", "content": "No response generated"} # Return dict
|
152
|
+
finally:
|
153
|
+
if prev_openai_api_key is not None:
|
154
|
+
os.environ["OPENAI_API_KEY"] = prev_openai_api_key
|
155
|
+
except OpenAIError as e:
|
156
|
+
logger.error(f"Chat completion failed for '{agent.name}': {e}")
|
157
|
+
raise
|
158
|
+
except Exception as e: # Catch broader errors during API call
|
159
|
+
logger.error(f"Unexpected error during chat completion for '{agent.name}': {e}", exc_info=True)
|
160
|
+
raise # Re-raise
|
161
|
+
|
162
|
+
|
163
|
+
async def get_chat_completion_message(
|
164
|
+
client: AsyncOpenAI,
|
165
|
+
agent: Agent,
|
166
|
+
history: List[Dict[str, Any]],
|
167
|
+
context_variables: dict,
|
168
|
+
current_llm_config: Dict[str, Any],
|
169
|
+
max_context_tokens: int,
|
170
|
+
max_context_messages: int,
|
171
|
+
tools: Optional[List[Dict[str, Any]]] = None, # <-- Added tools
|
172
|
+
tool_choice: Optional[str] = "auto", # <-- Added tool_choice
|
173
|
+
model_override: Optional[str] = None,
|
174
|
+
stream: bool = False,
|
175
|
+
debug: bool = False
|
176
|
+
) -> Union[Dict[str, Any], AsyncGenerator[Any, None]]: # Return dict or stream
|
177
|
+
"""
|
178
|
+
Wrapper to retrieve and validate a chat completion message (returns dict or stream).
|
179
|
+
|
180
|
+
Args:
|
181
|
+
Same as get_chat_completion.
|
182
|
+
|
183
|
+
Returns:
|
184
|
+
Union[Dict[str, Any], AsyncGenerator[Any, None]]: Validated LLM response message as dict or the stream.
|
185
|
+
"""
|
186
|
+
logger.debug(f"Fetching chat completion message for '{agent.name}'")
|
187
|
+
completion_result = await get_chat_completion(
|
188
|
+
client, agent, history, context_variables, current_llm_config,
|
189
|
+
max_context_tokens, max_context_messages,
|
190
|
+
tools=tools, tool_choice=tool_choice, # Pass through
|
191
|
+
model_override=model_override, stream=stream, debug=debug
|
192
|
+
)
|
193
|
+
# If streaming, completion_result is already the generator
|
194
|
+
# If not streaming, it's the message dictionary
|
195
|
+
return completion_result
|