flock-core 0.4.0b48__py3-none-any.whl → 0.4.0b50__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.

Files changed (32) hide show
  1. flock/__init__.py +45 -3
  2. flock/modules/mem0/mem0_module.py +63 -0
  3. flock/modules/mem0graph/__init__.py +1 -0
  4. flock/modules/mem0graph/mem0_graph_module.py +63 -0
  5. flock/webapp/app/api/execution.py +105 -47
  6. flock/webapp/app/chat.py +315 -24
  7. flock/webapp/app/config.py +15 -1
  8. flock/webapp/app/dependencies.py +22 -0
  9. flock/webapp/app/main.py +414 -14
  10. flock/webapp/app/services/flock_service.py +38 -13
  11. flock/webapp/app/services/sharing_models.py +43 -0
  12. flock/webapp/app/services/sharing_store.py +156 -0
  13. flock/webapp/static/css/chat.css +57 -0
  14. flock/webapp/templates/base.html +91 -1
  15. flock/webapp/templates/chat.html +93 -5
  16. flock/webapp/templates/partials/_agent_detail_form.html +3 -3
  17. flock/webapp/templates/partials/_chat_messages.html +1 -1
  18. flock/webapp/templates/partials/_chat_settings_form.html +22 -0
  19. flock/webapp/templates/partials/_execution_form.html +28 -1
  20. flock/webapp/templates/partials/_flock_properties_form.html +2 -2
  21. flock/webapp/templates/partials/_results_display.html +15 -11
  22. flock/webapp/templates/partials/_share_chat_link_snippet.html +11 -0
  23. flock/webapp/templates/partials/_share_link_snippet.html +35 -0
  24. flock/webapp/templates/partials/_structured_data_view.html +2 -2
  25. flock/webapp/templates/shared_run_page.html +143 -0
  26. {flock_core-0.4.0b48.dist-info → flock_core-0.4.0b50.dist-info}/METADATA +4 -2
  27. {flock_core-0.4.0b48.dist-info → flock_core-0.4.0b50.dist-info}/RECORD +31 -24
  28. flock/modules/zep/zep_module.py +0 -187
  29. /flock/modules/{zep → mem0}/__init__.py +0 -0
  30. {flock_core-0.4.0b48.dist-info → flock_core-0.4.0b50.dist-info}/WHEEL +0 -0
  31. {flock_core-0.4.0b48.dist-info → flock_core-0.4.0b50.dist-info}/entry_points.txt +0 -0
  32. {flock_core-0.4.0b48.dist-info → flock_core-0.4.0b50.dist-info}/licenses/LICENSE +0 -0
flock/webapp/app/chat.py CHANGED
@@ -1,16 +1,24 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import ast # Add import ast
3
4
  import json
4
5
  from datetime import datetime
5
6
  from uuid import uuid4
6
7
 
7
- from fastapi import APIRouter, Form, Request
8
+ import markdown2 # Added for Markdown to HTML conversion
9
+ from fastapi import APIRouter, Depends, Form, Request, Response
8
10
  from fastapi.responses import HTMLResponse
9
11
  from pydantic import BaseModel
10
12
 
13
+ from flock.core.flock import Flock
14
+ from flock.core.logging.logging import get_logger
15
+ from flock.webapp.app.dependencies import get_shared_link_store
11
16
  from flock.webapp.app.main import get_base_context_web, templates
17
+ from flock.webapp.app.services.sharing_models import SharedLinkConfig
18
+ from flock.webapp.app.services.sharing_store import SharedLinkStoreInterface
12
19
 
13
20
  router = APIRouter()
21
+ logger = get_logger("webapp.chat")
14
22
 
15
23
  # ---------------------------------------------------------------------------
16
24
  # In-memory session store (cookie-based). Not suitable for production scale.
@@ -20,7 +28,7 @@ _chat_sessions: dict[str, list[dict[str, str]]] = {}
20
28
  COOKIE_NAME = "chat_sid"
21
29
 
22
30
 
23
- def _ensure_session(request: Request):
31
+ def _ensure_session(request: Request) -> tuple[str, list[dict[str, str]]]:
24
32
  """Returns (sid, history_list) tuple and guarantees cookie presence."""
25
33
  sid: str | None = request.cookies.get(COOKIE_NAME)
26
34
  if not sid:
@@ -30,8 +38,23 @@ def _ensure_session(request: Request):
30
38
  return sid, _chat_sessions[sid]
31
39
 
32
40
 
41
+ def _get_history_for_shared_chat(request: Request, share_id: str) -> list[dict[str, str]]:
42
+ """Manages history for a shared chat session, namespaced by share_id and user's session_id."""
43
+ user_sid: str | None = request.cookies.get(COOKIE_NAME)
44
+ if not user_sid: # Should have been set by _ensure_session on page load
45
+ user_sid = uuid4().hex
46
+ # Note: This history will be ephemeral if the cookie isn't set back to the client,
47
+ # but _ensure_session on the shared chat page load should handle cookie setting.
48
+
49
+ # Composite key for shared chat history
50
+ shared_session_key = f"shared_{share_id}_{user_sid}"
51
+ if shared_session_key not in _chat_sessions:
52
+ _chat_sessions[shared_session_key] = []
53
+ return _chat_sessions[shared_session_key]
54
+
55
+
33
56
  # ---------------------------------------------------------------------------
34
- # Chat configuration (per app instance)
57
+ # Chat configuration (per app instance for non-shared, or from SharedLinkConfig for shared)
35
58
  # ---------------------------------------------------------------------------
36
59
 
37
60
 
@@ -42,13 +65,59 @@ class ChatConfig(BaseModel):
42
65
  response_key: str = "response"
43
66
 
44
67
 
45
- # Store a single global chat config on the FastAPI app state
68
+ # Store a single global chat config on the FastAPI app state for non-shared chat
46
69
  def get_chat_config(request: Request) -> ChatConfig:
47
70
  if not hasattr(request.app.state, "chat_config"):
48
71
  request.app.state.chat_config = ChatConfig()
49
72
  return request.app.state.chat_config
50
73
 
51
74
 
75
+ # ---------------------------------------------------------------------------
76
+ # Helper for Shared Chat Context
77
+ # ---------------------------------------------------------------------------
78
+ async def _get_shared_chat_context(
79
+ request: Request,
80
+ share_id: str,
81
+ store: SharedLinkStoreInterface = Depends(get_shared_link_store)
82
+ ) -> tuple[ChatConfig | None, Flock | None, SharedLinkConfig | None]:
83
+ shared_config_db = await store.get_config(share_id)
84
+
85
+ if not shared_config_db or shared_config_db.share_type != "chat":
86
+ logger.warning(f"Shared chat link {share_id} not found or not a chat share type.")
87
+ return None, None, None
88
+
89
+ # Retrieve the pre-loaded Flock instance for this share_id
90
+ # This is loaded by the /chat/shared/{share_id} endpoint in main.py (or will be)
91
+ # For chat.py, we will create a specific /chat/shared/{share_id} endpoint
92
+
93
+ loaded_flock: Flock | None = None
94
+ if hasattr(request.app.state, 'shared_flocks') and share_id in request.app.state.shared_flocks:
95
+ loaded_flock = request.app.state.shared_flocks[share_id]
96
+ else:
97
+ # Attempt to load on-the-fly if not found (e.g., direct API call without page load)
98
+ # This is a fallback and might be slower if the Flock definition is large.
99
+ # The main /chat/shared/{share_id} page route should pre-load this.
100
+ try:
101
+ from flock.core.flock import Flock as ConcreteFlock # Local import
102
+ loaded_flock = ConcreteFlock.from_yaml(shared_config_db.flock_definition)
103
+ if not hasattr(request.app.state, 'shared_flocks'):
104
+ request.app.state.shared_flocks = {}
105
+ request.app.state.shared_flocks[share_id] = loaded_flock # Cache it
106
+ logger.info(f"On-the-fly load of Flock for shared chat {share_id}.")
107
+ except Exception as e_load:
108
+ logger.error(f"Failed to load Flock from definition for shared chat {share_id}: {e_load}", exc_info=True)
109
+ return None, None, shared_config_db
110
+
111
+
112
+ frozen_chat_cfg = ChatConfig(
113
+ agent_name=shared_config_db.agent_name, # agent_name from SharedLinkConfig is the chat agent
114
+ message_key=shared_config_db.chat_message_key or "message",
115
+ history_key=shared_config_db.chat_history_key or "history",
116
+ response_key=shared_config_db.chat_response_key or "response",
117
+ )
118
+ return frozen_chat_cfg, loaded_flock, shared_config_db
119
+
120
+
52
121
  # ---------------------------------------------------------------------------
53
122
  # Routes
54
123
  # ---------------------------------------------------------------------------
@@ -60,7 +129,7 @@ async def chat_page(request: Request):
60
129
  sid, history = _ensure_session(request)
61
130
  cfg = get_chat_config(request)
62
131
  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"})
132
+ context.update({"history": history, "chat_cfg": cfg, "chat_subtitle": f"Agent: {cfg.agent_name}" if cfg.agent_name else "Echo demo", "is_shared_chat": False, "share_id": None})
64
133
  response = templates.TemplateResponse("chat.html", context)
65
134
  # Set cookie if not already present
66
135
  if COOKIE_NAME not in request.cookies:
@@ -86,31 +155,73 @@ async def chat_send(request: Request, message: str = Form(...)):
86
155
  cfg = get_chat_config(request)
87
156
  history.append({"role": "user", "text": message, "timestamp": current_time})
88
157
  start_time = datetime.now()
158
+ is_error = False # Initialize is_error
89
159
 
90
160
  flock_inst = getattr(request.app.state, "flock_instance", None)
91
161
  bot_agent = cfg.agent_name if cfg.agent_name else None
92
162
  bot_text: str
163
+
93
164
  if bot_agent and flock_inst and bot_agent in getattr(flock_inst, "agents", {}):
94
- # Build input according to mapping keys
95
165
  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]
166
+ if cfg.message_key: run_input[cfg.message_key] = message
167
+ if cfg.history_key: run_input[cfg.history_key] = [h["text"] for h in history if h.get("role") == "user" or h.get("role") == "bot"] # Simple text history
101
168
 
102
169
  try:
103
170
  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:
171
+ # Assuming result_dict might be the actual dict, or its string representation is what we need.
172
+ # For now, we work with bot_text derived from it.
107
173
  if cfg.response_key:
108
174
  bot_text = str(result_dict.get(cfg.response_key, result_dict))
109
175
  else:
110
176
  bot_text = str(result_dict)
177
+
178
+ except Exception as e:
179
+ bot_text = f"Error: {e}"
180
+ is_error = True
181
+
182
+ if not is_error:
183
+ original_bot_text = bot_text # Keep a copy
184
+ formatted_as_json = False
185
+
186
+ stripped_text = bot_text.strip()
187
+ if (stripped_text.startswith('{') and stripped_text.endswith('}')) or \
188
+ (stripped_text.startswith('[') and stripped_text.endswith(']')):
189
+ try:
190
+ parsed_obj = json.loads(bot_text)
191
+ if isinstance(parsed_obj, (dict, list)):
192
+ pretty_json = json.dumps(parsed_obj, indent=2).replace('\\n', '\n')
193
+ bot_text = f'''<pre><code class="language-json">{pretty_json}</code></pre>'''
194
+ formatted_as_json = True
195
+ except json.JSONDecodeError:
196
+ try:
197
+ evaluated_obj = ast.literal_eval(bot_text)
198
+ if isinstance(evaluated_obj, (dict, list)):
199
+ pretty_json = json.dumps(evaluated_obj, indent=2).replace('\\n', '\n')
200
+ bot_text = f'''<pre><code class="language-json">{pretty_json}</code></pre>'''
201
+ formatted_as_json = True
202
+ except (ValueError, SyntaxError, TypeError):
203
+ pass # Fall through
204
+ except Exception as e_json_fmt:
205
+ logger.error(f"Error formatting likely JSON: {e_json_fmt}. Original: {original_bot_text[:200]}", exc_info=True)
206
+ bot_text = original_bot_text
207
+
208
+ if not formatted_as_json:
209
+ try:
210
+ bot_text = markdown2.markdown(original_bot_text, extras=["fenced-code-blocks", "tables", "break-on-newline"])
211
+ except Exception:
212
+ logger.error(f"Error during Markdown conversion for bot_text. Original: {original_bot_text[:200]}", exc_info=True)
213
+ bot_text = original_bot_text # Fallback to original text, will be HTML escaped by Jinja if not |safe
214
+
111
215
  else:
112
- # Fallback echo behavior
113
- bot_text = f"Echo: {message}"
216
+ # Fallback echo behavior or agent not found messages
217
+ is_error = True # Treat these as plain text, no special formatting
218
+ if bot_agent and not flock_inst:
219
+ bot_text = f"Agent '{bot_agent}' configured, but no Flock loaded."
220
+ elif bot_agent and flock_inst and bot_agent not in getattr(flock_inst, "agents", {}):
221
+ bot_text = f"Agent '{bot_agent}' configured, but not found in the loaded Flock."
222
+ else: # No agent configured
223
+ bot_text = f"Echo: {message}"
224
+ # If even echo should be markdown, remove is_error=True here and let it pass through. For now, plain.
114
225
 
115
226
  duration_ms = int((datetime.now() - start_time).total_seconds() * 1000)
116
227
  history.append({"role": "bot", "text": bot_text, "timestamp": current_time, "agent": bot_agent or "echo", "duration_ms": duration_ms})
@@ -161,7 +272,7 @@ async def chat_settings_form(request: Request):
161
272
  return templates.TemplateResponse("partials/_chat_settings_form.html", context)
162
273
 
163
274
 
164
- @router.post("/chat/settings", response_class=HTMLResponse, include_in_schema=False)
275
+ @router.post("/chat/settings", include_in_schema=False)
165
276
  async def chat_settings_submit(
166
277
  request: Request,
167
278
  agent_name: str | None = Form(default=None),
@@ -169,19 +280,23 @@ async def chat_settings_submit(
169
280
  history_key: str = Form("history"),
170
281
  response_key: str = Form("response"),
171
282
  ):
172
- """Apply submitted chat config, then re-render the form with a success message."""
283
+ """Handles chat settings submission and triggers a toast notification."""
173
284
  cfg = get_chat_config(request)
174
- cfg.agent_name = agent_name or None
285
+ cfg.agent_name = agent_name
175
286
  cfg.message_key = message_key
176
287
  cfg.history_key = history_key
177
288
  cfg.response_key = response_key
178
289
 
179
- headers = {
180
- "HX-Trigger": json.dumps({"notify": {"type": "success", "message": "Chat settings saved"}}),
181
- "HX-Redirect": "/chat"
290
+ logger.info(f"Chat settings updated: Agent: {cfg.agent_name}, MsgKey: {cfg.message_key}, HistKey: {cfg.history_key}, RespKey: {cfg.response_key}")
291
+
292
+ toast_event = {
293
+ "showGlobalToast": {
294
+ "message": "Chat settings saved successfully!",
295
+ "type": "success"
296
+ }
182
297
  }
183
- # Response body empty; HTMX will redirect
184
- return HTMLResponse("", headers=headers)
298
+ headers = {"HX-Trigger": json.dumps(toast_event)}
299
+ return Response(status_code=204, headers=headers)
185
300
 
186
301
 
187
302
  # --- Stand-alone Chat HTML page access to settings --------------------------
@@ -231,3 +346,179 @@ async def htmx_chat_settings_partial(request: Request):
231
346
 
232
347
  context = {"request": request, "chat_cfg": cfg, "current_flock": flock_inst, "input_fields": input_fields, "output_fields": output_fields}
233
348
  return templates.TemplateResponse("partials/_chat_settings_form.html", context)
349
+
350
+
351
+ # ---------------------------------------------------------------------------
352
+ # Shared Chat Routes
353
+ # ---------------------------------------------------------------------------
354
+
355
+ @router.get("/chat/shared/{share_id}", response_class=HTMLResponse, tags=["Chat Sharing"])
356
+ async def page_shared_chat(
357
+ request: Request,
358
+ share_id: str,
359
+ store: SharedLinkStoreInterface = Depends(get_shared_link_store)
360
+ ):
361
+ """Serves the chat page for a shared chat session."""
362
+ logger.info(f"Accessing shared chat page for share_id: {share_id}")
363
+
364
+ sid, _ = _ensure_session(request) # Ensures user has a session for history tracking
365
+
366
+ shared_config_db = await store.get_config(share_id)
367
+
368
+ if not shared_config_db or shared_config_db.share_type != "chat":
369
+ logger.warning(f"Shared chat link {share_id} not found or not a chat share type.")
370
+ # Consider rendering an error template or redirecting
371
+ error_context = get_base_context_web(request, ui_mode="standalone", error="Shared chat link is invalid or has expired.")
372
+ return templates.TemplateResponse("error_page.html", {**error_context, "error_title": "Invalid Link"}, status_code=404)
373
+
374
+ # Load Flock from definition and cache in app.state.shared_flocks
375
+ loaded_flock: Flock | None = None
376
+ if hasattr(request.app.state, 'shared_flocks') and share_id in request.app.state.shared_flocks:
377
+ loaded_flock = request.app.state.shared_flocks[share_id]
378
+ else:
379
+ try:
380
+ from flock.core.flock import Flock as ConcreteFlock
381
+ loaded_flock = ConcreteFlock.from_yaml(shared_config_db.flock_definition)
382
+ if not hasattr(request.app.state, 'shared_flocks'):
383
+ request.app.state.shared_flocks = {}
384
+ request.app.state.shared_flocks[share_id] = loaded_flock
385
+ logger.info(f"Loaded and cached Flock for shared chat {share_id} in app.state.shared_flocks.")
386
+ except Exception as e_load:
387
+ logger.error(f"Fatal: Could not load Flock from definition for shared chat {share_id}: {e_load}", exc_info=True)
388
+ error_context = get_base_context_web(request, ui_mode="standalone", error=f"Could not load the shared Flock configuration: {e_load!s}")
389
+ return templates.TemplateResponse("error_page.html", {**error_context, "error_title": "Configuration Error"}, status_code=500)
390
+
391
+ frozen_chat_cfg = ChatConfig(
392
+ agent_name=shared_config_db.agent_name,
393
+ message_key=shared_config_db.chat_message_key or "message",
394
+ history_key=shared_config_db.chat_history_key or "history",
395
+ response_key=shared_config_db.chat_response_key or "response",
396
+ )
397
+
398
+ # Get history specific to this user and this shared chat
399
+ history = _get_history_for_shared_chat(request, share_id)
400
+
401
+ context = get_base_context_web(request, ui_mode="standalone")
402
+ context.update({
403
+ "history": history, # User-specific history for this shared chat
404
+ "chat_cfg": frozen_chat_cfg, # The "frozen" config from the share link
405
+ "chat_subtitle": f"Shared Chat - Agent: {frozen_chat_cfg.agent_name}" if frozen_chat_cfg.agent_name else "Shared Echo Chat",
406
+ "is_shared_chat": True,
407
+ "share_id": share_id,
408
+ "flock": loaded_flock # Pass flock for potential display, though backend uses cached one
409
+ })
410
+
411
+ response = templates.TemplateResponse("chat.html", context)
412
+ if COOKIE_NAME not in request.cookies: # Ensure cookie is set if _ensure_session created a new one
413
+ response.set_cookie(COOKIE_NAME, sid, max_age=60 * 60 * 24 * 7)
414
+ return response
415
+
416
+ @router.get("/chat/messages-shared/{share_id}", response_class=HTMLResponse, tags=["Chat Sharing"], include_in_schema=False)
417
+ async def chat_history_partial_shared(request: Request, share_id: str):
418
+ """HTMX endpoint that returns the rendered message list for a shared chat."""
419
+ # _ensure_session called on page load, so cookie should exist for history keying
420
+ history = _get_history_for_shared_chat(request, share_id)
421
+ return templates.TemplateResponse(
422
+ "partials/_chat_messages.html",
423
+ {"request": request, "history": history, "now": datetime.now}
424
+ )
425
+
426
+ @router.post("/chat/send-shared", response_class=HTMLResponse, tags=["Chat Sharing"])
427
+ async def chat_send_shared(
428
+ request: Request,
429
+ share_id: str = Form(...),
430
+ message: str = Form(...),
431
+ # Note: Dependencies need to be declared at the route level for FastAPI to inject them.
432
+ # So, we re-declare get_shared_link_store here or pass it to the helper if FastAPI handles sub-dependencies.
433
+ # For simplicity with current structure, let _get_shared_chat_context handle its own dependency.
434
+ # We can also make _get_shared_chat_context a Depends() if preferred.
435
+ store: SharedLinkStoreInterface = Depends(get_shared_link_store)
436
+ ):
437
+ """Handles message sending for a shared chat session."""
438
+ frozen_chat_cfg, flock_inst, _ = await _get_shared_chat_context(request, share_id, store)
439
+ is_error = False # Initialize is_error
440
+
441
+ if not frozen_chat_cfg or not flock_inst:
442
+ # Error response if config or flock couldn't be loaded
443
+ # This history is ephemeral as it won't be saved if the config is bad
444
+ error_history = [{"role": "bot", "text": "Error: Shared chat configuration is invalid or Flock not found.", "timestamp": datetime.now().strftime('%H:%M')}]
445
+ return templates.TemplateResponse(
446
+ "partials/_chat_messages.html",
447
+ {"request": request, "history": error_history, "now": datetime.now},
448
+ status_code=404
449
+ )
450
+
451
+ history = _get_history_for_shared_chat(request, share_id)
452
+ current_time = datetime.now().strftime('%H:%M')
453
+ history.append({"role": "user", "text": message, "timestamp": current_time})
454
+ start_time = datetime.now()
455
+
456
+ bot_agent = frozen_chat_cfg.agent_name
457
+ bot_text: str
458
+
459
+ if bot_agent and bot_agent in getattr(flock_inst, "agents", {}):
460
+ run_input: dict = {}
461
+ if frozen_chat_cfg.message_key: run_input[frozen_chat_cfg.message_key] = message
462
+ if frozen_chat_cfg.history_key: run_input[frozen_chat_cfg.history_key] = [h["text"] for h in history if h.get("role") == "user" or h.get("role") == "bot"]
463
+
464
+ try:
465
+ result_dict = await flock_inst.run_async(start_agent=bot_agent, input=run_input, box_result=False)
466
+ if frozen_chat_cfg.response_key:
467
+ bot_text = str(result_dict.get(frozen_chat_cfg.response_key, result_dict))
468
+ else:
469
+ bot_text = str(result_dict)
470
+
471
+ except Exception as e:
472
+ bot_text = f"Error running agent {bot_agent} in shared chat: {e}"
473
+ is_error = True
474
+ logger.error(f"Error in /chat/send-shared (agent: {bot_agent}, share: {share_id}): {e}", exc_info=True)
475
+
476
+ if not is_error:
477
+ original_bot_text = bot_text # Keep a copy
478
+ formatted_as_json = False
479
+
480
+ stripped_text = bot_text.strip()
481
+ if (stripped_text.startswith('{') and stripped_text.endswith('}')) or \
482
+ (stripped_text.startswith('[') and stripped_text.endswith(']')):
483
+ try:
484
+ parsed_obj = json.loads(bot_text)
485
+ if isinstance(parsed_obj, (dict, list)):
486
+ pretty_json = json.dumps(parsed_obj, indent=2).replace('\\n', '\n')
487
+ bot_text = f'''<pre><code class="language-json">{pretty_json}</code></pre>'''
488
+ formatted_as_json = True
489
+ except json.JSONDecodeError:
490
+ try:
491
+ evaluated_obj = ast.literal_eval(bot_text)
492
+ if isinstance(evaluated_obj, (dict, list)):
493
+ pretty_json = json.dumps(evaluated_obj, indent=2).replace('\\n', '\n')
494
+ bot_text = f'''<pre><code class="language-json">{pretty_json}</code></pre>'''
495
+ formatted_as_json = True
496
+ except (ValueError, SyntaxError, TypeError):
497
+ pass # Fall through
498
+ except Exception as e_json_fmt:
499
+ logger.error(f"Error formatting likely JSON (shared chat): {e_json_fmt}. Original: {original_bot_text[:200]}", exc_info=True)
500
+ bot_text = original_bot_text
501
+
502
+ if not formatted_as_json:
503
+ try:
504
+ bot_text = markdown2.markdown(original_bot_text, extras=["fenced-code-blocks", "tables", "break-on-newline"])
505
+ except Exception:
506
+ logger.error(f"Error during Markdown conversion for shared chat bot_text. Original: {original_bot_text[:200]}", exc_info=True)
507
+ bot_text = original_bot_text
508
+ else:
509
+ # Fallback if agent misconfigured or not found in the specific shared flock
510
+ is_error = True # Treat these as plain text
511
+ if bot_agent and bot_agent not in getattr(flock_inst, "agents", {}):
512
+ bot_text = f"Agent '{bot_agent}' (shared) not found in its Flock."
513
+ elif not bot_agent:
514
+ bot_text = f"No agent configured for this shared chat. Echoing: {message}"
515
+ else: # Should not happen if frozen_chat_cfg and flock_inst were valid earlier
516
+ bot_text = f"Shared Echo: {message}"
517
+
518
+ duration_ms = int((datetime.now() - start_time).total_seconds() * 1000)
519
+ history.append({"role": "bot", "text": bot_text, "timestamp": current_time, "agent": bot_agent or "shared-echo", "duration_ms": duration_ms})
520
+
521
+ return templates.TemplateResponse(
522
+ "partials/_chat_messages.html",
523
+ {"request": request, "history": history, "now": datetime.now}
524
+ )
@@ -11,6 +11,20 @@ from flock.core.logging.formatters.themes import OutputTheme
11
11
  FLOCK_FILES_DIR = Path(os.getenv("FLOCK_FILES_DIR", "./.flock_ui_projects"))
12
12
  FLOCK_FILES_DIR.mkdir(parents=True, exist_ok=True)
13
13
 
14
+ # --- Shared Links Database Configuration ---
15
+ # Default path is relative to the .flock/ directory in the workspace root if FLOCK_ROOT is not set
16
+ # or if .flock is not a sibling of FLOCK_BASE_DIR.
17
+ # More robustly, place it inside a user-specific or project-specific data directory.
18
+ _default_shared_links_db_parent = Path(os.getenv("FLOCK_ROOT", ".")) / ".flock"
19
+ SHARED_LINKS_DB_PATH = Path(
20
+ os.getenv(
21
+ "SHARED_LINKS_DB_PATH",
22
+ str(_default_shared_links_db_parent / "shared_links.db")
23
+ )
24
+ )
25
+ # Ensure the directory for the DB exists, though the store will also do this.
26
+ SHARED_LINKS_DB_PATH.parent.mkdir(parents=True, exist_ok=True)
27
+
14
28
  # --- Theme Configuration ---
15
29
  # Calculate themes directory relative to this config file's location, assuming structure:
16
30
  # src/flock/webapp/app/config.py
@@ -24,7 +38,7 @@ THEMES_DIR = FLOCK_BASE_DIR / "themes"
24
38
  CURRENT_FLOCK_INSTANCE: "Flock | None" = None
25
39
  CURRENT_FLOCK_FILENAME: str | None = None
26
40
 
27
- DEFAULT_THEME_NAME = OutputTheme.everblush.value # Default if random fails or invalid theme specified
41
+ DEFAULT_THEME_NAME = OutputTheme.ciapre.value # Default if random fails or invalid theme specified
28
42
 
29
43
  def list_available_themes() -> list[str]:
30
44
  """Scans the THEMES_DIR for .toml files and returns their names (without .toml)."""
@@ -8,11 +8,13 @@ from flock.core.api.custom_endpoint import FlockEndpoint
8
8
  if TYPE_CHECKING:
9
9
  from flock.core.api.run_store import RunStore
10
10
  from flock.core.flock import Flock
11
+ from flock.webapp.app.services.sharing_store import SharedLinkStoreInterface
11
12
 
12
13
 
13
14
  # These will be set once when the FastAPI app starts, via set_global_flock_services
14
15
  _flock_instance: Optional["Flock"] = None
15
16
  _run_store_instance: Optional["RunStore"] = None
17
+ _shared_link_store_instance: Optional["SharedLinkStoreInterface"] = None
16
18
 
17
19
  # Global-like variable (scoped to this module) to temporarily store custom endpoints
18
20
  # before the app is fully configured and the lifespan event runs.
@@ -74,6 +76,19 @@ def set_global_flock_services(flock: Optional["Flock"], run_store: "RunStore"):
74
76
  logger.info(f"Global services set in dependencies: Flock='{flock.name if flock else 'None'}', RunStore type='{type(run_store)}'")
75
77
 
76
78
 
79
+ def set_global_shared_link_store(store: "SharedLinkStoreInterface"):
80
+ """Called once at application startup to set the global SharedLinkStore."""
81
+ global _shared_link_store_instance
82
+ from flock.core.logging.logging import get_logger
83
+ logger = get_logger("dependencies")
84
+
85
+ if _shared_link_store_instance is not None:
86
+ logger.warning("Global SharedLinkStore is being re-initialized in dependencies.py.")
87
+
88
+ _shared_link_store_instance = store
89
+ logger.info(f"Global SharedLinkStore set in dependencies: Store type='{type(store)}'")
90
+
91
+
77
92
  def get_flock_instance() -> "Flock":
78
93
  """FastAPI dependency to get the globally available Flock instance."""
79
94
  if _flock_instance is None:
@@ -93,3 +108,10 @@ def get_run_store() -> "RunStore":
93
108
  # Similar to Flock instance, should be initialized at app startup.
94
109
  raise RuntimeError("RunStore instance has not been initialized in the application.")
95
110
  return _run_store_instance
111
+
112
+
113
+ def get_shared_link_store() -> "SharedLinkStoreInterface":
114
+ """FastAPI dependency to get the globally available SharedLinkStore instance."""
115
+ if _shared_link_store_instance is None:
116
+ raise RuntimeError("SharedLinkStore instance has not been initialized in the application.")
117
+ return _shared_link_store_instance