flock-core 0.4.0b43__py3-none-any.whl → 0.4.0b45__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.
Potentially problematic release.
This version of flock-core might be problematic. Click here for more details.
- flock/cli/manage_agents.py +19 -4
- flock/core/api/__init__.py +1 -2
- flock/core/api/endpoints.py +150 -218
- flock/core/api/main.py +134 -653
- flock/core/api/service.py +214 -0
- flock/core/flock.py +192 -134
- flock/core/flock_agent.py +31 -0
- flock/webapp/app/api/agent_management.py +135 -164
- flock/webapp/app/api/execution.py +76 -85
- flock/webapp/app/api/flock_management.py +60 -33
- flock/webapp/app/chat.py +233 -0
- flock/webapp/app/config.py +6 -3
- flock/webapp/app/dependencies.py +95 -0
- flock/webapp/app/main.py +320 -906
- flock/webapp/app/services/flock_service.py +183 -161
- flock/webapp/run.py +176 -100
- flock/webapp/static/css/chat.css +227 -0
- flock/webapp/static/css/components.css +167 -0
- flock/webapp/static/css/header.css +39 -0
- flock/webapp/static/css/layout.css +46 -0
- flock/webapp/static/css/sidebar.css +127 -0
- flock/webapp/templates/base.html +6 -1
- flock/webapp/templates/chat.html +60 -0
- flock/webapp/templates/chat_settings.html +20 -0
- flock/webapp/templates/flock_editor.html +1 -1
- flock/webapp/templates/partials/_agent_detail_form.html +8 -7
- flock/webapp/templates/partials/_agent_list.html +3 -3
- flock/webapp/templates/partials/_agent_manager_view.html +3 -4
- flock/webapp/templates/partials/_chat_container.html +9 -0
- flock/webapp/templates/partials/_chat_messages.html +13 -0
- flock/webapp/templates/partials/_chat_settings_form.html +65 -0
- flock/webapp/templates/partials/_execution_form.html +2 -2
- flock/webapp/templates/partials/_execution_view_container.html +1 -1
- flock/webapp/templates/partials/_flock_properties_form.html +2 -2
- flock/webapp/templates/partials/_registry_viewer_content.html +3 -3
- flock/webapp/templates/partials/_sidebar.html +17 -1
- flock/webapp/templates/registry_viewer.html +3 -3
- {flock_core-0.4.0b43.dist-info → flock_core-0.4.0b45.dist-info}/METADATA +1 -1
- {flock_core-0.4.0b43.dist-info → flock_core-0.4.0b45.dist-info}/RECORD +42 -31
- flock/webapp/static/css/custom.css +0 -612
- flock/webapp/templates/partials/_agent_manager_view_old.html +0 -19
- {flock_core-0.4.0b43.dist-info → flock_core-0.4.0b45.dist-info}/WHEEL +0 -0
- {flock_core-0.4.0b43.dist-info → flock_core-0.4.0b45.dist-info}/entry_points.txt +0 -0
- {flock_core-0.4.0b43.dist-info → flock_core-0.4.0b45.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,28 +1,44 @@
|
|
|
1
|
+
# src/flock/webapp/app/api/flock_management.py
|
|
1
2
|
from pathlib import Path
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
2
4
|
|
|
3
|
-
from fastapi import APIRouter, Form, Request
|
|
5
|
+
from fastapi import APIRouter, Depends, Form, Request # Added Depends
|
|
4
6
|
from fastapi.responses import HTMLResponse
|
|
5
7
|
from fastapi.templating import Jinja2Templates
|
|
6
8
|
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from flock.core.flock import Flock
|
|
11
|
+
|
|
12
|
+
# Import the dependency to get the current Flock instance
|
|
13
|
+
from flock.webapp.app.dependencies import (
|
|
14
|
+
get_flock_instance,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
# Service functions now take app_state
|
|
7
18
|
from flock.webapp.app.services.flock_service import (
|
|
8
|
-
get_current_flock_filename,
|
|
9
|
-
get_current_flock_instance,
|
|
10
19
|
save_current_flock_to_file_service,
|
|
11
20
|
update_flock_properties_service,
|
|
21
|
+
# get_current_flock_filename IS NO LONGER IMPORTED
|
|
22
|
+
# get_current_flock_instance IS NO LONGER IMPORTED
|
|
12
23
|
)
|
|
13
24
|
|
|
14
25
|
router = APIRouter()
|
|
15
|
-
BASE_DIR = Path(__file__).resolve().parent.parent.parent
|
|
26
|
+
BASE_DIR = Path(__file__).resolve().parent.parent.parent
|
|
16
27
|
templates = Jinja2Templates(directory=str(BASE_DIR / "templates"))
|
|
17
28
|
|
|
18
29
|
|
|
19
30
|
@router.get("/htmx/flock-properties-form", response_class=HTMLResponse)
|
|
20
31
|
async def htmx_get_flock_properties_form(
|
|
21
|
-
request: Request,
|
|
32
|
+
request: Request,
|
|
33
|
+
update_message: str = None,
|
|
34
|
+
success: bool = None,
|
|
35
|
+
current_flock: "Flock" = Depends(get_flock_instance) # Expect flock to be loaded for this form
|
|
22
36
|
):
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
37
|
+
# current_flock is now injected by FastAPI
|
|
38
|
+
# Get the filename from app.state, as it's managed there
|
|
39
|
+
current_filename: str | None = getattr(request.app.state, 'flock_filename', None)
|
|
40
|
+
|
|
41
|
+
if not current_flock: # Should be caught by Depends if get_flock_instance raises error
|
|
26
42
|
return HTMLResponse(
|
|
27
43
|
"<div class='error'>Error: No flock loaded. Please load or create one first.</div>"
|
|
28
44
|
)
|
|
@@ -30,8 +46,8 @@ async def htmx_get_flock_properties_form(
|
|
|
30
46
|
"partials/_flock_properties_form.html",
|
|
31
47
|
{
|
|
32
48
|
"request": request,
|
|
33
|
-
"flock":
|
|
34
|
-
"current_filename":
|
|
49
|
+
"flock": current_flock,
|
|
50
|
+
"current_filename": current_filename,
|
|
35
51
|
"update_message": update_message,
|
|
36
52
|
"success": success,
|
|
37
53
|
},
|
|
@@ -44,58 +60,69 @@ async def htmx_update_flock_properties(
|
|
|
44
60
|
flock_name: str = Form(...),
|
|
45
61
|
default_model: str = Form(...),
|
|
46
62
|
description: str = Form(""),
|
|
63
|
+
# current_flock: Flock = Depends(get_flock_instance) # Service will use app_state
|
|
47
64
|
):
|
|
65
|
+
# Pass request.app.state to the service function
|
|
48
66
|
success_update = update_flock_properties_service(
|
|
49
|
-
flock_name, default_model, description
|
|
67
|
+
flock_name, default_model, description, request.app.state
|
|
50
68
|
)
|
|
51
|
-
|
|
52
|
-
#
|
|
69
|
+
|
|
70
|
+
# Retrieve updated flock and filename from app.state for rendering the form
|
|
71
|
+
updated_flock: Flock | None = getattr(request.app.state, 'flock_instance', None)
|
|
72
|
+
updated_filename: str | None = getattr(request.app.state, 'flock_filename', None)
|
|
73
|
+
|
|
53
74
|
return templates.TemplateResponse(
|
|
54
75
|
"partials/_flock_properties_form.html",
|
|
55
76
|
{
|
|
56
77
|
"request": request,
|
|
57
|
-
"flock":
|
|
58
|
-
"current_filename":
|
|
78
|
+
"flock": updated_flock,
|
|
79
|
+
"current_filename": updated_filename,
|
|
59
80
|
"update_message": "Flock properties updated!"
|
|
60
81
|
if success_update
|
|
61
|
-
else "Failed to update properties.",
|
|
82
|
+
else "Failed to update properties. Check logs.",
|
|
62
83
|
"success": success_update,
|
|
63
84
|
},
|
|
64
85
|
)
|
|
65
86
|
|
|
66
87
|
|
|
67
88
|
@router.post("/htmx/save-flock", response_class=HTMLResponse)
|
|
68
|
-
async def htmx_save_flock(
|
|
69
|
-
|
|
70
|
-
|
|
89
|
+
async def htmx_save_flock(
|
|
90
|
+
request: Request,
|
|
91
|
+
save_filename: str = Form(...),
|
|
92
|
+
# current_flock: Flock = Depends(get_flock_instance) # Service will use app_state
|
|
93
|
+
):
|
|
94
|
+
current_flock_from_state: Flock | None = getattr(request.app.state, 'flock_instance', None)
|
|
95
|
+
current_filename_from_state: str | None = getattr(request.app.state, 'flock_filename', None)
|
|
96
|
+
|
|
97
|
+
if not save_filename.strip():
|
|
71
98
|
return templates.TemplateResponse(
|
|
72
99
|
"partials/_flock_properties_form.html",
|
|
73
100
|
{
|
|
74
101
|
"request": request,
|
|
75
|
-
"flock":
|
|
76
|
-
"current_filename":
|
|
102
|
+
"flock": current_flock_from_state,
|
|
103
|
+
"current_filename": current_filename_from_state,
|
|
77
104
|
"save_message": "Filename cannot be empty.",
|
|
78
105
|
"success": False,
|
|
79
106
|
},
|
|
80
107
|
)
|
|
81
108
|
|
|
82
|
-
if not (
|
|
83
|
-
save_filename
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
)
|
|
87
|
-
|
|
109
|
+
if not (save_filename.endswith(".yaml") or save_filename.endswith(".yml") or save_filename.endswith(".flock")):
|
|
110
|
+
save_filename += ".flock.yaml"
|
|
111
|
+
|
|
112
|
+
# Pass request.app.state to the service function
|
|
113
|
+
success, message = save_current_flock_to_file_service(save_filename, request.app.state)
|
|
114
|
+
|
|
115
|
+
# Retrieve potentially updated flock and filename from app.state
|
|
116
|
+
saved_flock: Flock | None = getattr(request.app.state, 'flock_instance', None)
|
|
117
|
+
saved_filename: str | None = getattr(request.app.state, 'flock_filename', None)
|
|
118
|
+
|
|
88
119
|
|
|
89
|
-
success, message = save_current_flock_to_file_service(save_filename)
|
|
90
|
-
flock = get_current_flock_instance()
|
|
91
120
|
return templates.TemplateResponse(
|
|
92
121
|
"partials/_flock_properties_form.html",
|
|
93
122
|
{
|
|
94
123
|
"request": request,
|
|
95
|
-
"flock":
|
|
96
|
-
"current_filename":
|
|
97
|
-
if success
|
|
98
|
-
else get_current_flock_filename(), # Update filename if save was successful
|
|
124
|
+
"flock": saved_flock, # Use the instance from app_state
|
|
125
|
+
"current_filename": saved_filename, # Use the filename from app_state (updated on successful save)
|
|
99
126
|
"save_message": message,
|
|
100
127
|
"success": success,
|
|
101
128
|
},
|
flock/webapp/app/chat.py
ADDED
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from uuid import uuid4
|
|
6
|
+
|
|
7
|
+
from fastapi import APIRouter, Form, Request
|
|
8
|
+
from fastapi.responses import HTMLResponse
|
|
9
|
+
from pydantic import BaseModel
|
|
10
|
+
|
|
11
|
+
from flock.webapp.app.main import get_base_context_web, templates
|
|
12
|
+
|
|
13
|
+
router = APIRouter()
|
|
14
|
+
|
|
15
|
+
# ---------------------------------------------------------------------------
|
|
16
|
+
# In-memory session store (cookie-based). Not suitable for production scale.
|
|
17
|
+
# ---------------------------------------------------------------------------
|
|
18
|
+
_chat_sessions: dict[str, list[dict[str, str]]] = {}
|
|
19
|
+
|
|
20
|
+
COOKIE_NAME = "chat_sid"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _ensure_session(request: Request):
|
|
24
|
+
"""Returns (sid, history_list) tuple and guarantees cookie presence."""
|
|
25
|
+
sid: str | None = request.cookies.get(COOKIE_NAME)
|
|
26
|
+
if not sid:
|
|
27
|
+
sid = uuid4().hex
|
|
28
|
+
if sid not in _chat_sessions:
|
|
29
|
+
_chat_sessions[sid] = []
|
|
30
|
+
return sid, _chat_sessions[sid]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
# ---------------------------------------------------------------------------
|
|
34
|
+
# Chat configuration (per app instance)
|
|
35
|
+
# ---------------------------------------------------------------------------
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class ChatConfig(BaseModel):
|
|
39
|
+
agent_name: str | None = None # Name of the Flock agent to chat with
|
|
40
|
+
message_key: str = "message"
|
|
41
|
+
history_key: str = "history"
|
|
42
|
+
response_key: str = "response"
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
# Store a single global chat config on the FastAPI app state
|
|
46
|
+
def get_chat_config(request: Request) -> ChatConfig:
|
|
47
|
+
if not hasattr(request.app.state, "chat_config"):
|
|
48
|
+
request.app.state.chat_config = ChatConfig()
|
|
49
|
+
return request.app.state.chat_config
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
# ---------------------------------------------------------------------------
|
|
53
|
+
# Routes
|
|
54
|
+
# ---------------------------------------------------------------------------
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@router.get("/chat", response_class=HTMLResponse, tags=["Chat"])
|
|
58
|
+
async def chat_page(request: Request):
|
|
59
|
+
"""Full-page chat UI (works even when the main UI is disabled)."""
|
|
60
|
+
sid, history = _ensure_session(request)
|
|
61
|
+
cfg = get_chat_config(request)
|
|
62
|
+
context = get_base_context_web(request, ui_mode="standalone")
|
|
63
|
+
context.update({"history": history, "chat_cfg": cfg, "chat_subtitle": f"Agent: {cfg.agent_name}" if cfg.agent_name else "Echo demo"})
|
|
64
|
+
response = templates.TemplateResponse("chat.html", context)
|
|
65
|
+
# Set cookie if not already present
|
|
66
|
+
if COOKIE_NAME not in request.cookies:
|
|
67
|
+
response.set_cookie(COOKIE_NAME, sid, max_age=60 * 60 * 24 * 7)
|
|
68
|
+
return response
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@router.get("/chat/messages", response_class=HTMLResponse, tags=["Chat"], include_in_schema=False)
|
|
72
|
+
async def chat_history_partial(request: Request):
|
|
73
|
+
"""HTMX endpoint that returns the rendered message list."""
|
|
74
|
+
_, history = _ensure_session(request)
|
|
75
|
+
return templates.TemplateResponse(
|
|
76
|
+
"partials/_chat_messages.html",
|
|
77
|
+
{"request": request, "history": history, "now": datetime.now}
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@router.post("/chat/send", response_class=HTMLResponse, tags=["Chat"])
|
|
82
|
+
async def chat_send(request: Request, message: str = Form(...)):
|
|
83
|
+
"""Echo-back mock implementation. Adds user msg + bot reply to history."""
|
|
84
|
+
_, history = _ensure_session(request)
|
|
85
|
+
current_time = datetime.now().strftime('%H:%M')
|
|
86
|
+
cfg = get_chat_config(request)
|
|
87
|
+
history.append({"role": "user", "text": message, "timestamp": current_time})
|
|
88
|
+
start_time = datetime.now()
|
|
89
|
+
|
|
90
|
+
flock_inst = getattr(request.app.state, "flock_instance", None)
|
|
91
|
+
bot_agent = cfg.agent_name if cfg.agent_name else None
|
|
92
|
+
bot_text: str
|
|
93
|
+
if bot_agent and flock_inst and bot_agent in getattr(flock_inst, "agents", {}):
|
|
94
|
+
# Build input according to mapping keys
|
|
95
|
+
run_input: dict = {}
|
|
96
|
+
if cfg.message_key:
|
|
97
|
+
run_input[cfg.message_key] = message
|
|
98
|
+
if cfg.history_key:
|
|
99
|
+
# Provide history without timestamps to keep things small
|
|
100
|
+
run_input[cfg.history_key] = [h["text"] for h in history]
|
|
101
|
+
|
|
102
|
+
try:
|
|
103
|
+
result_dict = await flock_inst.run_async(start_agent=bot_agent, input=run_input, box_result=False)
|
|
104
|
+
except Exception as e:
|
|
105
|
+
bot_text = f"Error: {e}"
|
|
106
|
+
else:
|
|
107
|
+
if cfg.response_key:
|
|
108
|
+
bot_text = str(result_dict.get(cfg.response_key, result_dict))
|
|
109
|
+
else:
|
|
110
|
+
bot_text = str(result_dict)
|
|
111
|
+
else:
|
|
112
|
+
# Fallback echo behavior
|
|
113
|
+
bot_text = f"Echo: {message}"
|
|
114
|
+
|
|
115
|
+
duration_ms = int((datetime.now() - start_time).total_seconds() * 1000)
|
|
116
|
+
history.append({"role": "bot", "text": bot_text, "timestamp": current_time, "agent": bot_agent or "echo", "duration_ms": duration_ms})
|
|
117
|
+
# Return updated history partial
|
|
118
|
+
return templates.TemplateResponse(
|
|
119
|
+
"partials/_chat_messages.html",
|
|
120
|
+
{"request": request, "history": history, "now": datetime.now}
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
@router.get("/ui/htmx/chat-view", response_class=HTMLResponse, tags=["Chat"], include_in_schema=False)
|
|
125
|
+
async def chat_container_partial(request: Request):
|
|
126
|
+
_ensure_session(request)
|
|
127
|
+
return templates.TemplateResponse("partials/_chat_container.html", {"request": request})
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
# ---------------------------------------------------------------------------
|
|
131
|
+
# Chat settings management
|
|
132
|
+
# ---------------------------------------------------------------------------
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
@router.get("/ui/htmx/chat-settings-form", response_class=HTMLResponse, include_in_schema=False)
|
|
136
|
+
async def chat_settings_form(request: Request):
|
|
137
|
+
"""Returns the form for configuring chat behaviour (HTMX partial)."""
|
|
138
|
+
cfg = get_chat_config(request)
|
|
139
|
+
flock_inst = getattr(request.app.state, "flock_instance", None)
|
|
140
|
+
input_fields, output_fields = [], []
|
|
141
|
+
if cfg.agent_name and flock_inst and cfg.agent_name in flock_inst.agents:
|
|
142
|
+
agent_obj = flock_inst.agents[cfg.agent_name]
|
|
143
|
+
# Expect signatures like "field: type | desc, ..." or "field: type" etc.
|
|
144
|
+
def _extract(sig: str):
|
|
145
|
+
fields = []
|
|
146
|
+
for seg in sig.split(','):
|
|
147
|
+
parts = seg.strip().split(':')
|
|
148
|
+
if parts:
|
|
149
|
+
fields.append(parts[0].strip())
|
|
150
|
+
return [f for f in fields if f]
|
|
151
|
+
input_fields = _extract(agent_obj.input) if getattr(agent_obj, 'input', '') else []
|
|
152
|
+
output_fields = _extract(agent_obj.output) if getattr(agent_obj, 'output', '') else []
|
|
153
|
+
|
|
154
|
+
context = get_base_context_web(request)
|
|
155
|
+
context.update({
|
|
156
|
+
"chat_cfg": cfg,
|
|
157
|
+
"current_flock": flock_inst,
|
|
158
|
+
"input_fields": input_fields,
|
|
159
|
+
"output_fields": output_fields,
|
|
160
|
+
})
|
|
161
|
+
return templates.TemplateResponse("partials/_chat_settings_form.html", context)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
@router.post("/chat/settings", response_class=HTMLResponse, include_in_schema=False)
|
|
165
|
+
async def chat_settings_submit(
|
|
166
|
+
request: Request,
|
|
167
|
+
agent_name: str | None = Form(default=None),
|
|
168
|
+
message_key: str = Form("message"),
|
|
169
|
+
history_key: str = Form("history"),
|
|
170
|
+
response_key: str = Form("response"),
|
|
171
|
+
):
|
|
172
|
+
"""Apply submitted chat config, then re-render the form with a success message."""
|
|
173
|
+
cfg = get_chat_config(request)
|
|
174
|
+
cfg.agent_name = agent_name or None
|
|
175
|
+
cfg.message_key = message_key
|
|
176
|
+
cfg.history_key = history_key
|
|
177
|
+
cfg.response_key = response_key
|
|
178
|
+
|
|
179
|
+
headers = {
|
|
180
|
+
"HX-Trigger": json.dumps({"notify": {"type": "success", "message": "Chat settings saved"}}),
|
|
181
|
+
"HX-Redirect": "/chat"
|
|
182
|
+
}
|
|
183
|
+
# Response body empty; HTMX will redirect
|
|
184
|
+
return HTMLResponse("", headers=headers)
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
# --- Stand-alone Chat HTML page access to settings --------------------------
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
@router.get("/chat/settings-standalone", response_class=HTMLResponse, tags=["Chat"], include_in_schema=False)
|
|
191
|
+
async def chat_settings_standalone(request: Request):
|
|
192
|
+
"""Standalone page to render chat settings (used by full-page chat HTML)."""
|
|
193
|
+
cfg = get_chat_config(request)
|
|
194
|
+
context = get_base_context_web(request, ui_mode="standalone")
|
|
195
|
+
context.update({
|
|
196
|
+
"chat_cfg": cfg,
|
|
197
|
+
"current_flock": getattr(request.app.state, "flock_instance", None),
|
|
198
|
+
})
|
|
199
|
+
return templates.TemplateResponse("chat_settings.html", context)
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
# ---------------------------------------------------------------------------
|
|
203
|
+
# Stand-alone HTMX partials (chat view & settings) for in-page swapping
|
|
204
|
+
# ---------------------------------------------------------------------------
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
@router.get("/chat/htmx/chat-view", response_class=HTMLResponse, include_in_schema=False)
|
|
208
|
+
async def htmx_chat_view(request: Request):
|
|
209
|
+
"""Return chat container partial for standalone page reload via HTMX."""
|
|
210
|
+
_ensure_session(request)
|
|
211
|
+
return templates.TemplateResponse("partials/_chat_container.html", {"request": request})
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
@router.get("/chat/htmx/settings-form", response_class=HTMLResponse, include_in_schema=False)
|
|
215
|
+
async def htmx_chat_settings_partial(request: Request):
|
|
216
|
+
cfg = get_chat_config(request)
|
|
217
|
+
# Allow temporarily selecting agent via query param without saving
|
|
218
|
+
agent_override = request.query_params.get("agent_name")
|
|
219
|
+
if agent_override is not None:
|
|
220
|
+
cfg = cfg.copy()
|
|
221
|
+
cfg.agent_name = agent_override or None
|
|
222
|
+
|
|
223
|
+
flock_inst = getattr(request.app.state, "flock_instance", None)
|
|
224
|
+
input_fields, output_fields = [], []
|
|
225
|
+
if cfg.agent_name and flock_inst and cfg.agent_name in flock_inst.agents:
|
|
226
|
+
agent_obj = flock_inst.agents[cfg.agent_name]
|
|
227
|
+
def _extract(sig: str):
|
|
228
|
+
return [seg.strip().split(':')[0].strip() for seg in sig.split(',') if seg.strip()]
|
|
229
|
+
input_fields = _extract(agent_obj.input) if getattr(agent_obj, 'input', '') else []
|
|
230
|
+
output_fields = _extract(agent_obj.output) if getattr(agent_obj, 'output', '') else []
|
|
231
|
+
|
|
232
|
+
context = {"request": request, "chat_cfg": cfg, "current_flock": flock_inst, "input_fields": input_fields, "output_fields": output_fields}
|
|
233
|
+
return templates.TemplateResponse("partials/_chat_settings_form.html", context)
|
flock/webapp/app/config.py
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import os
|
|
2
2
|
import random # Added for random theme selection
|
|
3
3
|
from pathlib import Path
|
|
4
|
+
from typing import TYPE_CHECKING
|
|
5
|
+
|
|
6
|
+
if TYPE_CHECKING:
|
|
7
|
+
from flock.core.flock import Flock
|
|
4
8
|
|
|
5
|
-
from flock.core import Flock # Add type hint
|
|
6
9
|
from flock.core.logging.formatters.themes import OutputTheme
|
|
7
10
|
|
|
8
11
|
FLOCK_FILES_DIR = Path(os.getenv("FLOCK_FILES_DIR", "./.flock_ui_projects"))
|
|
@@ -18,10 +21,10 @@ FLOCK_BASE_DIR = FLOCK_WEBAPP_DIR.parent # src/flock/
|
|
|
18
21
|
THEMES_DIR = FLOCK_BASE_DIR / "themes"
|
|
19
22
|
|
|
20
23
|
# Global state for MVP - NOT SUITABLE FOR PRODUCTION/MULTI-USER
|
|
21
|
-
CURRENT_FLOCK_INSTANCE: Flock | None = None
|
|
24
|
+
CURRENT_FLOCK_INSTANCE: "Flock | None" = None
|
|
22
25
|
CURRENT_FLOCK_FILENAME: str | None = None
|
|
23
26
|
|
|
24
|
-
DEFAULT_THEME_NAME = OutputTheme.
|
|
27
|
+
DEFAULT_THEME_NAME = OutputTheme.everblush.value # Default if random fails or invalid theme specified
|
|
25
28
|
|
|
26
29
|
def list_available_themes() -> list[str]:
|
|
27
30
|
"""Scans the THEMES_DIR for .toml files and returns their names (without .toml)."""
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# src/flock/webapp/app/dependencies.py
|
|
2
|
+
from collections.abc import Sequence
|
|
3
|
+
from typing import TYPE_CHECKING, Optional
|
|
4
|
+
|
|
5
|
+
# Import FlockEndpoint for type hinting
|
|
6
|
+
from flock.core.api.custom_endpoint import FlockEndpoint
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from flock.core.api.run_store import RunStore
|
|
10
|
+
from flock.core.flock import Flock
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
# These will be set once when the FastAPI app starts, via set_global_flock_services
|
|
14
|
+
_flock_instance: Optional["Flock"] = None
|
|
15
|
+
_run_store_instance: Optional["RunStore"] = None
|
|
16
|
+
|
|
17
|
+
# Global-like variable (scoped to this module) to temporarily store custom endpoints
|
|
18
|
+
# before the app is fully configured and the lifespan event runs.
|
|
19
|
+
_pending_custom_endpoints: list[FlockEndpoint] = []
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def add_pending_custom_endpoints(endpoints: Sequence[FlockEndpoint] | None):
|
|
23
|
+
"""Temporarily stores custom endpoints. Called by `start_unified_server`
|
|
24
|
+
before the FastAPI app's lifespan event can process them.
|
|
25
|
+
"""
|
|
26
|
+
global _pending_custom_endpoints
|
|
27
|
+
if endpoints:
|
|
28
|
+
# Ensure we are adding FlockEndpoint instances
|
|
29
|
+
for ep in endpoints:
|
|
30
|
+
if not isinstance(ep, FlockEndpoint):
|
|
31
|
+
# If it's a dict, try to convert it, assuming it was the old format
|
|
32
|
+
if isinstance(ep, dict) and "path" in ep and "callback" in ep:
|
|
33
|
+
try:
|
|
34
|
+
# Attempt to create FlockEndpoint from dict - assumes correct keys
|
|
35
|
+
# This is a basic conversion; more robust parsing might be needed
|
|
36
|
+
# if the dict structure is very different.
|
|
37
|
+
_pending_custom_endpoints.append(FlockEndpoint(**ep)) # type: ignore
|
|
38
|
+
except Exception as e:
|
|
39
|
+
print(f"Warning: Could not convert custom endpoint dict to FlockEndpoint: {ep}. Error: {e}")
|
|
40
|
+
else:
|
|
41
|
+
print(f"Warning: Custom endpoint is not a FlockEndpoint instance and cannot be automatically converted: {type(ep)}")
|
|
42
|
+
else:
|
|
43
|
+
_pending_custom_endpoints.append(ep)
|
|
44
|
+
print(f"Dependencies: Added {len(endpoints)} pending custom endpoints.")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def get_pending_custom_endpoints_and_clear() -> list[FlockEndpoint]:
|
|
48
|
+
"""Retrieves and clears pending custom endpoints.
|
|
49
|
+
Called by the FastAPI app's lifespan event manager in `webapp/app/main.py`.
|
|
50
|
+
"""
|
|
51
|
+
global _pending_custom_endpoints
|
|
52
|
+
endpoints_to_add = _pending_custom_endpoints[:] # Create a copy
|
|
53
|
+
_pending_custom_endpoints = [] # Clear the pending list
|
|
54
|
+
if endpoints_to_add:
|
|
55
|
+
print(f"Dependencies: Retrieved {len(endpoints_to_add)} pending custom endpoints for app registration.")
|
|
56
|
+
return endpoints_to_add
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def set_global_flock_services(flock: Optional["Flock"], run_store: "RunStore"):
|
|
60
|
+
"""Called once at application startup to set the global Flock and RunStore.
|
|
61
|
+
This is typically invoked by the server startup script (e.g., in webapp/run.py).
|
|
62
|
+
"""
|
|
63
|
+
global _flock_instance, _run_store_instance
|
|
64
|
+
from flock.core.logging.logging import (
|
|
65
|
+
get_logger, # Local import to avoid circularity at module level
|
|
66
|
+
)
|
|
67
|
+
logger = get_logger("dependencies")
|
|
68
|
+
|
|
69
|
+
if _flock_instance is not None or _run_store_instance is not None:
|
|
70
|
+
logger.warning("Global Flock services are being re-initialized in dependencies.py.")
|
|
71
|
+
|
|
72
|
+
_flock_instance = flock
|
|
73
|
+
_run_store_instance = run_store
|
|
74
|
+
logger.info(f"Global services set in dependencies: Flock='{flock.name if flock else 'None'}', RunStore type='{type(run_store)}'")
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def get_flock_instance() -> "Flock":
|
|
78
|
+
"""FastAPI dependency to get the globally available Flock instance."""
|
|
79
|
+
if _flock_instance is None:
|
|
80
|
+
# This can happen if accessed before `set_global_flock_services` or if it was cleared.
|
|
81
|
+
# The application should handle this gracefully, e.g. by redirecting to a setup page.
|
|
82
|
+
raise RuntimeError("Flock instance has not been initialized or is not currently available for DI.")
|
|
83
|
+
return _flock_instance
|
|
84
|
+
|
|
85
|
+
def get_optional_flock_instance() -> Optional["Flock"]:
|
|
86
|
+
"""FastAPI dependency to optionally get the Flock instance. Returns None if not set."""
|
|
87
|
+
return _flock_instance
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def get_run_store() -> "RunStore":
|
|
91
|
+
"""FastAPI dependency to get the globally available RunStore instance."""
|
|
92
|
+
if _run_store_instance is None:
|
|
93
|
+
# Similar to Flock instance, should be initialized at app startup.
|
|
94
|
+
raise RuntimeError("RunStore instance has not been initialized in the application.")
|
|
95
|
+
return _run_store_instance
|