flock-core 0.4.0b46__py3-none-any.whl → 0.4.0b49__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 (50) hide show
  1. flock/__init__.py +45 -3
  2. flock/core/flock.py +105 -61
  3. flock/core/flock_registry.py +45 -38
  4. flock/core/util/spliter.py +4 -0
  5. flock/evaluators/__init__.py +1 -0
  6. flock/evaluators/declarative/__init__.py +1 -0
  7. flock/modules/__init__.py +1 -0
  8. flock/modules/assertion/__init__.py +1 -0
  9. flock/modules/callback/__init__.py +1 -0
  10. flock/modules/mem0/__init__.py +1 -0
  11. flock/modules/mem0/mem0_module.py +63 -0
  12. flock/modules/mem0graph/__init__.py +1 -0
  13. flock/modules/mem0graph/mem0_graph_module.py +63 -0
  14. flock/modules/memory/__init__.py +1 -0
  15. flock/modules/output/__init__.py +1 -0
  16. flock/modules/performance/__init__.py +1 -0
  17. flock/tools/__init__.py +188 -0
  18. flock/{core/tools → tools}/azure_tools.py +284 -0
  19. flock/tools/code_tools.py +56 -0
  20. flock/tools/file_tools.py +140 -0
  21. flock/{core/tools/dev_tools/github.py → tools/github_tools.py} +3 -3
  22. flock/{core/tools → tools}/markdown_tools.py +14 -4
  23. flock/tools/system_tools.py +9 -0
  24. flock/{core/tools/llm_tools.py → tools/text_tools.py} +47 -25
  25. flock/tools/web_tools.py +90 -0
  26. flock/{core/tools → tools}/zendesk_tools.py +6 -6
  27. flock/webapp/app/api/execution.py +130 -30
  28. flock/webapp/app/chat.py +303 -16
  29. flock/webapp/app/config.py +15 -1
  30. flock/webapp/app/dependencies.py +22 -0
  31. flock/webapp/app/main.py +509 -18
  32. flock/webapp/app/services/flock_service.py +38 -13
  33. flock/webapp/app/services/sharing_models.py +43 -0
  34. flock/webapp/app/services/sharing_store.py +156 -0
  35. flock/webapp/static/css/chat.css +57 -0
  36. flock/webapp/templates/chat.html +29 -4
  37. flock/webapp/templates/partials/_chat_messages.html +1 -1
  38. flock/webapp/templates/partials/_chat_settings_form.html +22 -0
  39. flock/webapp/templates/partials/_execution_form.html +28 -1
  40. flock/webapp/templates/partials/_share_chat_link_snippet.html +11 -0
  41. flock/webapp/templates/partials/_share_link_snippet.html +35 -0
  42. flock/webapp/templates/shared_run_page.html +116 -0
  43. flock/workflow/activities.py +1 -0
  44. {flock_core-0.4.0b46.dist-info → flock_core-0.4.0b49.dist-info}/METADATA +27 -14
  45. {flock_core-0.4.0b46.dist-info → flock_core-0.4.0b49.dist-info}/RECORD +48 -28
  46. flock/core/tools/basic_tools.py +0 -317
  47. flock/modules/zep/zep_module.py +0 -187
  48. {flock_core-0.4.0b46.dist-info → flock_core-0.4.0b49.dist-info}/WHEEL +0 -0
  49. {flock_core-0.4.0b46.dist-info → flock_core-0.4.0b49.dist-info}/entry_points.txt +0 -0
  50. {flock_core-0.4.0b46.dist-info → flock_core-0.4.0b49.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
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})
@@ -231,3 +342,179 @@ async def htmx_chat_settings_partial(request: Request):
231
342
 
232
343
  context = {"request": request, "chat_cfg": cfg, "current_flock": flock_inst, "input_fields": input_fields, "output_fields": output_fields}
233
344
  return templates.TemplateResponse("partials/_chat_settings_form.html", context)
345
+
346
+
347
+ # ---------------------------------------------------------------------------
348
+ # Shared Chat Routes
349
+ # ---------------------------------------------------------------------------
350
+
351
+ @router.get("/chat/shared/{share_id}", response_class=HTMLResponse, tags=["Chat Sharing"])
352
+ async def page_shared_chat(
353
+ request: Request,
354
+ share_id: str,
355
+ store: SharedLinkStoreInterface = Depends(get_shared_link_store)
356
+ ):
357
+ """Serves the chat page for a shared chat session."""
358
+ logger.info(f"Accessing shared chat page for share_id: {share_id}")
359
+
360
+ sid, _ = _ensure_session(request) # Ensures user has a session for history tracking
361
+
362
+ shared_config_db = await store.get_config(share_id)
363
+
364
+ if not shared_config_db or shared_config_db.share_type != "chat":
365
+ logger.warning(f"Shared chat link {share_id} not found or not a chat share type.")
366
+ # Consider rendering an error template or redirecting
367
+ error_context = get_base_context_web(request, ui_mode="standalone", error="Shared chat link is invalid or has expired.")
368
+ return templates.TemplateResponse("error_page.html", {**error_context, "error_title": "Invalid Link"}, status_code=404)
369
+
370
+ # Load Flock from definition and cache in app.state.shared_flocks
371
+ loaded_flock: Flock | None = None
372
+ if hasattr(request.app.state, 'shared_flocks') and share_id in request.app.state.shared_flocks:
373
+ loaded_flock = request.app.state.shared_flocks[share_id]
374
+ else:
375
+ try:
376
+ from flock.core.flock import Flock as ConcreteFlock
377
+ loaded_flock = ConcreteFlock.from_yaml(shared_config_db.flock_definition)
378
+ if not hasattr(request.app.state, 'shared_flocks'):
379
+ request.app.state.shared_flocks = {}
380
+ request.app.state.shared_flocks[share_id] = loaded_flock
381
+ logger.info(f"Loaded and cached Flock for shared chat {share_id} in app.state.shared_flocks.")
382
+ except Exception as e_load:
383
+ logger.error(f"Fatal: Could not load Flock from definition for shared chat {share_id}: {e_load}", exc_info=True)
384
+ error_context = get_base_context_web(request, ui_mode="standalone", error=f"Could not load the shared Flock configuration: {e_load!s}")
385
+ return templates.TemplateResponse("error_page.html", {**error_context, "error_title": "Configuration Error"}, status_code=500)
386
+
387
+ frozen_chat_cfg = ChatConfig(
388
+ agent_name=shared_config_db.agent_name,
389
+ message_key=shared_config_db.chat_message_key or "message",
390
+ history_key=shared_config_db.chat_history_key or "history",
391
+ response_key=shared_config_db.chat_response_key or "response",
392
+ )
393
+
394
+ # Get history specific to this user and this shared chat
395
+ history = _get_history_for_shared_chat(request, share_id)
396
+
397
+ context = get_base_context_web(request, ui_mode="standalone")
398
+ context.update({
399
+ "history": history, # User-specific history for this shared chat
400
+ "chat_cfg": frozen_chat_cfg, # The "frozen" config from the share link
401
+ "chat_subtitle": f"Shared Chat - Agent: {frozen_chat_cfg.agent_name}" if frozen_chat_cfg.agent_name else "Shared Echo Chat",
402
+ "is_shared_chat": True,
403
+ "share_id": share_id,
404
+ "flock": loaded_flock # Pass flock for potential display, though backend uses cached one
405
+ })
406
+
407
+ response = templates.TemplateResponse("chat.html", context)
408
+ if COOKIE_NAME not in request.cookies: # Ensure cookie is set if _ensure_session created a new one
409
+ response.set_cookie(COOKIE_NAME, sid, max_age=60 * 60 * 24 * 7)
410
+ return response
411
+
412
+ @router.get("/chat/messages-shared/{share_id}", response_class=HTMLResponse, tags=["Chat Sharing"], include_in_schema=False)
413
+ async def chat_history_partial_shared(request: Request, share_id: str):
414
+ """HTMX endpoint that returns the rendered message list for a shared chat."""
415
+ # _ensure_session called on page load, so cookie should exist for history keying
416
+ history = _get_history_for_shared_chat(request, share_id)
417
+ return templates.TemplateResponse(
418
+ "partials/_chat_messages.html",
419
+ {"request": request, "history": history, "now": datetime.now}
420
+ )
421
+
422
+ @router.post("/chat/send-shared", response_class=HTMLResponse, tags=["Chat Sharing"])
423
+ async def chat_send_shared(
424
+ request: Request,
425
+ share_id: str = Form(...),
426
+ message: str = Form(...),
427
+ # Note: Dependencies need to be declared at the route level for FastAPI to inject them.
428
+ # So, we re-declare get_shared_link_store here or pass it to the helper if FastAPI handles sub-dependencies.
429
+ # For simplicity with current structure, let _get_shared_chat_context handle its own dependency.
430
+ # We can also make _get_shared_chat_context a Depends() if preferred.
431
+ store: SharedLinkStoreInterface = Depends(get_shared_link_store)
432
+ ):
433
+ """Handles message sending for a shared chat session."""
434
+ frozen_chat_cfg, flock_inst, _ = await _get_shared_chat_context(request, share_id, store)
435
+ is_error = False # Initialize is_error
436
+
437
+ if not frozen_chat_cfg or not flock_inst:
438
+ # Error response if config or flock couldn't be loaded
439
+ # This history is ephemeral as it won't be saved if the config is bad
440
+ error_history = [{"role": "bot", "text": "Error: Shared chat configuration is invalid or Flock not found.", "timestamp": datetime.now().strftime('%H:%M')}]
441
+ return templates.TemplateResponse(
442
+ "partials/_chat_messages.html",
443
+ {"request": request, "history": error_history, "now": datetime.now},
444
+ status_code=404
445
+ )
446
+
447
+ history = _get_history_for_shared_chat(request, share_id)
448
+ current_time = datetime.now().strftime('%H:%M')
449
+ history.append({"role": "user", "text": message, "timestamp": current_time})
450
+ start_time = datetime.now()
451
+
452
+ bot_agent = frozen_chat_cfg.agent_name
453
+ bot_text: str
454
+
455
+ if bot_agent and bot_agent in getattr(flock_inst, "agents", {}):
456
+ run_input: dict = {}
457
+ if frozen_chat_cfg.message_key: run_input[frozen_chat_cfg.message_key] = message
458
+ 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"]
459
+
460
+ try:
461
+ result_dict = await flock_inst.run_async(start_agent=bot_agent, input=run_input, box_result=False)
462
+ if frozen_chat_cfg.response_key:
463
+ bot_text = str(result_dict.get(frozen_chat_cfg.response_key, result_dict))
464
+ else:
465
+ bot_text = str(result_dict)
466
+
467
+ except Exception as e:
468
+ bot_text = f"Error running agent {bot_agent} in shared chat: {e}"
469
+ is_error = True
470
+ logger.error(f"Error in /chat/send-shared (agent: {bot_agent}, share: {share_id}): {e}", exc_info=True)
471
+
472
+ if not is_error:
473
+ original_bot_text = bot_text # Keep a copy
474
+ formatted_as_json = False
475
+
476
+ stripped_text = bot_text.strip()
477
+ if (stripped_text.startswith('{') and stripped_text.endswith('}')) or \
478
+ (stripped_text.startswith('[') and stripped_text.endswith(']')):
479
+ try:
480
+ parsed_obj = json.loads(bot_text)
481
+ if isinstance(parsed_obj, (dict, list)):
482
+ pretty_json = json.dumps(parsed_obj, indent=2).replace('\\n', '\n')
483
+ bot_text = f'''<pre><code class="language-json">{pretty_json}</code></pre>'''
484
+ formatted_as_json = True
485
+ except json.JSONDecodeError:
486
+ try:
487
+ evaluated_obj = ast.literal_eval(bot_text)
488
+ if isinstance(evaluated_obj, (dict, list)):
489
+ pretty_json = json.dumps(evaluated_obj, indent=2).replace('\\n', '\n')
490
+ bot_text = f'''<pre><code class="language-json">{pretty_json}</code></pre>'''
491
+ formatted_as_json = True
492
+ except (ValueError, SyntaxError, TypeError):
493
+ pass # Fall through
494
+ except Exception as e_json_fmt:
495
+ logger.error(f"Error formatting likely JSON (shared chat): {e_json_fmt}. Original: {original_bot_text[:200]}", exc_info=True)
496
+ bot_text = original_bot_text
497
+
498
+ if not formatted_as_json:
499
+ try:
500
+ bot_text = markdown2.markdown(original_bot_text, extras=["fenced-code-blocks", "tables", "break-on-newline"])
501
+ except Exception:
502
+ logger.error(f"Error during Markdown conversion for shared chat bot_text. Original: {original_bot_text[:200]}", exc_info=True)
503
+ bot_text = original_bot_text
504
+ else:
505
+ # Fallback if agent misconfigured or not found in the specific shared flock
506
+ is_error = True # Treat these as plain text
507
+ if bot_agent and bot_agent not in getattr(flock_inst, "agents", {}):
508
+ bot_text = f"Agent '{bot_agent}' (shared) not found in its Flock."
509
+ elif not bot_agent:
510
+ bot_text = f"No agent configured for this shared chat. Echoing: {message}"
511
+ else: # Should not happen if frozen_chat_cfg and flock_inst were valid earlier
512
+ bot_text = f"Shared Echo: {message}"
513
+
514
+ duration_ms = int((datetime.now() - start_time).total_seconds() * 1000)
515
+ history.append({"role": "bot", "text": bot_text, "timestamp": current_time, "agent": bot_agent or "shared-echo", "duration_ms": duration_ms})
516
+
517
+ return templates.TemplateResponse(
518
+ "partials/_chat_messages.html",
519
+ {"request": request, "history": history, "now": datetime.now}
520
+ )
@@ -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
@@ -70,7 +84,7 @@ def set_current_theme_name(theme_name: str | None):
70
84
  else:
71
85
  print(f"Warning: Theme 'random' specified, but no themes found in {THEMES_DIR}. Using default: {DEFAULT_THEME_NAME}")
72
86
  # resolved_theme remains DEFAULT_THEME_NAME
73
- elif theme_name in [t.value for t in OutputTheme] or theme_name in list_available_themes():
87
+ elif theme_name in list_available_themes():
74
88
  resolved_theme = theme_name
75
89
  else:
76
90
  print(f"Warning: Invalid theme name provided ('{theme_name}'). Using default: {DEFAULT_THEME_NAME}")
@@ -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