mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-05-29 19:35:20 +02:00
chore: linting
This commit is contained in:
parent
4dda02c06c
commit
94e834134f
80 changed files with 443 additions and 404 deletions
|
|
@ -98,9 +98,7 @@ def upgrade() -> None:
|
||||||
op.execute(
|
op.execute(
|
||||||
"CREATE INDEX ix_automation_triggers_automation_id ON automation_triggers(automation_id);"
|
"CREATE INDEX ix_automation_triggers_automation_id ON automation_triggers(automation_id);"
|
||||||
)
|
)
|
||||||
op.execute(
|
op.execute("CREATE INDEX ix_automation_triggers_type ON automation_triggers(type);")
|
||||||
"CREATE INDEX ix_automation_triggers_type ON automation_triggers(type);"
|
|
||||||
)
|
|
||||||
op.execute(
|
op.execute(
|
||||||
"CREATE INDEX ix_automation_triggers_enabled ON automation_triggers(enabled);"
|
"CREATE INDEX ix_automation_triggers_enabled ON automation_triggers(enabled);"
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,6 @@ from __future__ import annotations
|
||||||
|
|
||||||
from datetime import UTC, datetime
|
from datetime import UTC, datetime
|
||||||
|
|
||||||
|
|
||||||
_HEADER = """\
|
_HEADER = """\
|
||||||
You are the SurfSense automation drafter. Convert the user intent below
|
You are the SurfSense automation drafter. Convert the user intent below
|
||||||
into a SINGLE JSON object matching the AutomationCreate schema. Output
|
into a SINGLE JSON object matching the AutomationCreate schema. Output
|
||||||
|
|
|
||||||
|
|
@ -404,9 +404,7 @@ def build_task_tool_with_parent_config(
|
||||||
continue
|
continue
|
||||||
messages = payload.get("messages") or []
|
messages = payload.get("messages") or []
|
||||||
last_text = _safe_message_text(messages[-1]).rstrip() if messages else ""
|
last_text = _safe_message_text(messages[-1]).rstrip() if messages else ""
|
||||||
message_blocks.append(
|
message_blocks.append(f"[task {task_index}] {last_text or '<empty>'}")
|
||||||
f"[task {task_index}] {last_text or '<empty>'}"
|
|
||||||
)
|
|
||||||
try:
|
try:
|
||||||
child_trace = _build_tool_trace(messages)
|
child_trace = _build_tool_trace(messages)
|
||||||
except Exception:
|
except Exception:
|
||||||
|
|
|
||||||
|
|
@ -117,9 +117,7 @@ def create_generate_podcast_tool(
|
||||||
"podcast_id": podcast_id,
|
"podcast_id": podcast_id,
|
||||||
"title": podcast_title,
|
"title": podcast_title,
|
||||||
"file_location": file_location,
|
"file_location": file_location,
|
||||||
"message": (
|
"message": ("Podcast generated and saved to your podcast panel."),
|
||||||
"Podcast generated and saved to your podcast panel."
|
|
||||||
),
|
|
||||||
}
|
}
|
||||||
return with_receipt(
|
return with_receipt(
|
||||||
payload=payload,
|
payload=payload,
|
||||||
|
|
|
||||||
|
|
@ -126,8 +126,7 @@ def create_generate_video_presentation_tool(
|
||||||
elapsed,
|
elapsed,
|
||||||
)
|
)
|
||||||
err = (
|
err = (
|
||||||
"Background worker reported FAILED status for this "
|
"Background worker reported FAILED status for this video presentation."
|
||||||
"video presentation."
|
|
||||||
)
|
)
|
||||||
payload = {
|
payload = {
|
||||||
"status": VideoPresentationStatus.FAILED.value,
|
"status": VideoPresentationStatus.FAILED.value,
|
||||||
|
|
@ -151,9 +150,7 @@ def create_generate_video_presentation_tool(
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
error_message = str(e)
|
error_message = str(e)
|
||||||
logger.exception(
|
logger.exception("[generate_video_presentation] Error: %s", error_message)
|
||||||
"[generate_video_presentation] Error: %s", error_message
|
|
||||||
)
|
|
||||||
payload = {
|
payload = {
|
||||||
"status": VideoPresentationStatus.FAILED.value,
|
"status": VideoPresentationStatus.FAILED.value,
|
||||||
"error": error_message,
|
"error": error_message,
|
||||||
|
|
|
||||||
|
|
@ -131,9 +131,7 @@ def create_generate_podcast_tool(
|
||||||
"podcast_id": podcast_id,
|
"podcast_id": podcast_id,
|
||||||
"title": podcast_title,
|
"title": podcast_title,
|
||||||
"file_location": file_location,
|
"file_location": file_location,
|
||||||
"message": (
|
"message": ("Podcast generated and saved to your podcast panel."),
|
||||||
"Podcast generated and saved to your podcast panel."
|
|
||||||
),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# Only other terminal state is FAILED.
|
# Only other terminal state is FAILED.
|
||||||
|
|
@ -146,9 +144,7 @@ def create_generate_podcast_tool(
|
||||||
"status": PodcastStatus.FAILED.value,
|
"status": PodcastStatus.FAILED.value,
|
||||||
"podcast_id": podcast_id,
|
"podcast_id": podcast_id,
|
||||||
"title": podcast_title,
|
"title": podcast_title,
|
||||||
"error": (
|
"error": ("Background worker reported FAILED status for this podcast."),
|
||||||
"Background worker reported FAILED status for this podcast."
|
|
||||||
),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|
|
||||||
|
|
@ -127,9 +127,7 @@ def create_generate_video_presentation_tool(
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
error_message = str(e)
|
error_message = str(e)
|
||||||
logger.exception(
|
logger.exception("[generate_video_presentation] Error: %s", error_message)
|
||||||
"[generate_video_presentation] Error: %s", error_message
|
|
||||||
)
|
|
||||||
return {
|
return {
|
||||||
"status": VideoPresentationStatus.FAILED.value,
|
"status": VideoPresentationStatus.FAILED.value,
|
||||||
"error": error_message,
|
"error": error_message,
|
||||||
|
|
|
||||||
|
|
@ -21,4 +21,4 @@ __all__ = [
|
||||||
]
|
]
|
||||||
|
|
||||||
# Built-in actions self-register at import time.
|
# Built-in actions self-register at import time.
|
||||||
from . import agent_task # noqa: E402, F401
|
from . import agent_task # noqa: F401
|
||||||
|
|
|
||||||
|
|
@ -12,4 +12,4 @@ from .params import AgentTaskActionParams
|
||||||
__all__ = ["AgentTaskActionParams", "build_handler"]
|
__all__ = ["AgentTaskActionParams", "build_handler"]
|
||||||
|
|
||||||
# Side-effect: register on the actions store.
|
# Side-effect: register on the actions store.
|
||||||
from . import definition # noqa: E402, F401
|
from . import definition # noqa: F401
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,6 @@ from app.agents.multi_agent_chat import create_multi_agent_chat_deep_agent
|
||||||
from app.db import ChatVisibility, async_session_maker
|
from app.db import ChatVisibility, async_session_maker
|
||||||
|
|
||||||
from ..types import ActionContext
|
from ..types import ActionContext
|
||||||
|
|
||||||
from .auto_decide import build_auto_decisions
|
from .auto_decide import build_auto_decisions
|
||||||
from .dependencies import build_dependencies
|
from .dependencies import build_dependencies
|
||||||
from .finalize import extract_final_assistant_message
|
from .finalize import extract_final_assistant_message
|
||||||
|
|
|
||||||
|
|
@ -50,7 +50,7 @@ class AutomationRun(BaseModel, TimestampMixin):
|
||||||
definition_snapshot = Column(JSONB, nullable=False)
|
definition_snapshot = Column(JSONB, nullable=False)
|
||||||
|
|
||||||
# merged & validated inputs the run was dispatched with
|
# merged & validated inputs the run was dispatched with
|
||||||
# (trigger.static_inputs ∪ producer runtime data, static wins on collision)
|
# (trigger.static_inputs union producer runtime data, static wins on collision)
|
||||||
inputs = Column(JSONB, nullable=False, server_default="{}")
|
inputs = Column(JSONB, nullable=False, server_default="{}")
|
||||||
# one entry per executed step; agent_task entries carry their own
|
# one entry per executed step; agent_task entries carry their own
|
||||||
# `agent_session_id` inside their entry
|
# `agent_session_id` inside their entry
|
||||||
|
|
|
||||||
|
|
@ -6,9 +6,9 @@ from typing import Any
|
||||||
|
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
|
from app.automations.actions.types import ActionContext
|
||||||
from app.automations.persistence.enums.run_status import RunStatus
|
from app.automations.persistence.enums.run_status import RunStatus
|
||||||
from app.automations.persistence.models.run import AutomationRun
|
from app.automations.persistence.models.run import AutomationRun
|
||||||
from app.automations.actions.types import ActionContext
|
|
||||||
from app.automations.schemas.definition.envelope import AutomationDefinition
|
from app.automations.schemas.definition.envelope import AutomationDefinition
|
||||||
from app.automations.schemas.definition.plan_step import PlanStep
|
from app.automations.schemas.definition.plan_step import PlanStep
|
||||||
from app.automations.templating import build_run_context
|
from app.automations.templating import build_run_context
|
||||||
|
|
@ -32,7 +32,10 @@ async def execute_run(session: AsyncSession, run_id: int) -> None:
|
||||||
await repository.mark_failed(
|
await repository.mark_failed(
|
||||||
session,
|
session,
|
||||||
run,
|
run,
|
||||||
{"message": f"definition_snapshot invalid: {exc}", "type": type(exc).__name__},
|
{
|
||||||
|
"message": f"definition_snapshot invalid: {exc}",
|
||||||
|
"type": type(exc).__name__,
|
||||||
|
},
|
||||||
)
|
)
|
||||||
await session.commit()
|
await session.commit()
|
||||||
return
|
return
|
||||||
|
|
@ -92,7 +95,9 @@ async def _run_on_failure(
|
||||||
await session.commit()
|
await session.commit()
|
||||||
|
|
||||||
|
|
||||||
def _build_template_ctx(run: AutomationRun, step_outputs: dict[str, Any]) -> dict[str, Any]:
|
def _build_template_ctx(
|
||||||
|
run: AutomationRun, step_outputs: dict[str, Any]
|
||||||
|
) -> dict[str, Any]:
|
||||||
automation = run.automation
|
automation = run.automation
|
||||||
trigger = run.trigger
|
trigger = run.trigger
|
||||||
return build_run_context(
|
return build_run_context(
|
||||||
|
|
|
||||||
|
|
@ -30,14 +30,18 @@ async def execute_step(
|
||||||
try:
|
try:
|
||||||
should_run = evaluate_predicate(step.when, template_context)
|
should_run = evaluate_predicate(step.when, template_context)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
return _result(step, "failed", started_at, attempts=0, error=_error(exc, "when"))
|
return _result(
|
||||||
|
step, "failed", started_at, attempts=0, error=_error(exc, "when")
|
||||||
|
)
|
||||||
if not should_run:
|
if not should_run:
|
||||||
return _result(step, "skipped", started_at, attempts=0)
|
return _result(step, "skipped", started_at, attempts=0)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
resolved_params = render_value(step.params, template_context)
|
resolved_params = render_value(step.params, template_context)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
return _result(step, "failed", started_at, attempts=0, error=_error(exc, "render"))
|
return _result(
|
||||||
|
step, "failed", started_at, attempts=0, error=_error(exc, "render")
|
||||||
|
)
|
||||||
|
|
||||||
action = get_action(step.action)
|
action = get_action(step.action)
|
||||||
if action is None:
|
if action is None:
|
||||||
|
|
@ -46,12 +50,17 @@ async def execute_step(
|
||||||
"failed",
|
"failed",
|
||||||
started_at,
|
started_at,
|
||||||
attempts=0,
|
attempts=0,
|
||||||
error={"message": f"action not registered: {step.action}", "type": "ActionNotFound"},
|
error={
|
||||||
|
"message": f"action not registered: {step.action}",
|
||||||
|
"type": "ActionNotFound",
|
||||||
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
handler = action.build_handler(action_context)
|
handler = action.build_handler(action_context)
|
||||||
|
|
||||||
max_retries = step.max_retries if step.max_retries is not None else default_max_retries
|
max_retries = (
|
||||||
|
step.max_retries if step.max_retries is not None else default_max_retries
|
||||||
|
)
|
||||||
timeout = step.timeout_seconds or default_timeout_seconds
|
timeout = step.timeout_seconds or default_timeout_seconds
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|
@ -62,7 +71,9 @@ async def execute_step(
|
||||||
timeout=timeout,
|
timeout=timeout,
|
||||||
)
|
)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
return _result(step, "failed", started_at, attempts=max_retries + 1, error=_error(exc))
|
return _result(
|
||||||
|
step, "failed", started_at, attempts=max_retries + 1, error=_error(exc)
|
||||||
|
)
|
||||||
|
|
||||||
return _result(step, "succeeded", started_at, attempts=attempts, result=result)
|
return _result(step, "succeeded", started_at, attempts=attempts, result=result)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,9 @@ from .plan_step import PlanStep
|
||||||
class Execution(BaseModel):
|
class Execution(BaseModel):
|
||||||
model_config = ConfigDict(extra="forbid")
|
model_config = ConfigDict(extra="forbid")
|
||||||
|
|
||||||
timeout_seconds: int = Field(default=600, gt=0, description="Wall-clock cap for the run.")
|
timeout_seconds: int = Field(
|
||||||
|
default=600, gt=0, description="Wall-clock cap for the run."
|
||||||
|
)
|
||||||
max_retries: int = Field(default=2, ge=0, description="Per-step retry budget.")
|
max_retries: int = Field(default=2, ge=0, description="Per-step retry budget.")
|
||||||
retry_backoff: Literal["exponential", "linear", "none"] = "exponential"
|
retry_backoff: Literal["exponential", "linear", "none"] = "exponential"
|
||||||
concurrency: Literal["drop_if_running", "queue", "always"] = "drop_if_running"
|
concurrency: Literal["drop_if_running", "queue", "always"] = "drop_if_running"
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,9 @@ class PlanStep(BaseModel):
|
||||||
model_config = ConfigDict(extra="forbid")
|
model_config = ConfigDict(extra="forbid")
|
||||||
|
|
||||||
step_id: str = Field(..., min_length=1, description="Unique within the plan.")
|
step_id: str = Field(..., min_length=1, description="Unique within the plan.")
|
||||||
action: str = Field(..., min_length=1, description="Action type; resolved via registry.")
|
action: str = Field(
|
||||||
|
..., min_length=1, description="Action type; resolved via registry."
|
||||||
|
)
|
||||||
when: str | None = Field(
|
when: str | None = Field(
|
||||||
default=None,
|
default=None,
|
||||||
description="Optional predicate; step is skipped when falsy.",
|
description="Optional predicate; step is skipped when falsy.",
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,9 @@ from pydantic import BaseModel, ConfigDict, Field
|
||||||
class TriggerSpec(BaseModel):
|
class TriggerSpec(BaseModel):
|
||||||
model_config = ConfigDict(extra="forbid")
|
model_config = ConfigDict(extra="forbid")
|
||||||
|
|
||||||
type: str = Field(..., min_length=1, description="Trigger type; resolved via registry.")
|
type: str = Field(
|
||||||
|
..., min_length=1, description="Trigger type; resolved via registry."
|
||||||
|
)
|
||||||
params: dict[str, Any] = Field(
|
params: dict[str, Any] = Field(
|
||||||
default_factory=dict,
|
default_factory=dict,
|
||||||
description="Type-specific params; validated against the trigger's schema.",
|
description="Type-specific params; validated against the trigger's schema.",
|
||||||
|
|
|
||||||
|
|
@ -10,14 +10,14 @@ from sqlalchemy import func, select
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
from sqlalchemy.orm import selectinload
|
from sqlalchemy.orm import selectinload
|
||||||
|
|
||||||
|
from app.automations.persistence.enums.trigger_type import TriggerType
|
||||||
|
from app.automations.persistence.models.automation import Automation
|
||||||
|
from app.automations.persistence.models.trigger import AutomationTrigger
|
||||||
from app.automations.schemas.api import (
|
from app.automations.schemas.api import (
|
||||||
AutomationCreate,
|
AutomationCreate,
|
||||||
AutomationUpdate,
|
AutomationUpdate,
|
||||||
TriggerCreate,
|
TriggerCreate,
|
||||||
)
|
)
|
||||||
from app.automations.persistence.enums.trigger_type import TriggerType
|
|
||||||
from app.automations.persistence.models.automation import Automation
|
|
||||||
from app.automations.persistence.models.trigger import AutomationTrigger
|
|
||||||
from app.automations.triggers import get_trigger
|
from app.automations.triggers import get_trigger
|
||||||
from app.automations.triggers.schedule import compute_next_fire_at
|
from app.automations.triggers.schedule import compute_next_fire_at
|
||||||
from app.db import Permission, User, get_async_session
|
from app.db import Permission, User, get_async_session
|
||||||
|
|
@ -34,7 +34,9 @@ class AutomationService:
|
||||||
|
|
||||||
async def create(self, payload: AutomationCreate) -> Automation:
|
async def create(self, payload: AutomationCreate) -> Automation:
|
||||||
"""Create an automation and its initial triggers in one transaction."""
|
"""Create an automation and its initial triggers in one transaction."""
|
||||||
await self._authorize(payload.search_space_id, Permission.AUTOMATIONS_CREATE.value)
|
await self._authorize(
|
||||||
|
payload.search_space_id, Permission.AUTOMATIONS_CREATE.value
|
||||||
|
)
|
||||||
|
|
||||||
automation = Automation(
|
automation = Automation(
|
||||||
search_space_id=payload.search_space_id,
|
search_space_id=payload.search_space_id,
|
||||||
|
|
@ -67,22 +69,32 @@ class AutomationService:
|
||||||
)
|
)
|
||||||
|
|
||||||
rows = (
|
rows = (
|
||||||
|
(
|
||||||
await self.session.execute(
|
await self.session.execute(
|
||||||
base.order_by(Automation.created_at.desc()).limit(limit).offset(offset)
|
base.order_by(Automation.created_at.desc())
|
||||||
|
.limit(limit)
|
||||||
|
.offset(offset)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.scalars()
|
||||||
|
.all()
|
||||||
)
|
)
|
||||||
).scalars().all()
|
|
||||||
return list(rows), int(total or 0)
|
return list(rows), int(total or 0)
|
||||||
|
|
||||||
async def get(self, automation_id: int) -> Automation:
|
async def get(self, automation_id: int) -> Automation:
|
||||||
"""Get an automation with its triggers loaded."""
|
"""Get an automation with its triggers loaded."""
|
||||||
automation = await self._get_with_triggers_or_raise(automation_id)
|
automation = await self._get_with_triggers_or_raise(automation_id)
|
||||||
await self._authorize(automation.search_space_id, Permission.AUTOMATIONS_READ.value)
|
await self._authorize(
|
||||||
|
automation.search_space_id, Permission.AUTOMATIONS_READ.value
|
||||||
|
)
|
||||||
return automation
|
return automation
|
||||||
|
|
||||||
async def update(self, automation_id: int, patch: AutomationUpdate) -> Automation:
|
async def update(self, automation_id: int, patch: AutomationUpdate) -> Automation:
|
||||||
"""Patch fields. Bumps ``version`` when ``definition`` changes."""
|
"""Patch fields. Bumps ``version`` when ``definition`` changes."""
|
||||||
automation = await self._get_with_triggers_or_raise(automation_id)
|
automation = await self._get_with_triggers_or_raise(automation_id)
|
||||||
await self._authorize(automation.search_space_id, Permission.AUTOMATIONS_UPDATE.value)
|
await self._authorize(
|
||||||
|
automation.search_space_id, Permission.AUTOMATIONS_UPDATE.value
|
||||||
|
)
|
||||||
|
|
||||||
data = patch.model_dump(exclude_unset=True)
|
data = patch.model_dump(exclude_unset=True)
|
||||||
|
|
||||||
|
|
@ -93,7 +105,9 @@ class AutomationService:
|
||||||
if "status" in data:
|
if "status" in data:
|
||||||
automation.status = data["status"]
|
automation.status = data["status"]
|
||||||
if "definition" in data:
|
if "definition" in data:
|
||||||
automation.definition = patch.definition.model_dump(mode="json", by_alias=True)
|
automation.definition = patch.definition.model_dump(
|
||||||
|
mode="json", by_alias=True
|
||||||
|
)
|
||||||
automation.version += 1
|
automation.version += 1
|
||||||
|
|
||||||
await self.session.commit()
|
await self.session.commit()
|
||||||
|
|
@ -102,7 +116,9 @@ class AutomationService:
|
||||||
async def delete(self, automation_id: int) -> None:
|
async def delete(self, automation_id: int) -> None:
|
||||||
"""Delete an automation; FK cascades remove triggers and runs."""
|
"""Delete an automation; FK cascades remove triggers and runs."""
|
||||||
automation = await self._get_or_raise(automation_id)
|
automation = await self._get_or_raise(automation_id)
|
||||||
await self._authorize(automation.search_space_id, Permission.AUTOMATIONS_DELETE.value)
|
await self._authorize(
|
||||||
|
automation.search_space_id, Permission.AUTOMATIONS_DELETE.value
|
||||||
|
)
|
||||||
await self.session.delete(automation)
|
await self.session.delete(automation)
|
||||||
await self.session.commit()
|
await self.session.commit()
|
||||||
|
|
||||||
|
|
@ -141,7 +157,9 @@ def _build_trigger(spec: TriggerCreate) -> AutomationTrigger:
|
||||||
"""Validate trigger params via its registered Pydantic model and build the ORM row."""
|
"""Validate trigger params via its registered Pydantic model and build the ORM row."""
|
||||||
definition = get_trigger(spec.type.value)
|
definition = get_trigger(spec.type.value)
|
||||||
if definition is None:
|
if definition is None:
|
||||||
raise HTTPException(status_code=422, detail=f"unknown trigger type {spec.type.value!r}")
|
raise HTTPException(
|
||||||
|
status_code=422, detail=f"unknown trigger type {spec.type.value!r}"
|
||||||
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
validated = definition.params_model.model_validate(spec.params)
|
validated = definition.params_model.model_validate(spec.params)
|
||||||
|
|
|
||||||
|
|
@ -36,10 +36,16 @@ class RunService:
|
||||||
)
|
)
|
||||||
|
|
||||||
rows = (
|
rows = (
|
||||||
|
(
|
||||||
await self.session.execute(
|
await self.session.execute(
|
||||||
base.order_by(AutomationRun.created_at.desc()).limit(limit).offset(offset)
|
base.order_by(AutomationRun.created_at.desc())
|
||||||
|
.limit(limit)
|
||||||
|
.offset(offset)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.scalars()
|
||||||
|
.all()
|
||||||
)
|
)
|
||||||
).scalars().all()
|
|
||||||
return list(rows), int(total or 0)
|
return list(rows), int(total or 0)
|
||||||
|
|
||||||
async def get(self, *, automation_id: int, run_id: int) -> AutomationRun:
|
async def get(self, *, automation_id: int, run_id: int) -> AutomationRun:
|
||||||
|
|
|
||||||
|
|
@ -8,10 +8,10 @@ from fastapi import Depends, HTTPException
|
||||||
from pydantic import ValidationError
|
from pydantic import ValidationError
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
from app.automations.schemas.api import TriggerCreate, TriggerUpdate
|
|
||||||
from app.automations.persistence.enums.trigger_type import TriggerType
|
from app.automations.persistence.enums.trigger_type import TriggerType
|
||||||
from app.automations.persistence.models.automation import Automation
|
from app.automations.persistence.models.automation import Automation
|
||||||
from app.automations.persistence.models.trigger import AutomationTrigger
|
from app.automations.persistence.models.trigger import AutomationTrigger
|
||||||
|
from app.automations.schemas.api import TriggerCreate, TriggerUpdate
|
||||||
from app.automations.triggers import get_trigger
|
from app.automations.triggers import get_trigger
|
||||||
from app.automations.triggers.schedule import compute_next_fire_at
|
from app.automations.triggers.schedule import compute_next_fire_at
|
||||||
from app.db import Permission, User, get_async_session
|
from app.db import Permission, User, get_async_session
|
||||||
|
|
@ -40,7 +40,9 @@ class TriggerService:
|
||||||
params=validated_params,
|
params=validated_params,
|
||||||
static_inputs=payload.static_inputs,
|
static_inputs=payload.static_inputs,
|
||||||
enabled=payload.enabled,
|
enabled=payload.enabled,
|
||||||
next_fire_at=_initial_next_fire(payload.type, validated_params, payload.enabled),
|
next_fire_at=_initial_next_fire(
|
||||||
|
payload.type, validated_params, payload.enabled
|
||||||
|
),
|
||||||
)
|
)
|
||||||
self.session.add(trigger)
|
self.session.add(trigger)
|
||||||
await self.session.commit()
|
await self.session.commit()
|
||||||
|
|
@ -54,7 +56,9 @@ class TriggerService:
|
||||||
trigger_id: int,
|
trigger_id: int,
|
||||||
patch: TriggerUpdate,
|
patch: TriggerUpdate,
|
||||||
) -> AutomationTrigger:
|
) -> AutomationTrigger:
|
||||||
await self._authorize_automation(automation_id, Permission.AUTOMATIONS_UPDATE.value)
|
await self._authorize_automation(
|
||||||
|
automation_id, Permission.AUTOMATIONS_UPDATE.value
|
||||||
|
)
|
||||||
trigger = await self._get_trigger_or_raise(automation_id, trigger_id)
|
trigger = await self._get_trigger_or_raise(automation_id, trigger_id)
|
||||||
|
|
||||||
data = patch.model_dump(exclude_unset=True)
|
data = patch.model_dump(exclude_unset=True)
|
||||||
|
|
@ -80,7 +84,9 @@ class TriggerService:
|
||||||
return trigger
|
return trigger
|
||||||
|
|
||||||
async def remove(self, *, automation_id: int, trigger_id: int) -> None:
|
async def remove(self, *, automation_id: int, trigger_id: int) -> None:
|
||||||
await self._authorize_automation(automation_id, Permission.AUTOMATIONS_UPDATE.value)
|
await self._authorize_automation(
|
||||||
|
automation_id, Permission.AUTOMATIONS_UPDATE.value
|
||||||
|
)
|
||||||
trigger = await self._get_trigger_or_raise(automation_id, trigger_id)
|
trigger = await self._get_trigger_or_raise(automation_id, trigger_id)
|
||||||
await self.session.delete(trigger)
|
await self.session.delete(trigger)
|
||||||
await self.session.commit()
|
await self.session.commit()
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ TASK_NAME = "automation_run_execute"
|
||||||
|
|
||||||
|
|
||||||
@celery_app.task(name=TASK_NAME, bind=True)
|
@celery_app.task(name=TASK_NAME, bind=True)
|
||||||
def automation_run_execute(self, run_id: int) -> None: # noqa: ARG001 — Celery bind
|
def automation_run_execute(self, run_id: int) -> None:
|
||||||
"""Execute one ``AutomationRun``. Idempotent: terminal runs no-op."""
|
"""Execute one ``AutomationRun``. Idempotent: terminal runs no-op."""
|
||||||
return run_async_celery_task(lambda: _impl(run_id))
|
return run_async_celery_task(lambda: _impl(run_id))
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -103,9 +103,7 @@ async def _self_heal_null_next_fire(session: AsyncSession, *, now: datetime) ->
|
||||||
await session.commit()
|
await session.commit()
|
||||||
|
|
||||||
|
|
||||||
async def _claim_due_triggers(
|
async def _claim_due_triggers(session: AsyncSession, *, now: datetime) -> list[_Claim]:
|
||||||
session: AsyncSession, *, now: datetime
|
|
||||||
) -> list[_Claim]:
|
|
||||||
"""Lock and advance due rows; return per-trigger fire context."""
|
"""Lock and advance due rows; return per-trigger fire context."""
|
||||||
stmt = (
|
stmt = (
|
||||||
select(AutomationTrigger)
|
select(AutomationTrigger)
|
||||||
|
|
|
||||||
|
|
@ -17,4 +17,4 @@ __all__ = [
|
||||||
]
|
]
|
||||||
|
|
||||||
# Built-in triggers self-register at import time.
|
# Built-in triggers self-register at import time.
|
||||||
from . import schedule # noqa: E402, F401
|
from . import schedule # noqa: F401
|
||||||
|
|
|
||||||
|
|
@ -15,4 +15,4 @@ __all__ = [
|
||||||
]
|
]
|
||||||
|
|
||||||
# Side-effect: register on the triggers store.
|
# Side-effect: register on the triggers store.
|
||||||
from . import definition # noqa: E402, F401
|
from . import definition # noqa: F401
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,10 @@ def compute_next_fire_at(cron: str, timezone: str, *, after: datetime) -> dateti
|
||||||
given timezone before evaluation so DST and IANA rules apply correctly.
|
given timezone before evaluation so DST and IANA rules apply correctly.
|
||||||
"""
|
"""
|
||||||
tz = ZoneInfo(timezone)
|
tz = ZoneInfo(timezone)
|
||||||
base = after.astimezone(tz) if after.tzinfo else after.replace(tzinfo=UTC).astimezone(tz)
|
base = (
|
||||||
|
after.astimezone(tz)
|
||||||
|
if after.tzinfo
|
||||||
|
else after.replace(tzinfo=UTC).astimezone(tz)
|
||||||
|
)
|
||||||
nxt: datetime = croniter(cron, base).get_next(datetime)
|
nxt: datetime = croniter(cron, base).get_next(datetime)
|
||||||
return nxt.astimezone(UTC)
|
return nxt.astimezone(UTC)
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,9 @@ from .cron import InvalidCronError, validate_cron
|
||||||
class ScheduleTriggerParams(BaseModel):
|
class ScheduleTriggerParams(BaseModel):
|
||||||
model_config = ConfigDict(extra="forbid")
|
model_config = ConfigDict(extra="forbid")
|
||||||
|
|
||||||
cron: str = Field(..., description="Five-field cron expression.", examples=["0 9 * * 1-5"])
|
cron: str = Field(
|
||||||
|
..., description="Five-field cron expression.", examples=["0 9 * * 1-5"]
|
||||||
|
)
|
||||||
timezone: str = Field(..., description="IANA timezone.", examples=["Africa/Kigali"])
|
timezone: str = Field(..., description="IANA timezone.", examples=["Africa/Kigali"])
|
||||||
|
|
||||||
@model_validator(mode="after")
|
@model_validator(mode="after")
|
||||||
|
|
|
||||||
|
|
@ -2605,7 +2605,6 @@ from app.automations.persistence import ( # noqa: E402, F401
|
||||||
AutomationTrigger,
|
AutomationTrigger,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
engine = create_async_engine(
|
engine = create_async_engine(
|
||||||
DATABASE_URL,
|
DATABASE_URL,
|
||||||
pool_size=30,
|
pool_size=30,
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
from fastapi import APIRouter
|
from fastapi import APIRouter
|
||||||
|
|
||||||
|
from app.automations.api import router as automations_router
|
||||||
|
|
||||||
from .agent_action_log_route import router as agent_action_log_router
|
from .agent_action_log_route import router as agent_action_log_router
|
||||||
from .agent_flags_route import router as agent_flags_router
|
from .agent_flags_route import router as agent_flags_router
|
||||||
from .agent_permissions_route import router as agent_permissions_router
|
from .agent_permissions_route import router as agent_permissions_router
|
||||||
|
|
@ -7,7 +9,6 @@ from .agent_revert_route import router as agent_revert_router
|
||||||
from .airtable_add_connector_route import (
|
from .airtable_add_connector_route import (
|
||||||
router as airtable_add_connector_router,
|
router as airtable_add_connector_router,
|
||||||
)
|
)
|
||||||
from app.automations.api import router as automations_router
|
|
||||||
from .chat_comments_routes import router as chat_comments_router
|
from .chat_comments_routes import router as chat_comments_router
|
||||||
from .circleback_webhook_route import router as circleback_webhook_router
|
from .circleback_webhook_route import router as circleback_webhook_router
|
||||||
from .clickup_add_connector_route import router as clickup_add_connector_router
|
from .clickup_add_connector_route import router as clickup_add_connector_router
|
||||||
|
|
|
||||||
|
|
@ -19,9 +19,7 @@ def extract_todos_from_deepagents(command_output: Any) -> dict:
|
||||||
elif isinstance(command_output, dict):
|
elif isinstance(command_output, dict):
|
||||||
if "todos" in command_output:
|
if "todos" in command_output:
|
||||||
todos_data = command_output.get("todos", [])
|
todos_data = command_output.get("todos", [])
|
||||||
elif "update" in command_output and isinstance(
|
elif "update" in command_output and isinstance(command_output["update"], dict):
|
||||||
command_output["update"], dict
|
|
||||||
):
|
|
||||||
todos_data = command_output["update"].get("todos", [])
|
todos_data = command_output["update"].get("todos", [])
|
||||||
|
|
||||||
return {"todos": todos_data}
|
return {"todos": todos_data}
|
||||||
|
|
|
||||||
|
|
@ -69,17 +69,13 @@ async def resolve_initial_auto_pin(
|
||||||
"pin.requires_image_input": requires_image_input,
|
"pin.requires_image_input": requires_image_input,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
return AutoPinResult(
|
return AutoPinResult(llm_config_id=pinned.resolved_llm_config_id, error=None)
|
||||||
llm_config_id=pinned.resolved_llm_config_id, error=None
|
|
||||||
)
|
|
||||||
except ValueError as pin_error:
|
except ValueError as pin_error:
|
||||||
# The "no vision-capable cfg" path raises a ValueError whose message
|
# The "no vision-capable cfg" path raises a ValueError whose message
|
||||||
# we map to the friendly image-input SSE error so the user sees the
|
# we map to the friendly image-input SSE error so the user sees the
|
||||||
# same message regardless of whether the gate fired in the resolver or
|
# same message regardless of whether the gate fired in the resolver or
|
||||||
# in ``llm_capability.assert_vision_capability_for_image_turn``.
|
# in ``llm_capability.assert_vision_capability_for_image_turn``.
|
||||||
is_vision_failure = (
|
is_vision_failure = requires_image_input and "vision-capable" in str(pin_error)
|
||||||
requires_image_input and "vision-capable" in str(pin_error)
|
|
||||||
)
|
|
||||||
error_code = (
|
error_code = (
|
||||||
"MODEL_DOES_NOT_SUPPORT_IMAGE_INPUT"
|
"MODEL_DOES_NOT_SUPPORT_IMAGE_INPUT"
|
||||||
if is_vision_failure
|
if is_vision_failure
|
||||||
|
|
|
||||||
|
|
@ -207,9 +207,7 @@ async def _resolve_mentions_for_query(
|
||||||
try:
|
try:
|
||||||
chip_objs.append(MentionedDocumentInfo.model_validate(raw))
|
chip_objs.append(MentionedDocumentInfo.model_validate(raw))
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.debug(
|
logger.debug("stream_new_chat: dropping malformed mention chip %r", raw)
|
||||||
"stream_new_chat: dropping malformed mention chip %r", raw
|
|
||||||
)
|
|
||||||
|
|
||||||
resolved = await resolve_mentions(
|
resolved = await resolve_mentions(
|
||||||
session,
|
session,
|
||||||
|
|
|
||||||
|
|
@ -48,9 +48,7 @@ def check_image_input_capability(
|
||||||
return None
|
return None
|
||||||
|
|
||||||
model_label = agent_config.config_name or agent_config.model_name or "model"
|
model_label = agent_config.config_name or agent_config.model_name or "model"
|
||||||
ot.add_event(
|
ot.add_event("quota.denied", {"quota.code": "MODEL_DOES_NOT_SUPPORT_IMAGE_INPUT"})
|
||||||
"quota.denied", {"quota.code": "MODEL_DOES_NOT_SUPPORT_IMAGE_INPUT"}
|
|
||||||
)
|
|
||||||
return (
|
return (
|
||||||
(
|
(
|
||||||
f"The selected model ({model_label}) does not support "
|
f"The selected model ({model_label}) does not support "
|
||||||
|
|
|
||||||
|
|
@ -259,7 +259,8 @@ async def stream_new_chat(
|
||||||
|
|
||||||
if needs_premium_quota(agent_config, user_id):
|
if needs_premium_quota(agent_config, user_id):
|
||||||
premium_reservation = await reserve_premium(
|
premium_reservation = await reserve_premium(
|
||||||
agent_config=agent_config, user_id=user_id # type: ignore[arg-type]
|
agent_config=agent_config,
|
||||||
|
user_id=user_id, # type: ignore[arg-type]
|
||||||
)
|
)
|
||||||
if not premium_reservation.allowed:
|
if not premium_reservation.allowed:
|
||||||
ot.add_event("quota.denied", {"quota.code": "PREMIUM_QUOTA_EXHAUSTED"})
|
ot.add_event("quota.denied", {"quota.code": "PREMIUM_QUOTA_EXHAUSTED"})
|
||||||
|
|
@ -492,7 +493,9 @@ async def stream_new_chat(
|
||||||
|
|
||||||
# --- Block 4: First SSE frames ---
|
# --- Block 4: First SSE frames ---
|
||||||
|
|
||||||
for sse in iter_initial_frames(streaming_service, turn_id=stream_result.turn_id):
|
for sse in iter_initial_frames(
|
||||||
|
streaming_service, turn_id=stream_result.turn_id
|
||||||
|
):
|
||||||
yield sse
|
yield sse
|
||||||
|
|
||||||
# --- Block 5: Persistence join + message-id frames ---
|
# --- Block 5: Persistence join + message-id frames ---
|
||||||
|
|
@ -693,7 +696,9 @@ async def stream_new_chat(
|
||||||
fallback_commit_search_space_id=search_space_id,
|
fallback_commit_search_space_id=search_space_id,
|
||||||
fallback_commit_created_by_id=user_id,
|
fallback_commit_created_by_id=user_id,
|
||||||
fallback_commit_filesystem_mode=(
|
fallback_commit_filesystem_mode=(
|
||||||
filesystem_selection.mode if filesystem_selection else FilesystemMode.CLOUD
|
filesystem_selection.mode
|
||||||
|
if filesystem_selection
|
||||||
|
else FilesystemMode.CLOUD
|
||||||
),
|
),
|
||||||
fallback_commit_thread_id=chat_id,
|
fallback_commit_thread_id=chat_id,
|
||||||
runtime_context=runtime_context,
|
runtime_context=runtime_context,
|
||||||
|
|
@ -715,11 +720,7 @@ async def stream_new_chat(
|
||||||
title_emitted = True
|
title_emitted = True
|
||||||
# Account for the case where the task completed but produced no
|
# Account for the case where the task completed but produced no
|
||||||
# title — flip the flag anyway so we don't keep checking it.
|
# title — flip the flag anyway so we don't keep checking it.
|
||||||
if (
|
if title_task is not None and title_task.done() and not title_emitted:
|
||||||
title_task is not None
|
|
||||||
and title_task.done()
|
|
||||||
and not title_emitted
|
|
||||||
):
|
|
||||||
title_emitted = True
|
title_emitted = True
|
||||||
|
|
||||||
_perf_log.info(
|
_perf_log.info(
|
||||||
|
|
@ -811,9 +812,7 @@ async def stream_new_chat(
|
||||||
end_turn(str(chat_id))
|
end_turn(str(chat_id))
|
||||||
|
|
||||||
if premium_reservation is not None and user_id:
|
if premium_reservation is not None and user_id:
|
||||||
await release_premium(
|
await release_premium(reservation=premium_reservation, user_id=user_id)
|
||||||
reservation=premium_reservation, user_id=user_id
|
|
||||||
)
|
|
||||||
|
|
||||||
await close_session_and_clear_ai_responding(session, chat_id)
|
await close_session_and_clear_ai_responding(session, chat_id)
|
||||||
|
|
||||||
|
|
@ -852,9 +851,9 @@ async def stream_new_chat(
|
||||||
|
|
||||||
# Break circular refs held by the agent graph, tools, and LLM
|
# Break circular refs held by the agent graph, tools, and LLM
|
||||||
# wrappers so the GC can reclaim them in a single pass.
|
# wrappers so the GC can reclaim them in a single pass.
|
||||||
agent = llm = connector_service = None # noqa: F841
|
agent = llm = connector_service = None
|
||||||
input_state = stream_result = None # noqa: F841
|
input_state = stream_result = None
|
||||||
session = None # noqa: F841
|
session = None
|
||||||
|
|
||||||
run_gc_pass(log_prefix="stream_new_chat", chat_id=chat_id)
|
run_gc_pass(log_prefix="stream_new_chat", chat_id=chat_id)
|
||||||
close_chat_request_span(
|
close_chat_request_span(
|
||||||
|
|
|
||||||
|
|
@ -30,9 +30,7 @@ def build_new_chat_runtime_context(
|
||||||
return SurfSenseContextSchema(
|
return SurfSenseContextSchema(
|
||||||
search_space_id=search_space_id,
|
search_space_id=search_space_id,
|
||||||
mentioned_document_ids=list(mentioned_document_ids or []),
|
mentioned_document_ids=list(mentioned_document_ids or []),
|
||||||
mentioned_folder_ids=list(
|
mentioned_folder_ids=list(accepted_folder_ids or mentioned_folder_ids or []),
|
||||||
accepted_folder_ids or mentioned_folder_ids or []
|
|
||||||
),
|
|
||||||
request_id=request_id,
|
request_id=request_id,
|
||||||
turn_id=turn_id,
|
turn_id=turn_id,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -133,12 +133,8 @@ async def _generate_title(
|
||||||
# inherited Azure endpoint — see ``provider_api_base`` for the
|
# inherited Azure endpoint — see ``provider_api_base`` for the
|
||||||
# same bug repro on the image-gen / vision paths.
|
# same bug repro on the image-gen / vision paths.
|
||||||
raw_model = getattr(llm, "model", "") or ""
|
raw_model = getattr(llm, "model", "") or ""
|
||||||
provider_prefix = (
|
provider_prefix = raw_model.split("/", 1)[0] if "/" in raw_model else None
|
||||||
raw_model.split("/", 1)[0] if "/" in raw_model else None
|
provider_value = agent_config.provider if agent_config is not None else None
|
||||||
)
|
|
||||||
provider_value = (
|
|
||||||
agent_config.provider if agent_config is not None else None
|
|
||||||
)
|
|
||||||
title_api_base = resolve_api_base(
|
title_api_base = resolve_api_base(
|
||||||
provider=provider_value,
|
provider=provider_value,
|
||||||
provider_prefix=provider_prefix,
|
provider_prefix=provider_prefix,
|
||||||
|
|
|
||||||
|
|
@ -15,14 +15,10 @@ building blocks under ``flows/shared/``. Mirrors ``stream_new_chat`` but:
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import contextlib
|
import contextlib
|
||||||
import gc
|
|
||||||
import logging
|
import logging
|
||||||
import sys
|
|
||||||
import time
|
import time
|
||||||
import uuid as _uuid
|
|
||||||
from collections.abc import AsyncGenerator
|
from collections.abc import AsyncGenerator
|
||||||
from functools import partial
|
from functools import partial
|
||||||
from typing import Any
|
|
||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
|
|
||||||
import anyio
|
import anyio
|
||||||
|
|
@ -32,7 +28,7 @@ from app.agents.new_chat.chat_deepagent import create_surfsense_deep_agent
|
||||||
from app.agents.new_chat.filesystem_selection import FilesystemMode, FilesystemSelection
|
from app.agents.new_chat.filesystem_selection import FilesystemMode, FilesystemSelection
|
||||||
from app.agents.new_chat.middleware.busy_mutex import end_turn
|
from app.agents.new_chat.middleware.busy_mutex import end_turn
|
||||||
from app.config import config as _app_config
|
from app.config import config as _app_config
|
||||||
from app.db import ChatVisibility, async_session_maker, shielded_async_session
|
from app.db import ChatVisibility, async_session_maker
|
||||||
from app.observability import otel as ot
|
from app.observability import otel as ot
|
||||||
from app.services.chat_session_state_service import set_ai_responding
|
from app.services.chat_session_state_service import set_ai_responding
|
||||||
from app.services.new_streaming_service import VercelStreamingService
|
from app.services.new_streaming_service import VercelStreamingService
|
||||||
|
|
@ -89,7 +85,7 @@ from app.tasks.chat.streaming.flows.shared.terminal_error import (
|
||||||
)
|
)
|
||||||
from app.tasks.chat.streaming.shared.stream_result import StreamResult
|
from app.tasks.chat.streaming.shared.stream_result import StreamResult
|
||||||
from app.tasks.chat.streaming.shared.utils import resume_step_prefix
|
from app.tasks.chat.streaming.shared.utils import resume_step_prefix
|
||||||
from app.utils.perf import get_perf_logger, log_system_snapshot
|
from app.utils.perf import get_perf_logger
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
_perf_log = get_perf_logger()
|
_perf_log = get_perf_logger()
|
||||||
|
|
@ -217,12 +213,11 @@ async def stream_resume_chat(
|
||||||
|
|
||||||
if needs_premium_quota(agent_config, user_id):
|
if needs_premium_quota(agent_config, user_id):
|
||||||
premium_reservation = await reserve_premium(
|
premium_reservation = await reserve_premium(
|
||||||
agent_config=agent_config, user_id=user_id # type: ignore[arg-type]
|
agent_config=agent_config,
|
||||||
|
user_id=user_id, # type: ignore[arg-type]
|
||||||
)
|
)
|
||||||
if not premium_reservation.allowed:
|
if not premium_reservation.allowed:
|
||||||
ot.add_event(
|
ot.add_event("quota.denied", {"quota.code": "PREMIUM_QUOTA_EXHAUSTED"})
|
||||||
"quota.denied", {"quota.code": "PREMIUM_QUOTA_EXHAUSTED"}
|
|
||||||
)
|
|
||||||
if requested_llm_config_id == 0:
|
if requested_llm_config_id == 0:
|
||||||
try:
|
try:
|
||||||
pinned_fb = await resolve_or_get_pinned_llm_config_id(
|
pinned_fb = await resolve_or_get_pinned_llm_config_id(
|
||||||
|
|
@ -396,7 +391,9 @@ async def stream_resume_chat(
|
||||||
|
|
||||||
# --- First SSE frames ---
|
# --- First SSE frames ---
|
||||||
|
|
||||||
for sse in iter_initial_frames(streaming_service, turn_id=stream_result.turn_id):
|
for sse in iter_initial_frames(
|
||||||
|
streaming_service, turn_id=stream_result.turn_id
|
||||||
|
):
|
||||||
yield sse
|
yield sse
|
||||||
|
|
||||||
# --- Assistant-shell persistence + id frame ---
|
# --- Assistant-shell persistence + id frame ---
|
||||||
|
|
@ -517,7 +514,9 @@ async def stream_resume_chat(
|
||||||
fallback_commit_search_space_id=search_space_id,
|
fallback_commit_search_space_id=search_space_id,
|
||||||
fallback_commit_created_by_id=user_id,
|
fallback_commit_created_by_id=user_id,
|
||||||
fallback_commit_filesystem_mode=(
|
fallback_commit_filesystem_mode=(
|
||||||
filesystem_selection.mode if filesystem_selection else FilesystemMode.CLOUD
|
filesystem_selection.mode
|
||||||
|
if filesystem_selection
|
||||||
|
else FilesystemMode.CLOUD
|
||||||
),
|
),
|
||||||
fallback_commit_thread_id=chat_id,
|
fallback_commit_thread_id=chat_id,
|
||||||
runtime_context=runtime_context,
|
runtime_context=runtime_context,
|
||||||
|
|
@ -589,9 +588,7 @@ async def stream_resume_chat(
|
||||||
end_turn(str(chat_id))
|
end_turn(str(chat_id))
|
||||||
|
|
||||||
if premium_reservation is not None and user_id:
|
if premium_reservation is not None and user_id:
|
||||||
await release_premium(
|
await release_premium(reservation=premium_reservation, user_id=user_id)
|
||||||
reservation=premium_reservation, user_id=user_id
|
|
||||||
)
|
|
||||||
|
|
||||||
await close_session_and_clear_ai_responding(session, chat_id)
|
await close_session_and_clear_ai_responding(session, chat_id)
|
||||||
|
|
||||||
|
|
@ -609,13 +606,11 @@ async def stream_resume_chat(
|
||||||
if not busy_error_raised:
|
if not busy_error_raised:
|
||||||
with contextlib.suppress(Exception):
|
with contextlib.suppress(Exception):
|
||||||
end_turn(str(chat_id))
|
end_turn(str(chat_id))
|
||||||
_perf_log.info(
|
_perf_log.info("[stream_resume] end_turn cleanup (chat_id=%s)", chat_id)
|
||||||
"[stream_resume] end_turn cleanup (chat_id=%s)", chat_id
|
|
||||||
)
|
|
||||||
|
|
||||||
agent = llm = connector_service = None # noqa: F841
|
agent = llm = connector_service = None
|
||||||
stream_result = None # noqa: F841
|
stream_result = None
|
||||||
session = None # noqa: F841
|
session = None
|
||||||
|
|
||||||
run_gc_pass(log_prefix="stream_resume", chat_id=chat_id)
|
run_gc_pass(log_prefix="stream_resume", chat_id=chat_id)
|
||||||
close_chat_request_span(
|
close_chat_request_span(
|
||||||
|
|
|
||||||
|
|
@ -47,9 +47,7 @@ async def build_resume_routing(
|
||||||
slice_decisions_by_tool_call,
|
slice_decisions_by_tool_call,
|
||||||
)
|
)
|
||||||
|
|
||||||
parent_state = await agent.aget_state(
|
parent_state = await agent.aget_state({"configurable": {"thread_id": str(chat_id)}})
|
||||||
{"configurable": {"thread_id": str(chat_id)}}
|
|
||||||
)
|
|
||||||
pending = collect_pending_tool_calls(parent_state)
|
pending = collect_pending_tool_calls(parent_state)
|
||||||
_perf_log.info(
|
_perf_log.info(
|
||||||
"[hitl_route] resume_entry chat_id=%s decisions=%d pending_subagents=%d",
|
"[hitl_route] resume_entry chat_id=%s decisions=%d pending_subagents=%d",
|
||||||
|
|
|
||||||
|
|
@ -49,9 +49,7 @@ async def finalize_assistant_message(
|
||||||
was never assigned.
|
was never assigned.
|
||||||
"""
|
"""
|
||||||
if not (
|
if not (
|
||||||
stream_result
|
stream_result and stream_result.turn_id and stream_result.assistant_message_id
|
||||||
and stream_result.turn_id
|
|
||||||
and stream_result.assistant_message_id
|
|
||||||
):
|
):
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -39,9 +39,7 @@ async def close_session_and_clear_ai_responding(
|
||||||
async with shielded_async_session() as fresh_session:
|
async with shielded_async_session() as fresh_session:
|
||||||
await clear_ai_responding(fresh_session, chat_id)
|
await clear_ai_responding(fresh_session, chat_id)
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.warning(
|
logger.warning("Failed to clear AI responding state for thread %s", chat_id)
|
||||||
"Failed to clear AI responding state for thread %s", chat_id
|
|
||||||
)
|
|
||||||
|
|
||||||
with contextlib.suppress(Exception):
|
with contextlib.suppress(Exception):
|
||||||
session.expunge_all()
|
session.expunge_all()
|
||||||
|
|
|
||||||
|
|
@ -41,9 +41,7 @@ class PremiumReservation:
|
||||||
allowed: bool
|
allowed: bool
|
||||||
|
|
||||||
|
|
||||||
def needs_premium_quota(
|
def needs_premium_quota(agent_config: AgentConfig | None, user_id: str | None) -> bool:
|
||||||
agent_config: AgentConfig | None, user_id: str | None
|
|
||||||
) -> bool:
|
|
||||||
return bool(agent_config is not None and user_id and agent_config.is_premium)
|
return bool(agent_config is not None and user_id and agent_config.is_premium)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -61,8 +59,10 @@ async def reserve_premium(
|
||||||
request_id = _uuid.uuid4().hex[:16]
|
request_id = _uuid.uuid4().hex[:16]
|
||||||
litellm_params = agent_config.litellm_params or {}
|
litellm_params = agent_config.litellm_params or {}
|
||||||
base_model = (
|
base_model = (
|
||||||
litellm_params.get("base_model") if isinstance(litellm_params, dict) else None
|
(litellm_params.get("base_model") if isinstance(litellm_params, dict) else None)
|
||||||
) or agent_config.model_name or ""
|
or agent_config.model_name
|
||||||
|
or ""
|
||||||
|
)
|
||||||
reserve_amount_micros = estimate_call_reserve_micros(
|
reserve_amount_micros = estimate_call_reserve_micros(
|
||||||
base_model=base_model,
|
base_model=base_model,
|
||||||
quota_reserve_tokens=agent_config.quota_reserve_tokens,
|
quota_reserve_tokens=agent_config.quota_reserve_tokens,
|
||||||
|
|
|
||||||
|
|
@ -6,8 +6,7 @@ import contextlib
|
||||||
import sys
|
import sys
|
||||||
from typing import Any, Literal
|
from typing import Any, Literal
|
||||||
|
|
||||||
from app.observability import metrics as ot_metrics
|
from app.observability import metrics as ot_metrics, otel as ot
|
||||||
from app.observability import otel as ot
|
|
||||||
|
|
||||||
|
|
||||||
def open_chat_request_span(
|
def open_chat_request_span(
|
||||||
|
|
|
||||||
|
|
@ -15,8 +15,7 @@ from collections.abc import Iterator
|
||||||
from typing import Any, Literal
|
from typing import Any, Literal
|
||||||
|
|
||||||
from app.agents.new_chat.errors import BusyError
|
from app.agents.new_chat.errors import BusyError
|
||||||
from app.observability import metrics as ot_metrics
|
from app.observability import metrics as ot_metrics, otel as ot
|
||||||
from app.observability import otel as ot
|
|
||||||
from app.services.new_streaming_service import VercelStreamingService
|
from app.services.new_streaming_service import VercelStreamingService
|
||||||
from app.tasks.chat.streaming.errors.classifier import classify_stream_exception
|
from app.tasks.chat.streaming.errors.classifier import classify_stream_exception
|
||||||
from app.tasks.chat.streaming.errors.emitter import emit_stream_terminal_error
|
from app.tasks.chat.streaming.errors.emitter import emit_stream_terminal_error
|
||||||
|
|
|
||||||
|
|
@ -72,7 +72,11 @@ def test_extract_returns_none_when_no_assistant_text_is_present() -> None:
|
||||||
anything?" rather than guess whether ``""`` means silence or empty
|
anything?" rather than guess whether ``""`` means silence or empty
|
||||||
output. Empty-string contents are normalized to ``None`` too."""
|
output. Empty-string contents are normalized to ``None`` too."""
|
||||||
no_ai = {"messages": [HumanMessage(content="just a question")]}
|
no_ai = {"messages": [HumanMessage(content="just a question")]}
|
||||||
only_tools = {"messages": [AIMessage(content=[{"type": "tool_use", "name": "x", "input": {}}])]}
|
only_tools = {
|
||||||
|
"messages": [
|
||||||
|
AIMessage(content=[{"type": "tool_use", "name": "x", "input": {}}])
|
||||||
|
]
|
||||||
|
}
|
||||||
empty_string = {"messages": [AIMessage(content=" ")]}
|
empty_string = {"messages": [AIMessage(content=" ")]}
|
||||||
|
|
||||||
assert extract_final_assistant_message(no_ai) is None
|
assert extract_final_assistant_message(no_ai) is None
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,9 @@ async def test_with_retries_returns_result_and_attempts_one_on_first_success() -
|
||||||
assert calls == 1
|
assert calls == 1
|
||||||
|
|
||||||
|
|
||||||
async def test_with_retries_returns_attempt_count_when_succeeding_after_failures() -> None:
|
async def test_with_retries_returns_attempt_count_when_succeeding_after_failures() -> (
|
||||||
|
None
|
||||||
|
):
|
||||||
"""A coroutine that fails twice then succeeds returns ``attempts=3``
|
"""A coroutine that fails twice then succeeds returns ``attempts=3``
|
||||||
(the actual attempt that produced the result). Locks the contract
|
(the actual attempt that produced the result). Locks the contract
|
||||||
that the caller can distinguish first-try success from a recovery."""
|
that the caller can distinguish first-try success from a recovery."""
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,9 @@ from app.automations.schemas.definition.plan_step import PlanStep
|
||||||
pytestmark = pytest.mark.unit
|
pytestmark = pytest.mark.unit
|
||||||
|
|
||||||
|
|
||||||
def test_automation_definition_accepts_minimal_valid_input_with_sensible_defaults() -> None:
|
def test_automation_definition_accepts_minimal_valid_input_with_sensible_defaults() -> (
|
||||||
|
None
|
||||||
|
):
|
||||||
"""A definition with just ``name`` + a one-step ``plan`` is valid and
|
"""A definition with just ``name`` + a one-step ``plan`` is valid and
|
||||||
fills in the rest with safe defaults so users don't have to write
|
fills in the rest with safe defaults so users don't have to write
|
||||||
out every section to get started."""
|
out every section to get started."""
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,9 @@ def test_environment_finalizes_datetime_output_to_iso_string() -> None:
|
||||||
when emitting ``inputs.fired_at`` and other datetime values."""
|
when emitting ``inputs.fired_at`` and other datetime values."""
|
||||||
dt = datetime(2026, 5, 28, 14, 30, tzinfo=UTC)
|
dt = datetime(2026, 5, 28, 14, 30, tzinfo=UTC)
|
||||||
|
|
||||||
assert render_template("{{ moment }}", {"moment": dt}) == "2026-05-28T14:30:00+00:00"
|
assert (
|
||||||
|
render_template("{{ moment }}", {"moment": dt}) == "2026-05-28T14:30:00+00:00"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_environment_finalizes_none_output_to_empty_string() -> None:
|
def test_environment_finalizes_none_output_to_empty_string() -> None:
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,7 @@ def test_action_definition_params_schema_reflects_params_model() -> None:
|
||||||
name="N",
|
name="N",
|
||||||
description="D",
|
description="D",
|
||||||
params_model=_Topic,
|
params_model=_Topic,
|
||||||
build_handler=lambda _ctx: (lambda _p: {}), # type: ignore[arg-type,return-value]
|
build_handler=lambda _ctx: lambda _p: {}, # type: ignore[arg-type,return-value]
|
||||||
)
|
)
|
||||||
|
|
||||||
schema = definition.params_schema
|
schema = definition.params_schema
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,9 @@ class _Params(BaseModel):
|
||||||
|
|
||||||
|
|
||||||
def _trigger(type_: str = "test_trigger") -> TriggerDefinition:
|
def _trigger(type_: str = "test_trigger") -> TriggerDefinition:
|
||||||
return TriggerDefinition(type=type_, description="Test trigger.", params_model=_Params)
|
return TriggerDefinition(
|
||||||
|
type=type_, description="Test trigger.", params_model=_Params
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _action(type_: str = "test_action") -> ActionDefinition:
|
def _action(type_: str = "test_action") -> ActionDefinition:
|
||||||
|
|
@ -38,7 +40,7 @@ def _action(type_: str = "test_action") -> ActionDefinition:
|
||||||
name="Test",
|
name="Test",
|
||||||
description="Test action.",
|
description="Test action.",
|
||||||
params_model=_Params,
|
params_model=_Params,
|
||||||
build_handler=lambda _ctx: (lambda _p: {}), # type: ignore[arg-type,return-value]
|
build_handler=lambda _ctx: lambda _p: {}, # type: ignore[arg-type,return-value]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -45,8 +45,12 @@ def test_compute_next_fire_at_respects_dst_offset_change() -> None:
|
||||||
winter_after = datetime(2026, 2, 15, 0, 0, tzinfo=UTC)
|
winter_after = datetime(2026, 2, 15, 0, 0, tzinfo=UTC)
|
||||||
summer_after = datetime(2026, 4, 15, 0, 0, tzinfo=UTC)
|
summer_after = datetime(2026, 4, 15, 0, 0, tzinfo=UTC)
|
||||||
|
|
||||||
winter_fire = compute_next_fire_at("0 9 * * *", "America/New_York", after=winter_after)
|
winter_fire = compute_next_fire_at(
|
||||||
summer_fire = compute_next_fire_at("0 9 * * *", "America/New_York", after=summer_after)
|
"0 9 * * *", "America/New_York", after=winter_after
|
||||||
|
)
|
||||||
|
summer_fire = compute_next_fire_at(
|
||||||
|
"0 9 * * *", "America/New_York", after=summer_after
|
||||||
|
)
|
||||||
|
|
||||||
assert winter_fire == datetime(2026, 2, 15, 14, 0, tzinfo=UTC)
|
assert winter_fire == datetime(2026, 2, 15, 14, 0, tzinfo=UTC)
|
||||||
assert summer_fire == datetime(2026, 4, 15, 13, 0, tzinfo=UTC)
|
assert summer_fire == datetime(2026, 4, 15, 13, 0, tzinfo=UTC)
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,6 @@ import pytest
|
||||||
|
|
||||||
from app.agents.new_chat.context import SurfSenseContextSchema
|
from app.agents.new_chat.context import SurfSenseContextSchema
|
||||||
from app.services.new_streaming_service import VercelStreamingService
|
from app.services.new_streaming_service import VercelStreamingService
|
||||||
|
|
||||||
from app.tasks.chat.stream_new_chat import (
|
from app.tasks.chat.stream_new_chat import (
|
||||||
stream_new_chat as old_stream_new_chat,
|
stream_new_chat as old_stream_new_chat,
|
||||||
stream_resume_chat as old_stream_resume_chat,
|
stream_resume_chat as old_stream_resume_chat,
|
||||||
|
|
@ -152,7 +151,13 @@ class _FakeSurfsenseDoc:
|
||||||
"user_query, image_urls, docs, expected_title, expected_action",
|
"user_query, image_urls, docs, expected_title, expected_action",
|
||||||
[
|
[
|
||||||
("hello world", None, [], "Understanding your request", "Processing"),
|
("hello world", None, [], "Understanding your request", "Processing"),
|
||||||
("", ["data:image/png;base64,AAA"], [], "Understanding your request", "Processing"),
|
(
|
||||||
|
"",
|
||||||
|
["data:image/png;base64,AAA"],
|
||||||
|
[],
|
||||||
|
"Understanding your request",
|
||||||
|
"Processing",
|
||||||
|
),
|
||||||
("", None, [], "Understanding your request", "Processing"),
|
("", None, [], "Understanding your request", "Processing"),
|
||||||
(
|
(
|
||||||
"doc question",
|
"doc question",
|
||||||
|
|
@ -209,9 +214,10 @@ def test_initial_thinking_step_collapses_many_doc_names() -> None:
|
||||||
|
|
||||||
|
|
||||||
def test_image_capability_passes_without_images() -> None:
|
def test_image_capability_passes_without_images() -> None:
|
||||||
assert check_image_input_capability(
|
assert (
|
||||||
user_image_data_urls=None, agent_config=None
|
check_image_input_capability(user_image_data_urls=None, agent_config=None)
|
||||||
) is None
|
is None
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_image_capability_passes_when_capability_unknown() -> None:
|
def test_image_capability_passes_when_capability_unknown() -> None:
|
||||||
|
|
@ -500,9 +506,7 @@ def test_can_recover_provider_rate_limit_rejects_non_rate_limit_exception() -> N
|
||||||
def test_spawn_set_ai_responding_bg_noop_without_user_id() -> None:
|
def test_spawn_set_ai_responding_bg_noop_without_user_id() -> None:
|
||||||
async def _run() -> set[asyncio.Task]:
|
async def _run() -> set[asyncio.Task]:
|
||||||
background: set[asyncio.Task] = set()
|
background: set[asyncio.Task] = set()
|
||||||
spawn_set_ai_responding_bg(
|
spawn_set_ai_responding_bg(chat_id=1, user_id=None, background_tasks=background)
|
||||||
chat_id=1, user_id=None, background_tasks=background
|
|
||||||
)
|
|
||||||
return background
|
return background
|
||||||
|
|
||||||
bg = asyncio.run(_run())
|
bg = asyncio.run(_run())
|
||||||
|
|
|
||||||
|
|
@ -221,10 +221,7 @@ export default async function FreeHubPage() {
|
||||||
<Separator className="my-12 max-w-4xl mx-auto" />
|
<Separator className="my-12 max-w-4xl mx-auto" />
|
||||||
|
|
||||||
{/* In-content ad: above the model table */}
|
{/* In-content ad: above the model table */}
|
||||||
<aside
|
<aside aria-label="Advertisement" className="max-w-4xl mx-auto mb-8 min-h-[100px]">
|
||||||
aria-label="Advertisement"
|
|
||||||
className="max-w-4xl mx-auto mb-8 min-h-[100px]"
|
|
||||||
>
|
|
||||||
<AdUnit slot={ADSENSE_SLOTS.freeHubInContent} />
|
<AdUnit slot={ADSENSE_SLOTS.freeHubInContent} />
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
|
|
@ -353,10 +350,7 @@ export default async function FreeHubPage() {
|
||||||
<Separator className="my-12 max-w-4xl mx-auto" />
|
<Separator className="my-12 max-w-4xl mx-auto" />
|
||||||
|
|
||||||
{/* In-content ad: after CTA, before FAQ */}
|
{/* In-content ad: after CTA, before FAQ */}
|
||||||
<aside
|
<aside aria-label="Advertisement" className="max-w-3xl mx-auto my-8 min-h-[100px]">
|
||||||
aria-label="Advertisement"
|
|
||||||
className="max-w-3xl mx-auto my-8 min-h-[100px]"
|
|
||||||
>
|
|
||||||
<AdUnit slot={ADSENSE_SLOTS.freeHubBeforeFaq} />
|
<AdUnit slot={ADSENSE_SLOTS.freeHubBeforeFaq} />
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -37,9 +37,9 @@ export default function PrivacyPolicy() {
|
||||||
</p>
|
</p>
|
||||||
<p className="mt-4">
|
<p className="mt-4">
|
||||||
By accessing or using the Service, you acknowledge that you have read and understood
|
By accessing or using the Service, you acknowledge that you have read and understood
|
||||||
this Privacy Policy. If you do not agree with our policies and practices, do not use
|
this Privacy Policy. If you do not agree with our policies and practices, do not use the
|
||||||
the Service. We may modify this policy from time to time; material changes will be
|
Service. We may modify this policy from time to time; material changes will be reflected
|
||||||
reflected by updating the "Last updated" date above.
|
by updating the "Last updated" date above.
|
||||||
</p>
|
</p>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|
@ -71,9 +71,9 @@ export default function PrivacyPolicy() {
|
||||||
Notion, Confluence, GitHub, and others) under the scopes you authorize.
|
Notion, Confluence, GitHub, and others) under the scopes you authorize.
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<strong>Billing Data</strong> includes information necessary to process payments
|
<strong>Billing Data</strong> includes information necessary to process payments (such
|
||||||
(such as transaction identifiers and credit balances). Card details are handled by
|
as transaction identifiers and credit balances). Card details are handled by our
|
||||||
our payment processor and are not stored on our servers.
|
payment processor and are not stored on our servers.
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<strong>Technical Data</strong> includes internet protocol (IP) address, browser type
|
<strong>Technical Data</strong> includes internet protocol (IP) address, browser type
|
||||||
|
|
@ -126,8 +126,8 @@ export default function PrivacyPolicy() {
|
||||||
incidents.
|
incidents.
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
To communicate with you about product updates, security notices, support requests,
|
To communicate with you about product updates, security notices, support requests, and
|
||||||
and (with your consent where required) marketing.
|
(with your consent where required) marketing.
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
To serve and measure advertising on pages where ads are shown (currently, our free
|
To serve and measure advertising on pages where ads are shown (currently, our free
|
||||||
|
|
@ -141,8 +141,8 @@ export default function PrivacyPolicy() {
|
||||||
<h2 className="text-2xl font-semibold mb-4">4. Cookies and Tracking Technologies</h2>
|
<h2 className="text-2xl font-semibold mb-4">4. Cookies and Tracking Technologies</h2>
|
||||||
<p>
|
<p>
|
||||||
We and our partners use cookies, local storage, and similar technologies to operate the
|
We and our partners use cookies, local storage, and similar technologies to operate the
|
||||||
Service, remember your preferences, measure usage, and serve advertising. The
|
Service, remember your preferences, measure usage, and serve advertising. The categories
|
||||||
categories include:
|
include:
|
||||||
</p>
|
</p>
|
||||||
<ul className="list-disc pl-6 my-4 space-y-2">
|
<ul className="list-disc pl-6 my-4 space-y-2">
|
||||||
<li>
|
<li>
|
||||||
|
|
@ -179,9 +179,9 @@ export default function PrivacyPolicy() {
|
||||||
</p>
|
</p>
|
||||||
<ul className="list-disc pl-6 my-4 space-y-2">
|
<ul className="list-disc pl-6 my-4 space-y-2">
|
||||||
<li>
|
<li>
|
||||||
Google, as a third-party vendor, uses cookies (including the DoubleClick DART
|
Google, as a third-party vendor, uses cookies (including the DoubleClick DART cookie)
|
||||||
cookie) to serve ads to you based on your visits to our Service and other websites
|
to serve ads to you based on your visits to our Service and other websites on the
|
||||||
on the Internet.
|
Internet.
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
Google's use of advertising cookies enables it and its partners to serve ads to you
|
Google's use of advertising cookies enables it and its partners to serve ads to you
|
||||||
|
|
@ -195,14 +195,12 @@ export default function PrivacyPolicy() {
|
||||||
<a href="https://www.youronlinechoices.com/">youronlinechoices.com</a> (EU).
|
<a href="https://www.youronlinechoices.com/">youronlinechoices.com</a> (EU).
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
For users in the European Economic Area, the United Kingdom, and Switzerland, we
|
For users in the European Economic Area, the United Kingdom, and Switzerland, we use a
|
||||||
use a Google-certified Consent Management Platform to obtain your consent for
|
Google-certified Consent Management Platform to obtain your consent for personalized
|
||||||
personalized advertising before such cookies are set. You may change or withdraw
|
advertising before such cookies are set. You may change or withdraw your consent at
|
||||||
your consent at any time through the consent banner.
|
any time through the consent banner.
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
We do not knowingly serve personalized advertising to children. See Section 11.
|
|
||||||
</li>
|
</li>
|
||||||
|
<li>We do not knowingly serve personalized advertising to children. See Section 11.</li>
|
||||||
</ul>
|
</ul>
|
||||||
<p className="mt-4">
|
<p className="mt-4">
|
||||||
For more information about how Google uses data when you use our Service, see{" "}
|
For more information about how Google uses data when you use our Service, see{" "}
|
||||||
|
|
@ -217,8 +215,8 @@ export default function PrivacyPolicy() {
|
||||||
<h2 className="text-2xl font-semibold mb-4">6. Data Security</h2>
|
<h2 className="text-2xl font-semibold mb-4">6. Data Security</h2>
|
||||||
<p>
|
<p>
|
||||||
We implement technical and organizational measures designed to protect your personal
|
We implement technical and organizational measures designed to protect your personal
|
||||||
data against accidental loss, unauthorized access, alteration, and disclosure. Access
|
data against accidental loss, unauthorized access, alteration, and disclosure. Access to
|
||||||
to personal data is limited to personnel who need it to operate the Service.
|
personal data is limited to personnel who need it to operate the Service.
|
||||||
</p>
|
</p>
|
||||||
<p className="mt-4">
|
<p className="mt-4">
|
||||||
No system can be guaranteed to be fully secure. We cannot guarantee that personal data
|
No system can be guaranteed to be fully secure. We cannot guarantee that personal data
|
||||||
|
|
@ -232,10 +230,10 @@ export default function PrivacyPolicy() {
|
||||||
<p>
|
<p>
|
||||||
We retain personal data only for as long as necessary to provide the Service and to
|
We retain personal data only for as long as necessary to provide the Service and to
|
||||||
comply with our legal, accounting, and reporting obligations. Account data is retained
|
comply with our legal, accounting, and reporting obligations. Account data is retained
|
||||||
for the life of your account; you can request deletion at any time. Aggregated data
|
for the life of your account; you can request deletion at any time. Aggregated data that
|
||||||
that no longer identifies you may be retained indefinitely for analytics and product
|
no longer identifies you may be retained indefinitely for analytics and product
|
||||||
improvement purposes. Anonymous chat sessions on our free pages are not retained in
|
improvement purposes. Anonymous chat sessions on our free pages are not retained in any
|
||||||
any user-linked database.
|
user-linked database.
|
||||||
</p>
|
</p>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|
@ -243,8 +241,7 @@ export default function PrivacyPolicy() {
|
||||||
<h2 className="text-2xl font-semibold mb-4">8. Third-Party Services</h2>
|
<h2 className="text-2xl font-semibold mb-4">8. Third-Party Services</h2>
|
||||||
<p>
|
<p>
|
||||||
We rely on the following categories of third-party processors and providers to operate
|
We rely on the following categories of third-party processors and providers to operate
|
||||||
the Service. Each is bound by its own privacy policy, which we encourage you to
|
the Service. Each is bound by its own privacy policy, which we encourage you to review:
|
||||||
review:
|
|
||||||
</p>
|
</p>
|
||||||
<ul className="list-disc pl-6 my-4 space-y-2">
|
<ul className="list-disc pl-6 my-4 space-y-2">
|
||||||
<li>
|
<li>
|
||||||
|
|
@ -261,9 +258,9 @@ export default function PrivacyPolicy() {
|
||||||
<strong>Advertising</strong>: Google AdSense (see Section 5).
|
<strong>Advertising</strong>: Google AdSense (see Section 5).
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<strong>Large language model providers</strong>: OpenAI, Anthropic, Google, and
|
<strong>Large language model providers</strong>: OpenAI, Anthropic, Google, and other
|
||||||
other LLM providers process the prompts and content you submit to the Service in
|
LLM providers process the prompts and content you submit to the Service in order to
|
||||||
order to generate responses.
|
generate responses.
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<strong>Integration providers</strong>: When you explicitly connect a third-party
|
<strong>Integration providers</strong>: When you explicitly connect a third-party
|
||||||
|
|
@ -278,9 +275,7 @@ export default function PrivacyPolicy() {
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section className="mb-8">
|
<section className="mb-8">
|
||||||
<h2 className="text-2xl font-semibold mb-4">
|
<h2 className="text-2xl font-semibold mb-4">9. Your Legal Rights (Including GDPR)</h2>
|
||||||
9. Your Legal Rights (Including GDPR)
|
|
||||||
</h2>
|
|
||||||
<p>
|
<p>
|
||||||
Subject to applicable law, you have the following rights in relation to your personal
|
Subject to applicable law, you have the following rights in relation to your personal
|
||||||
data:
|
data:
|
||||||
|
|
@ -314,17 +309,17 @@ export default function PrivacyPolicy() {
|
||||||
</p>
|
</p>
|
||||||
<ul className="list-disc pl-6 my-4 space-y-2">
|
<ul className="list-disc pl-6 my-4 space-y-2">
|
||||||
<li>
|
<li>
|
||||||
The right to know what categories of personal information we have collected about
|
The right to know what categories of personal information we have collected about you
|
||||||
you and how it is used and shared.
|
and how it is used and shared.
|
||||||
</li>
|
</li>
|
||||||
<li>The right to delete personal information we have collected from you.</li>
|
<li>The right to delete personal information we have collected from you.</li>
|
||||||
<li>The right to correct inaccurate personal information.</li>
|
<li>The right to correct inaccurate personal information.</li>
|
||||||
<li>
|
<li>
|
||||||
The right to opt out of the "sale" or "sharing" of personal information for
|
The right to opt out of the "sale" or "sharing" of personal information for
|
||||||
cross-context behavioral advertising. We do not sell personal data; however,
|
cross-context behavioral advertising. We do not sell personal data; however,
|
||||||
advertising cookies set by Google AdSense may be considered "sharing" under
|
advertising cookies set by Google AdSense may be considered "sharing" under California
|
||||||
California law. To opt out, you can use the consent controls described in Section 5
|
law. To opt out, you can use the consent controls described in Section 5 or enable a
|
||||||
or enable a Global Privacy Control (GPC) signal in your browser, which we honor.
|
Global Privacy Control (GPC) signal in your browser, which we honor.
|
||||||
</li>
|
</li>
|
||||||
<li>The right not to be discriminated against for exercising your privacy rights.</li>
|
<li>The right not to be discriminated against for exercising your privacy rights.</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
@ -337,33 +332,32 @@ export default function PrivacyPolicy() {
|
||||||
<h2 className="text-2xl font-semibold mb-4">11. Children's Privacy</h2>
|
<h2 className="text-2xl font-semibold mb-4">11. Children's Privacy</h2>
|
||||||
<p>
|
<p>
|
||||||
The Service is not directed to children under 13 (or under 16 in the EEA, UK, and
|
The Service is not directed to children under 13 (or under 16 in the EEA, UK, and
|
||||||
Switzerland). We do not knowingly collect personal data from children. If you believe
|
Switzerland). We do not knowingly collect personal data from children. If you believe a
|
||||||
a child has provided us with personal data, please contact us and we will take steps
|
child has provided us with personal data, please contact us and we will take steps to
|
||||||
to delete it. We do not knowingly serve personalized advertising to children.
|
delete it. We do not knowingly serve personalized advertising to children.
|
||||||
</p>
|
</p>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section className="mb-8">
|
<section className="mb-8">
|
||||||
<h2 className="text-2xl font-semibold mb-4">12. Changes to This Policy</h2>
|
<h2 className="text-2xl font-semibold mb-4">12. Changes to This Policy</h2>
|
||||||
<p>
|
<p>
|
||||||
We may update this Privacy Policy from time to time to reflect changes in our
|
We may update this Privacy Policy from time to time to reflect changes in our practices,
|
||||||
practices, technology, legal requirements, or for other operational reasons. When we
|
technology, legal requirements, or for other operational reasons. When we make material
|
||||||
make material changes, we will update the "Last updated" date at the top of this page
|
changes, we will update the "Last updated" date at the top of this page and, where
|
||||||
and, where appropriate, provide additional notice (such as an in-product notification
|
appropriate, provide additional notice (such as an in-product notification or email).
|
||||||
or email). Your continued use of the Service after the updated policy becomes
|
Your continued use of the Service after the updated policy becomes effective constitutes
|
||||||
effective constitutes your acceptance of the revised policy.
|
your acceptance of the revised policy.
|
||||||
</p>
|
</p>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section className="mb-8">
|
<section className="mb-8">
|
||||||
<h2 className="text-2xl font-semibold mb-4">13. Contact Us</h2>
|
<h2 className="text-2xl font-semibold mb-4">13. Contact Us</h2>
|
||||||
<p>
|
<p>
|
||||||
If you have questions about this Privacy Policy or our privacy practices, or if you
|
If you have questions about this Privacy Policy or our privacy practices, or if you want
|
||||||
want to exercise any of your rights, please contact us at:
|
to exercise any of your rights, please contact us at:
|
||||||
</p>
|
</p>
|
||||||
<p className="mt-2">
|
<p className="mt-2">
|
||||||
<strong>Email:</strong>{" "}
|
<strong>Email:</strong> <a href="mailto:rohan@surfsense.com">rohan@surfsense.com</a>
|
||||||
<a href="mailto:rohan@surfsense.com">rohan@surfsense.com</a>
|
|
||||||
</p>
|
</p>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
import { mustGetQuery } from "@rocicorp/zero";
|
import { mustGetQuery } from "@rocicorp/zero";
|
||||||
import { handleQueryRequest } from "@rocicorp/zero/server";
|
import { handleQueryRequest } from "@rocicorp/zero/server";
|
||||||
import { NextResponse } from "next/server";
|
import { NextResponse } from "next/server";
|
||||||
|
import { BACKEND_URL } from "@/lib/env-config";
|
||||||
import type { Context } from "@/types/zero";
|
import type { Context } from "@/types/zero";
|
||||||
import { queries } from "@/zero/queries";
|
import { queries } from "@/zero/queries";
|
||||||
import { schema } from "@/zero/schema";
|
import { schema } from "@/zero/schema";
|
||||||
import { BACKEND_URL } from "@/lib/env-config";
|
|
||||||
|
|
||||||
const backendURL = BACKEND_URL;
|
const backendURL = BACKEND_URL;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,10 +16,7 @@ interface AutomationEditContentProps {
|
||||||
* structure but gates on ``canUpdate`` instead of ``canRead``: a user who
|
* structure but gates on ``canUpdate`` instead of ``canRead``: a user who
|
||||||
* can read but not update is bounced to the access-denied panel.
|
* can read but not update is bounced to the access-denied panel.
|
||||||
*/
|
*/
|
||||||
export function AutomationEditContent({
|
export function AutomationEditContent({ searchSpaceId, automationId }: AutomationEditContentProps) {
|
||||||
searchSpaceId,
|
|
||||||
automationId,
|
|
||||||
}: AutomationEditContentProps) {
|
|
||||||
const perms = useAutomationPermissions();
|
const perms = useAutomationPermissions();
|
||||||
const validId = Number.isInteger(automationId) && automationId > 0;
|
const validId = Number.isInteger(automationId) && automationId > 0;
|
||||||
const { data: automation, isLoading, error } = useAutomation(validId ? automationId : undefined);
|
const { data: automation, isLoading, error } = useAutomation(validId ? automationId : undefined);
|
||||||
|
|
|
||||||
|
|
@ -9,10 +9,7 @@ import { JsonView } from "@/components/json-view";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import { Spinner } from "@/components/ui/spinner";
|
import { Spinner } from "@/components/ui/spinner";
|
||||||
import {
|
import { type Automation, automationUpdateRequest } from "@/contracts/types/automation.types";
|
||||||
type Automation,
|
|
||||||
automationUpdateRequest,
|
|
||||||
} from "@/contracts/types/automation.types";
|
|
||||||
|
|
||||||
interface AutomationEditFormProps {
|
interface AutomationEditFormProps {
|
||||||
automation: Automation;
|
automation: Automation;
|
||||||
|
|
|
||||||
|
|
@ -69,11 +69,11 @@ import { documentsApiService } from "@/lib/apis/documents-api.service";
|
||||||
import { getBearerToken } from "@/lib/auth-utils";
|
import { getBearerToken } from "@/lib/auth-utils";
|
||||||
import { type ChatFlow, classifyChatError } from "@/lib/chat/chat-error-classifier";
|
import { type ChatFlow, classifyChatError } from "@/lib/chat/chat-error-classifier";
|
||||||
import { tagPreAcceptSendFailure, toHttpResponseError } from "@/lib/chat/chat-request-errors";
|
import { tagPreAcceptSendFailure, toHttpResponseError } from "@/lib/chat/chat-request-errors";
|
||||||
|
import { getMentionDocKey } from "@/lib/chat/mention-doc-key";
|
||||||
import {
|
import {
|
||||||
convertToThreadMessage,
|
convertToThreadMessage,
|
||||||
reconcileInterruptedAssistantMessages,
|
reconcileInterruptedAssistantMessages,
|
||||||
} from "@/lib/chat/message-utils";
|
} from "@/lib/chat/message-utils";
|
||||||
import { getMentionDocKey } from "@/lib/chat/mention-doc-key";
|
|
||||||
import {
|
import {
|
||||||
isPodcastGenerating,
|
isPodcastGenerating,
|
||||||
looksLikePodcastRequest,
|
looksLikePodcastRequest,
|
||||||
|
|
@ -110,6 +110,7 @@ import {
|
||||||
extractUserTurnForNewChatApi,
|
extractUserTurnForNewChatApi,
|
||||||
type NewChatUserImagePayload,
|
type NewChatUserImagePayload,
|
||||||
} from "@/lib/chat/user-turn-api-parts";
|
} from "@/lib/chat/user-turn-api-parts";
|
||||||
|
import { BACKEND_URL } from "@/lib/env-config";
|
||||||
import { NotFoundError } from "@/lib/error";
|
import { NotFoundError } from "@/lib/error";
|
||||||
import {
|
import {
|
||||||
trackChatBlocked,
|
trackChatBlocked,
|
||||||
|
|
@ -119,7 +120,7 @@ import {
|
||||||
trackChatResponseReceived,
|
trackChatResponseReceived,
|
||||||
} from "@/lib/posthog/events";
|
} from "@/lib/posthog/events";
|
||||||
import Loading from "../loading";
|
import Loading from "../loading";
|
||||||
import { BACKEND_URL } from "@/lib/env-config";
|
|
||||||
const MobileEditorPanel = dynamic(
|
const MobileEditorPanel = dynamic(
|
||||||
() =>
|
() =>
|
||||||
import("@/components/editor-panel/editor-panel").then((m) => ({
|
import("@/components/editor-panel/editor-panel").then((m) => ({
|
||||||
|
|
@ -1977,14 +1978,12 @@ export default function NewChatPage() {
|
||||||
mentioned_folder_ids: regenerateFolderIds.length > 0 ? regenerateFolderIds : undefined,
|
mentioned_folder_ids: regenerateFolderIds.length > 0 ? regenerateFolderIds : undefined,
|
||||||
mentioned_connector_ids:
|
mentioned_connector_ids:
|
||||||
regenerateConnectors.length > 0 ? regenerateConnectors.map((d) => d.id) : undefined,
|
regenerateConnectors.length > 0 ? regenerateConnectors.map((d) => d.id) : undefined,
|
||||||
mentioned_connectors:
|
mentioned_connectors: regenerateConnectors.length > 0 ? regenerateConnectors : undefined,
|
||||||
regenerateConnectors.length > 0 ? regenerateConnectors : undefined,
|
|
||||||
// Full mention metadata for the regenerate-specific
|
// Full mention metadata for the regenerate-specific
|
||||||
// source list. Only meaningful for edit (the BE only
|
// source list. Only meaningful for edit (the BE only
|
||||||
// re-persists a user row when ``user_query`` is set);
|
// re-persists a user row when ``user_query`` is set);
|
||||||
// reload reuses the original turn's mentioned_documents.
|
// reload reuses the original turn's mentioned_documents.
|
||||||
mentioned_documents:
|
mentioned_documents: sourceMentionedDocs.length > 0 ? sourceMentionedDocs : undefined,
|
||||||
sourceMentionedDocs.length > 0 ? sourceMentionedDocs : undefined,
|
|
||||||
};
|
};
|
||||||
if (isEdit) {
|
if (isEdit) {
|
||||||
requestBody.user_images = editExtras?.userImages ?? [];
|
requestBody.user_images = editExtras?.userImages ?? [];
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,7 @@ import {
|
||||||
deleteMemberMutationAtom,
|
deleteMemberMutationAtom,
|
||||||
updateMemberMutationAtom,
|
updateMemberMutationAtom,
|
||||||
} from "@/atoms/members/members-mutation.atoms";
|
} from "@/atoms/members/members-mutation.atoms";
|
||||||
import { membersAtom, myAccessAtom, canPerform } from "@/atoms/members/members-query.atoms";
|
import { canPerform, membersAtom, myAccessAtom } from "@/atoms/members/members-query.atoms";
|
||||||
import {
|
import {
|
||||||
AlertDialog,
|
AlertDialog,
|
||||||
AlertDialogAction,
|
AlertDialogAction,
|
||||||
|
|
|
||||||
|
|
@ -13,8 +13,8 @@ import {
|
||||||
CheckIcon,
|
CheckIcon,
|
||||||
ClipboardPaste,
|
ClipboardPaste,
|
||||||
CopyIcon,
|
CopyIcon,
|
||||||
DownloadIcon,
|
|
||||||
Dot,
|
Dot,
|
||||||
|
DownloadIcon,
|
||||||
ExternalLink,
|
ExternalLink,
|
||||||
Globe,
|
Globe,
|
||||||
MessageCircleReply,
|
MessageCircleReply,
|
||||||
|
|
|
||||||
|
|
@ -6,14 +6,13 @@ import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { EnumConnectorName } from "@/contracts/enums/connector";
|
import { EnumConnectorName } from "@/contracts/enums/connector";
|
||||||
import { useApiKey } from "@/hooks/use-api-key";
|
import { useApiKey } from "@/hooks/use-api-key";
|
||||||
|
import { BACKEND_URL } from "@/lib/env-config";
|
||||||
import { getConnectorBenefits } from "../connector-benefits";
|
import { getConnectorBenefits } from "../connector-benefits";
|
||||||
import type { ConnectFormProps } from "../index";
|
import type { ConnectFormProps } from "../index";
|
||||||
import { BACKEND_URL } from "@/lib/env-config";
|
|
||||||
|
|
||||||
const PLUGIN_RELEASES_URL =
|
const PLUGIN_RELEASES_URL =
|
||||||
"https://github.com/MODSetter/SurfSense/releases?q=obsidian&expanded=true";
|
"https://github.com/MODSetter/SurfSense/releases?q=obsidian&expanded=true";
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Obsidian connect form for the plugin-only architecture.
|
* Obsidian connect form for the plugin-only architecture.
|
||||||
*
|
*
|
||||||
|
|
|
||||||
|
|
@ -9,8 +9,8 @@ import { Button } from "@/components/ui/button";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { authenticatedFetch } from "@/lib/auth-utils";
|
import { authenticatedFetch } from "@/lib/auth-utils";
|
||||||
import type { ConnectorConfigProps } from "../index";
|
|
||||||
import { BACKEND_URL } from "@/lib/env-config";
|
import { BACKEND_URL } from "@/lib/env-config";
|
||||||
|
import type { ConnectorConfigProps } from "../index";
|
||||||
export interface CirclebackConfigProps extends ConnectorConfigProps {
|
export interface CirclebackConfigProps extends ConnectorConfigProps {
|
||||||
onNameChange?: (name: string) => void;
|
onNameChange?: (name: string) => void;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ import { LIVE_CONNECTOR_TYPES } from "../../constants/connector-constants";
|
||||||
import { getConnectorDisplayName } from "../../tabs/all-connectors-tab";
|
import { getConnectorDisplayName } from "../../tabs/all-connectors-tab";
|
||||||
import { MCPServiceConfig } from "../components/mcp-service-config";
|
import { MCPServiceConfig } from "../components/mcp-service-config";
|
||||||
import { getConnectorConfigComponent } from "../index";
|
import { getConnectorConfigComponent } from "../index";
|
||||||
|
|
||||||
const VISION_LLM_CONNECTOR_TYPES = new Set<SearchSourceConnector["connector_type"]>([
|
const VISION_LLM_CONNECTOR_TYPES = new Set<SearchSourceConnector["connector_type"]>([
|
||||||
EnumConnectorName.GOOGLE_DRIVE_CONNECTOR,
|
EnumConnectorName.GOOGLE_DRIVE_CONNECTOR,
|
||||||
EnumConnectorName.COMPOSIO_GOOGLE_DRIVE_CONNECTOR,
|
EnumConnectorName.COMPOSIO_GOOGLE_DRIVE_CONNECTOR,
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ import { cn } from "@/lib/utils";
|
||||||
import { LIVE_CONNECTOR_TYPES } from "../constants/connector-constants";
|
import { LIVE_CONNECTOR_TYPES } from "../constants/connector-constants";
|
||||||
import { useConnectorStatus } from "../hooks/use-connector-status";
|
import { useConnectorStatus } from "../hooks/use-connector-status";
|
||||||
import { getConnectorDisplayName } from "../tabs/all-connectors-tab";
|
import { getConnectorDisplayName } from "../tabs/all-connectors-tab";
|
||||||
|
|
||||||
interface ConnectorAccountsListViewProps {
|
interface ConnectorAccountsListViewProps {
|
||||||
connectorType: string;
|
connectorType: string;
|
||||||
connectorTitle: string;
|
connectorTitle: string;
|
||||||
|
|
|
||||||
|
|
@ -97,7 +97,12 @@ interface InlineMentionEditorProps {
|
||||||
onActionClose?: () => void;
|
onActionClose?: () => void;
|
||||||
onSubmit?: () => void;
|
onSubmit?: () => void;
|
||||||
onChange?: (text: string, docs: MentionedDocument[]) => void;
|
onChange?: (text: string, docs: MentionedDocument[]) => void;
|
||||||
onDocumentRemove?: (docId: number, docType?: string, kind?: MentionKind, connectorType?: string) => void;
|
onDocumentRemove?: (
|
||||||
|
docId: number,
|
||||||
|
docType?: string,
|
||||||
|
kind?: MentionKind,
|
||||||
|
connectorType?: string
|
||||||
|
) => void;
|
||||||
onKeyDown?: (e: React.KeyboardEvent) => void;
|
onKeyDown?: (e: React.KeyboardEvent) => void;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
className?: string;
|
className?: string;
|
||||||
|
|
@ -171,9 +176,10 @@ const MentionElement: FC<PlateElementProps<MentionElementNode>> = ({
|
||||||
{isFolder ? (
|
{isFolder ? (
|
||||||
<FolderIcon className="h-3 w-3" />
|
<FolderIcon className="h-3 w-3" />
|
||||||
) : isConnector ? (
|
) : isConnector ? (
|
||||||
getConnectorIcon(element.connector_type ?? element.document_type ?? "UNKNOWN", "h-3 w-3") ?? (
|
(getConnectorIcon(
|
||||||
<PlugIcon className="h-3 w-3" />
|
element.connector_type ?? element.document_type ?? "UNKNOWN",
|
||||||
)
|
"h-3 w-3"
|
||||||
|
) ?? <PlugIcon className="h-3 w-3" />)
|
||||||
) : (
|
) : (
|
||||||
getConnectorIcon(element.document_type ?? "UNKNOWN", "h-3 w-3")
|
getConnectorIcon(element.document_type ?? "UNKNOWN", "h-3 w-3")
|
||||||
)}
|
)}
|
||||||
|
|
@ -357,7 +363,11 @@ function getSelectionAnchorRect(root: HTMLElement | null): SuggestionAnchorRect
|
||||||
const rect = range.getClientRects()[0] ?? range.getBoundingClientRect();
|
const rect = range.getClientRects()[0] ?? range.getBoundingClientRect();
|
||||||
if (rect.width > 0 || rect.height > 0) return rectToAnchor(rect);
|
if (rect.width > 0 || rect.height > 0) return rectToAnchor(rect);
|
||||||
|
|
||||||
if (range.collapsed && range.startContainer.nodeType === Node.TEXT_NODE && range.startOffset > 0) {
|
if (
|
||||||
|
range.collapsed &&
|
||||||
|
range.startContainer.nodeType === Node.TEXT_NODE &&
|
||||||
|
range.startOffset > 0
|
||||||
|
) {
|
||||||
const fallbackRange = range.cloneRange();
|
const fallbackRange = range.cloneRange();
|
||||||
fallbackRange.setStart(range.startContainer, range.startOffset - 1);
|
fallbackRange.setStart(range.startContainer, range.startOffset - 1);
|
||||||
fallbackRange.setEnd(range.startContainer, range.startOffset);
|
fallbackRange.setEnd(range.startContainer, range.startOffset);
|
||||||
|
|
|
||||||
|
|
@ -68,11 +68,6 @@ import {
|
||||||
import { TooltipIconButton } from "@/components/assistant-ui/tooltip-icon-button";
|
import { TooltipIconButton } from "@/components/assistant-ui/tooltip-icon-button";
|
||||||
import { UserMessage } from "@/components/assistant-ui/user-message";
|
import { UserMessage } from "@/components/assistant-ui/user-message";
|
||||||
import { ComposerSuggestionPopoverContent } from "@/components/new-chat/composer-suggestion-popup";
|
import { ComposerSuggestionPopoverContent } from "@/components/new-chat/composer-suggestion-popup";
|
||||||
import {
|
|
||||||
DocumentMentionPicker,
|
|
||||||
promoteRecentMention,
|
|
||||||
type DocumentMentionPickerRef,
|
|
||||||
} from "../new-chat/document-mention-picker";
|
|
||||||
import { PromptPicker, type PromptPickerRef } from "@/components/new-chat/prompt-picker";
|
import { PromptPicker, type PromptPickerRef } from "@/components/new-chat/prompt-picker";
|
||||||
import { Avatar, AvatarFallback, AvatarGroup } from "@/components/ui/avatar";
|
import { Avatar, AvatarFallback, AvatarGroup } from "@/components/ui/avatar";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
|
|
@ -112,6 +107,11 @@ import { captureDisplayToPngDataUrl } from "@/lib/chat/display-media-capture";
|
||||||
import { getMentionDocKey } from "@/lib/chat/mention-doc-key";
|
import { getMentionDocKey } from "@/lib/chat/mention-doc-key";
|
||||||
import { slideoutOpenedTickAtom } from "@/lib/layout-events";
|
import { slideoutOpenedTickAtom } from "@/lib/layout-events";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
import {
|
||||||
|
DocumentMentionPicker,
|
||||||
|
type DocumentMentionPickerRef,
|
||||||
|
promoteRecentMention,
|
||||||
|
} from "../new-chat/document-mention-picker";
|
||||||
|
|
||||||
const COMPOSER_PLACEHOLDER = "Ask anything, type / for prompts, type @ to mention docs";
|
const COMPOSER_PLACEHOLDER = "Ask anything, type / for prompts, type @ to mention docs";
|
||||||
|
|
||||||
|
|
@ -601,7 +601,8 @@ const Composer: FC = () => {
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleActionTrigger = useCallback((trigger: SuggestionTriggerInfo) => {
|
const handleActionTrigger = useCallback(
|
||||||
|
(trigger: SuggestionTriggerInfo) => {
|
||||||
const anchorPoint = getComposerSuggestionAnchorPoint(
|
const anchorPoint = getComposerSuggestionAnchorPoint(
|
||||||
trigger.anchorRect,
|
trigger.anchorRect,
|
||||||
clipboardInitialText ? "bottom" : "top"
|
clipboardInitialText ? "bottom" : "top"
|
||||||
|
|
@ -615,7 +616,9 @@ const Composer: FC = () => {
|
||||||
setSuggestionAnchorPoint((current) => current ?? anchorPoint);
|
setSuggestionAnchorPoint((current) => current ?? anchorPoint);
|
||||||
setShowPromptPicker(true);
|
setShowPromptPicker(true);
|
||||||
setActionQuery(trigger.query);
|
setActionQuery(trigger.query);
|
||||||
}, [clipboardInitialText]);
|
},
|
||||||
|
[clipboardInitialText]
|
||||||
|
);
|
||||||
|
|
||||||
const handleActionClose = useCallback(() => {
|
const handleActionClose = useCallback(() => {
|
||||||
if (showPromptPicker) {
|
if (showPromptPicker) {
|
||||||
|
|
@ -754,7 +757,12 @@ const Composer: FC = () => {
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const handleDocumentRemove = useCallback(
|
const handleDocumentRemove = useCallback(
|
||||||
(docId: number, docType?: string, kind?: "doc" | "folder" | "connector", connectorType?: string) => {
|
(
|
||||||
|
docId: number,
|
||||||
|
docType?: string,
|
||||||
|
kind?: "doc" | "folder" | "connector",
|
||||||
|
connectorType?: string
|
||||||
|
) => {
|
||||||
setMentionedDocuments((prev) => {
|
setMentionedDocuments((prev) => {
|
||||||
const removedKey = getMentionDocKey({
|
const removedKey = getMentionDocKey({
|
||||||
id: docId,
|
id: docId,
|
||||||
|
|
@ -768,7 +776,8 @@ const Composer: FC = () => {
|
||||||
[setMentionedDocuments]
|
[setMentionedDocuments]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleDocumentsMention = useCallback((mentions: MentionedDocumentInfo[]) => {
|
const handleDocumentsMention = useCallback(
|
||||||
|
(mentions: MentionedDocumentInfo[]) => {
|
||||||
const parsedSearchSpaceId = Number(search_space_id);
|
const parsedSearchSpaceId = Number(search_space_id);
|
||||||
const editorMentionedDocs = editorRef.current?.getMentionedDocuments() ?? [];
|
const editorMentionedDocs = editorRef.current?.getMentionedDocuments() ?? [];
|
||||||
const editorDocKeys = new Set(editorMentionedDocs.map((doc) => getMentionDocKey(doc)));
|
const editorDocKeys = new Set(editorMentionedDocs.map((doc) => getMentionDocKey(doc)));
|
||||||
|
|
@ -788,7 +797,9 @@ const Composer: FC = () => {
|
||||||
// onChange — no second write path here.
|
// onChange — no second write path here.
|
||||||
setMentionQuery("");
|
setMentionQuery("");
|
||||||
setSuggestionAnchorPoint(null);
|
setSuggestionAnchorPoint(null);
|
||||||
}, [search_space_id]);
|
},
|
||||||
|
[search_space_id]
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const editor = editorRef.current;
|
const editor = editorRef.current;
|
||||||
|
|
|
||||||
|
|
@ -104,9 +104,9 @@ const UserTextPart: FC = () => {
|
||||||
const icon = isFolder ? (
|
const icon = isFolder ? (
|
||||||
<FolderIcon className="size-3.5" />
|
<FolderIcon className="size-3.5" />
|
||||||
) : isConnector ? (
|
) : isConnector ? (
|
||||||
getConnectorIcon(segment.doc.connector_type, "size-3.5") ?? (
|
(getConnectorIcon(segment.doc.connector_type, "size-3.5") ?? (
|
||||||
<Plug className="size-3.5" />
|
<Plug className="size-3.5" />
|
||||||
)
|
))
|
||||||
) : (
|
) : (
|
||||||
getConnectorIcon(segment.doc.document_type ?? "UNKNOWN", "size-3.5")
|
getConnectorIcon(segment.doc.document_type ?? "UNKNOWN", "size-3.5")
|
||||||
);
|
);
|
||||||
|
|
@ -123,7 +123,9 @@ const UserTextPart: FC = () => {
|
||||||
: segment.doc.title
|
: segment.doc.title
|
||||||
}
|
}
|
||||||
onClick={
|
onClick={
|
||||||
isFolder || isConnector ? undefined : () => handleOpenDoc(segment.doc.id, segment.doc.title)
|
isFolder || isConnector
|
||||||
|
? undefined
|
||||||
|
: () => handleOpenDoc(segment.doc.id, segment.doc.title)
|
||||||
}
|
}
|
||||||
className="mx-0.5"
|
className="mx-0.5"
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,7 @@ import { useElectronAPI } from "@/hooks/use-platform";
|
||||||
import { authenticatedFetch, getBearerToken, redirectToLogin } from "@/lib/auth-utils";
|
import { authenticatedFetch, getBearerToken, redirectToLogin } from "@/lib/auth-utils";
|
||||||
import { inferMonacoLanguageFromPath } from "@/lib/editor-language";
|
import { inferMonacoLanguageFromPath } from "@/lib/editor-language";
|
||||||
import { BACKEND_URL } from "@/lib/env-config";
|
import { BACKEND_URL } from "@/lib/env-config";
|
||||||
|
|
||||||
const PlateEditor = dynamic(
|
const PlateEditor = dynamic(
|
||||||
() => import("@/components/editor/plate-editor").then((m) => ({ default: m.PlateEditor })),
|
() => import("@/components/editor/plate-editor").then((m) => ({ default: m.PlateEditor })),
|
||||||
{ ssr: false, loading: () => <EditorPanelSkeleton /> }
|
{ ssr: false, loading: () => <EditorPanelSkeleton /> }
|
||||||
|
|
|
||||||
|
|
@ -6,11 +6,12 @@ import { Button } from "@/components/ui/button";
|
||||||
import type { AnonModel, AnonQuotaResponse } from "@/contracts/types/anonymous-chat.types";
|
import type { AnonModel, AnonQuotaResponse } from "@/contracts/types/anonymous-chat.types";
|
||||||
import { anonymousChatApiService } from "@/lib/apis/anonymous-chat-api.service";
|
import { anonymousChatApiService } from "@/lib/apis/anonymous-chat-api.service";
|
||||||
import { readSSEStream } from "@/lib/chat/streaming-state";
|
import { readSSEStream } from "@/lib/chat/streaming-state";
|
||||||
|
import { BACKEND_URL } from "@/lib/env-config";
|
||||||
import { trackAnonymousChatMessageSent } from "@/lib/posthog/events";
|
import { trackAnonymousChatMessageSent } from "@/lib/posthog/events";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { QuotaBar } from "./quota-bar";
|
import { QuotaBar } from "./quota-bar";
|
||||||
import { QuotaWarningBanner } from "./quota-warning-banner";
|
import { QuotaWarningBanner } from "./quota-warning-banner";
|
||||||
import { BACKEND_URL } from "@/lib/env-config";
|
|
||||||
interface Message {
|
interface Message {
|
||||||
id: string;
|
id: string;
|
||||||
role: "user" | "assistant";
|
role: "user" | "assistant";
|
||||||
|
|
@ -80,9 +81,7 @@ export function AnonymousChat({ model }: AnonymousChatProps) {
|
||||||
content: m.content,
|
content: m.content,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const response = await fetch(
|
const response = await fetch(`${BACKEND_URL}/api/v1/public/anon-chat/stream`, {
|
||||||
`${BACKEND_URL}/api/v1/public/anon-chat/stream`,
|
|
||||||
{
|
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
credentials: "include",
|
credentials: "include",
|
||||||
|
|
@ -91,8 +90,7 @@ export function AnonymousChat({ model }: AnonymousChatProps) {
|
||||||
messages: chatHistory,
|
messages: chatHistory,
|
||||||
}),
|
}),
|
||||||
signal: controller.signal,
|
signal: controller.signal,
|
||||||
}
|
});
|
||||||
);
|
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
if (response.status === 429) {
|
if (response.status === 429) {
|
||||||
|
|
|
||||||
|
|
@ -193,11 +193,7 @@ export function SearchSpaceAvatar({
|
||||||
|
|
||||||
// If delete or settings handlers are provided, expose them through a dropdown menu.
|
// If delete or settings handlers are provided, expose them through a dropdown menu.
|
||||||
if (onDelete || onSettings) {
|
if (onDelete || onSettings) {
|
||||||
const trigger = (
|
const trigger = <DropdownMenuTrigger asChild>{avatarButton(true)}</DropdownMenuTrigger>;
|
||||||
<DropdownMenuTrigger asChild>
|
|
||||||
{avatarButton(true)}
|
|
||||||
</DropdownMenuTrigger>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DropdownMenu open={menuOpen} onOpenChange={setMenuOpen}>
|
<DropdownMenu open={menuOpen} onOpenChange={setMenuOpen}>
|
||||||
|
|
|
||||||
|
|
@ -78,11 +78,11 @@ import { foldersApiService } from "@/lib/apis/folders-api.service";
|
||||||
import { searchSpacesApiService } from "@/lib/apis/search-spaces-api.service";
|
import { searchSpacesApiService } from "@/lib/apis/search-spaces-api.service";
|
||||||
import { authenticatedFetch } from "@/lib/auth-utils";
|
import { authenticatedFetch } from "@/lib/auth-utils";
|
||||||
import { getMentionDocKey } from "@/lib/chat/mention-doc-key";
|
import { getMentionDocKey } from "@/lib/chat/mention-doc-key";
|
||||||
|
import { BACKEND_URL } from "@/lib/env-config";
|
||||||
import { uploadFolderScan } from "@/lib/folder-sync-upload";
|
import { uploadFolderScan } from "@/lib/folder-sync-upload";
|
||||||
import { getSupportedExtensionsSet } from "@/lib/supported-extensions";
|
import { getSupportedExtensionsSet } from "@/lib/supported-extensions";
|
||||||
import { queries } from "@/zero/queries/index";
|
import { queries } from "@/zero/queries/index";
|
||||||
import { SidebarSlideOutPanel } from "./SidebarSlideOutPanel";
|
import { SidebarSlideOutPanel } from "./SidebarSlideOutPanel";
|
||||||
import { BACKEND_URL } from "@/lib/env-config";
|
|
||||||
|
|
||||||
const DesktopLocalTabContent = dynamic(
|
const DesktopLocalTabContent = dynamic(
|
||||||
() => import("./DesktopLocalTabContent").then((mod) => mod.DesktopLocalTabContent),
|
() => import("./DesktopLocalTabContent").then((mod) => mod.DesktopLocalTabContent),
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ import { Button } from "@/components/ui/button";
|
||||||
import { Spinner } from "@/components/ui/spinner";
|
import { Spinner } from "@/components/ui/spinner";
|
||||||
import { authenticatedFetch, getBearerToken, redirectToLogin } from "@/lib/auth-utils";
|
import { authenticatedFetch, getBearerToken, redirectToLogin } from "@/lib/auth-utils";
|
||||||
import { BACKEND_URL } from "@/lib/env-config";
|
import { BACKEND_URL } from "@/lib/env-config";
|
||||||
|
|
||||||
const LARGE_DOCUMENT_THRESHOLD = 2 * 1024 * 1024; // 2MB
|
const LARGE_DOCUMENT_THRESHOLD = 2 * 1024 * 1024; // 2MB
|
||||||
|
|
||||||
interface DocumentContent {
|
interface DocumentContent {
|
||||||
|
|
|
||||||
|
|
@ -117,7 +117,10 @@ const ComposerSuggestionItem = React.forwardRef<
|
||||||
));
|
));
|
||||||
ComposerSuggestionItem.displayName = "ComposerSuggestionItem";
|
ComposerSuggestionItem.displayName = "ComposerSuggestionItem";
|
||||||
|
|
||||||
function ComposerSuggestionSeparator({ className, ...props }: React.ComponentProps<typeof Separator>) {
|
function ComposerSuggestionSeparator({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof Separator>) {
|
||||||
return (
|
return (
|
||||||
<div className={cn("my-0.5 px-2.5", className)}>
|
<div className={cn("my-0.5 px-2.5", className)}>
|
||||||
<Separator className="bg-popover-border" {...props} />
|
<Separator className="bg-popover-border" {...props} />
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
import { useQuery as useZeroQuery } from "@rocicorp/zero/react";
|
import { useQuery as useZeroQuery } from "@rocicorp/zero/react";
|
||||||
import { keepPreviousData, useQuery } from "@tanstack/react-query";
|
import { keepPreviousData, useQuery } from "@tanstack/react-query";
|
||||||
|
import { useAtomValue } from "jotai";
|
||||||
import {
|
import {
|
||||||
BookOpen,
|
BookOpen,
|
||||||
ChevronLeft,
|
ChevronLeft,
|
||||||
|
|
@ -22,7 +23,6 @@ import {
|
||||||
useState,
|
useState,
|
||||||
} from "react";
|
} from "react";
|
||||||
import type { MentionedDocumentInfo } from "@/atoms/chat/mentioned-documents.atom";
|
import type { MentionedDocumentInfo } from "@/atoms/chat/mentioned-documents.atom";
|
||||||
import { useAtomValue } from "jotai";
|
|
||||||
import { connectorsAtom } from "@/atoms/connectors/connector-query.atoms";
|
import { connectorsAtom } from "@/atoms/connectors/connector-query.atoms";
|
||||||
import { getConnectorTitle } from "@/components/assistant-ui/connector-popup/constants/connector-constants";
|
import { getConnectorTitle } from "@/components/assistant-ui/connector-popup/constants/connector-constants";
|
||||||
import { getConnectorDisplayName } from "@/components/assistant-ui/connector-popup/tabs/all-connectors-tab";
|
import { getConnectorDisplayName } from "@/components/assistant-ui/connector-popup/tabs/all-connectors-tab";
|
||||||
|
|
@ -178,7 +178,9 @@ function useDebounced<T>(value: T, delay = DEBOUNCE_MS) {
|
||||||
return debounced;
|
return debounced;
|
||||||
}
|
}
|
||||||
|
|
||||||
function makeDocMention(doc: Pick<Document, "id" | "title" | "document_type">): MentionedDocumentInfo {
|
function makeDocMention(
|
||||||
|
doc: Pick<Document, "id" | "title" | "document_type">
|
||||||
|
): MentionedDocumentInfo {
|
||||||
return {
|
return {
|
||||||
id: doc.id,
|
id: doc.id,
|
||||||
title: doc.title,
|
title: doc.title,
|
||||||
|
|
@ -187,9 +189,10 @@ function makeDocMention(doc: Pick<Document, "id" | "title" | "document_type">):
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function makeFolderMention(
|
function makeFolderMention(folder: {
|
||||||
folder: { id: number; title: string }
|
id: number;
|
||||||
): Extract<MentionedDocumentInfo, { kind: "folder" }> {
|
title: string;
|
||||||
|
}): Extract<MentionedDocumentInfo, { kind: "folder" }> {
|
||||||
return {
|
return {
|
||||||
id: folder.id,
|
id: folder.id,
|
||||||
title: folder.title,
|
title: folder.title,
|
||||||
|
|
@ -352,9 +355,11 @@ export const DocumentMentionPicker = forwardRef<
|
||||||
page_size: PAGE_SIZE,
|
page_size: PAGE_SIZE,
|
||||||
...(isSearchValid ? { title: debouncedSearch.trim() } : {}),
|
...(isSearchValid ? { title: debouncedSearch.trim() } : {}),
|
||||||
};
|
};
|
||||||
const response: SearchDocumentTitlesResponse = await documentsApiService.searchDocumentTitles({
|
const response: SearchDocumentTitlesResponse = await documentsApiService.searchDocumentTitles(
|
||||||
|
{
|
||||||
queryParams,
|
queryParams,
|
||||||
});
|
}
|
||||||
|
);
|
||||||
|
|
||||||
setAccumulatedDocuments((prev) => [...prev, ...response.items]);
|
setAccumulatedDocuments((prev) => [...prev, ...response.items]);
|
||||||
setHasMore(response.has_more);
|
setHasMore(response.has_more);
|
||||||
|
|
@ -431,7 +436,13 @@ export const DocumentMentionPicker = forwardRef<
|
||||||
)
|
)
|
||||||
.filter((mention): mention is MentionedDocumentInfo => mention !== null)
|
.filter((mention): mention is MentionedDocumentInfo => mention !== null)
|
||||||
.slice(0, RECENTS_LIMIT),
|
.slice(0, RECENTS_LIMIT),
|
||||||
[activeConnectors, hasHydratedRecentDocs, recentMentions, recentValidationDocuments, zeroFolders]
|
[
|
||||||
|
activeConnectors,
|
||||||
|
hasHydratedRecentDocs,
|
||||||
|
recentMentions,
|
||||||
|
recentValidationDocuments,
|
||||||
|
zeroFolders,
|
||||||
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
const selectedKeys = useMemo(
|
const selectedKeys = useMemo(
|
||||||
|
|
@ -460,8 +471,7 @@ export const DocumentMentionPicker = forwardRef<
|
||||||
[visibleRecentMentions, selectedKeys]
|
[visibleRecentMentions, selectedKeys]
|
||||||
);
|
);
|
||||||
|
|
||||||
const rootNodes = useMemo<ComposerSuggestionNode<ResourceNodeValue>[]>(
|
const rootNodes = useMemo<ComposerSuggestionNode<ResourceNodeValue>[]>(() => {
|
||||||
() => {
|
|
||||||
const nodes: ComposerSuggestionNode<ResourceNodeValue>[] = [...recentRootNodes];
|
const nodes: ComposerSuggestionNode<ResourceNodeValue>[] = [...recentRootNodes];
|
||||||
if (showSurfsenseDocsRoot) {
|
if (showSurfsenseDocsRoot) {
|
||||||
nodes.push({
|
nodes.push({
|
||||||
|
|
@ -495,12 +505,12 @@ export const DocumentMentionPicker = forwardRef<
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
return nodes;
|
return nodes;
|
||||||
},
|
}, [activeConnectors.length, recentRootNodes, showSurfsenseDocsRoot]);
|
||||||
[activeConnectors.length, recentRootNodes, showSurfsenseDocsRoot]
|
|
||||||
);
|
|
||||||
|
|
||||||
const searchNodes = useMemo<ComposerSuggestionNode<ResourceNodeValue>[]>(() => {
|
const searchNodes = useMemo<ComposerSuggestionNode<ResourceNodeValue>[]>(() => {
|
||||||
const searchLower = (isSingleCharSearch ? deferredSearch : debouncedSearch).trim().toLowerCase();
|
const searchLower = (isSingleCharSearch ? deferredSearch : debouncedSearch)
|
||||||
|
.trim()
|
||||||
|
.toLowerCase();
|
||||||
const docNodes = actualDocuments.map((doc) => {
|
const docNodes = actualDocuments.map((doc) => {
|
||||||
const mention = makeDocMention(doc);
|
const mention = makeDocMention(doc);
|
||||||
return {
|
return {
|
||||||
|
|
@ -619,7 +629,9 @@ export const DocumentMentionPicker = forwardRef<
|
||||||
id: getMentionDocKey(mention),
|
id: getMentionDocKey(mention),
|
||||||
label: getConnectorDisplayName(connector.name),
|
label: getConnectorDisplayName(connector.name),
|
||||||
subtitle: `${view.title} account`,
|
subtitle: `${view.title} account`,
|
||||||
icon: getConnectorIcon(connector.connector_type, "size-4") ?? <Unplug className="size-4" />,
|
icon: getConnectorIcon(connector.connector_type, "size-4") ?? (
|
||||||
|
<Unplug className="size-4" />
|
||||||
|
),
|
||||||
type: "item" as const,
|
type: "item" as const,
|
||||||
disabled: selectedKeys.has(getMentionDocKey(mention)),
|
disabled: selectedKeys.has(getMentionDocKey(mention)),
|
||||||
value: { kind: "mention" as const, mention },
|
value: { kind: "mention" as const, mention },
|
||||||
|
|
|
||||||
|
|
@ -129,7 +129,9 @@ export const PromptPicker = forwardRef<PromptPickerRef, PromptPickerProps>(funct
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<ComposerSuggestionSkeleton rows={8} mobileRows={8} />
|
<ComposerSuggestionSkeleton rows={8} mobileRows={8} />
|
||||||
) : isError ? (
|
) : isError ? (
|
||||||
<ComposerSuggestionMessage variant="destructive">Failed to load prompts</ComposerSuggestionMessage>
|
<ComposerSuggestionMessage variant="destructive">
|
||||||
|
Failed to load prompts
|
||||||
|
</ComposerSuggestionMessage>
|
||||||
) : filtered.length === 0 ? (
|
) : filtered.length === 0 ? (
|
||||||
<ComposerSuggestionMessage>No matching prompts</ComposerSuggestionMessage>
|
<ComposerSuggestionMessage>No matching prompts</ComposerSuggestionMessage>
|
||||||
) : (
|
) : (
|
||||||
|
|
|
||||||
|
|
@ -12,9 +12,9 @@ import { Label } from "@/components/ui/label";
|
||||||
import { Skeleton } from "@/components/ui/skeleton";
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
import { searchSpacesApiService } from "@/lib/apis/search-spaces-api.service";
|
import { searchSpacesApiService } from "@/lib/apis/search-spaces-api.service";
|
||||||
import { authenticatedFetch } from "@/lib/auth-utils";
|
import { authenticatedFetch } from "@/lib/auth-utils";
|
||||||
|
import { BACKEND_URL } from "@/lib/env-config";
|
||||||
import { cacheKeys } from "@/lib/query-client/cache-keys";
|
import { cacheKeys } from "@/lib/query-client/cache-keys";
|
||||||
import { Spinner } from "../ui/spinner";
|
import { Spinner } from "../ui/spinner";
|
||||||
import { BACKEND_URL } from "@/lib/env-config";
|
|
||||||
|
|
||||||
interface GeneralSettingsManagerProps {
|
interface GeneralSettingsManagerProps {
|
||||||
searchSpaceId: number;
|
searchSpaceId: number;
|
||||||
|
|
|
||||||
|
|
@ -20,10 +20,7 @@ interface PromptConfigManagerProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function PromptConfigManager({ searchSpaceId }: PromptConfigManagerProps) {
|
export function PromptConfigManager({ searchSpaceId }: PromptConfigManagerProps) {
|
||||||
const {
|
const { data: searchSpace, isLoading: loading } = useQuery({
|
||||||
data: searchSpace,
|
|
||||||
isLoading: loading,
|
|
||||||
} = useQuery({
|
|
||||||
queryKey: cacheKeys.searchSpaces.detail(searchSpaceId.toString()),
|
queryKey: cacheKeys.searchSpaces.detail(searchSpaceId.toString()),
|
||||||
queryFn: () => searchSpacesApiService.getSearchSpace({ id: searchSpaceId }),
|
queryFn: () => searchSpacesApiService.getSearchSpace({ id: searchSpaceId }),
|
||||||
enabled: !!searchSpaceId,
|
enabled: !!searchSpaceId,
|
||||||
|
|
@ -56,8 +53,7 @@ export function PromptConfigManager({ searchSpaceId }: PromptConfigManagerProps)
|
||||||
});
|
});
|
||||||
toast.success("System instructions saved successfully");
|
toast.success("System instructions saved successfully");
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
const message =
|
const message = error instanceof Error ? error.message : "Failed to save system instructions";
|
||||||
error instanceof Error ? error.message : "Failed to save system instructions";
|
|
||||||
console.error("Error saving system instructions:", error);
|
console.error("Error saving system instructions:", error);
|
||||||
toast.error(message);
|
toast.error(message);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ import { baseApiService } from "@/lib/apis/base-api.service";
|
||||||
import { authenticatedFetch } from "@/lib/auth-utils";
|
import { authenticatedFetch } from "@/lib/auth-utils";
|
||||||
import { clearActivePodcastTaskId, setActivePodcastTaskId } from "@/lib/chat/podcast-state";
|
import { clearActivePodcastTaskId, setActivePodcastTaskId } from "@/lib/chat/podcast-state";
|
||||||
import { BACKEND_URL } from "@/lib/env-config";
|
import { BACKEND_URL } from "@/lib/env-config";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Zod schemas for runtime validation
|
* Zod schemas for runtime validation
|
||||||
*/
|
*/
|
||||||
|
|
@ -193,10 +194,10 @@ function PodcastPlayer({
|
||||||
} else {
|
} else {
|
||||||
// Authenticated view - fetch audio and details in parallel
|
// Authenticated view - fetch audio and details in parallel
|
||||||
const [audioResponse, details] = await Promise.all([
|
const [audioResponse, details] = await Promise.all([
|
||||||
authenticatedFetch(
|
authenticatedFetch(`${BACKEND_URL}/api/v1/podcasts/${podcastId}/audio`, {
|
||||||
`${BACKEND_URL}/api/v1/podcasts/${podcastId}/audio`,
|
method: "GET",
|
||||||
{ method: "GET", signal: controller.signal }
|
signal: controller.signal,
|
||||||
),
|
}),
|
||||||
baseApiService.get<unknown>(`/api/v1/podcasts/${podcastId}`),
|
baseApiService.get<unknown>(`/api/v1/podcasts/${podcastId}`),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import { TextShimmerLoader } from "@/components/prompt-kit/loader";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { baseApiService } from "@/lib/apis/base-api.service";
|
import { baseApiService } from "@/lib/apis/base-api.service";
|
||||||
import { authenticatedFetch } from "@/lib/auth-utils";
|
import { authenticatedFetch } from "@/lib/auth-utils";
|
||||||
|
import { BACKEND_URL } from "@/lib/env-config";
|
||||||
import { compileCheck, compileToComponent } from "@/lib/remotion/compile-check";
|
import { compileCheck, compileToComponent } from "@/lib/remotion/compile-check";
|
||||||
import { FPS } from "@/lib/remotion/constants";
|
import { FPS } from "@/lib/remotion/constants";
|
||||||
import {
|
import {
|
||||||
|
|
@ -19,7 +20,6 @@ import {
|
||||||
type CompiledSlide,
|
type CompiledSlide,
|
||||||
} from "./combined-player";
|
} from "./combined-player";
|
||||||
import { getPptxExportErrorToast, getVideoDownloadErrorToast } from "./errors";
|
import { getPptxExportErrorToast, getVideoDownloadErrorToast } from "./errors";
|
||||||
import { BACKEND_URL } from "@/lib/env-config";
|
|
||||||
|
|
||||||
const GenerateVideoPresentationArgsSchema = z.object({
|
const GenerateVideoPresentationArgsSchema = z.object({
|
||||||
source_content: z.string(),
|
source_content: z.string(),
|
||||||
|
|
|
||||||
|
|
@ -107,9 +107,7 @@ export const useSearchSourceConnectors = (lazy: boolean = false, searchSpaceId?:
|
||||||
setError(null);
|
setError(null);
|
||||||
|
|
||||||
// Build URL with optional search_space_id query parameter
|
// Build URL with optional search_space_id query parameter
|
||||||
const url = new URL(
|
const url = new URL(`${BACKEND_URL}/api/v1/search-source-connectors`);
|
||||||
`${BACKEND_URL}/api/v1/search-source-connectors`
|
|
||||||
);
|
|
||||||
if (spaceId !== undefined) {
|
if (spaceId !== undefined) {
|
||||||
url.searchParams.append("search_space_id", spaceId.toString());
|
url.searchParams.append("search_space_id", spaceId.toString());
|
||||||
}
|
}
|
||||||
|
|
@ -169,9 +167,7 @@ export const useSearchSourceConnectors = (lazy: boolean = false, searchSpaceId?:
|
||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
// Add search_space_id as a query parameter
|
// Add search_space_id as a query parameter
|
||||||
const url = new URL(
|
const url = new URL(`${BACKEND_URL}/api/v1/search-source-connectors`);
|
||||||
`${BACKEND_URL}/api/v1/search-source-connectors`
|
|
||||||
);
|
|
||||||
url.searchParams.append("search_space_id", spaceId.toString());
|
url.searchParams.append("search_space_id", spaceId.toString());
|
||||||
|
|
||||||
const response = await authenticatedFetch(url.toString(), {
|
const response = await authenticatedFetch(url.toString(), {
|
||||||
|
|
@ -283,9 +279,7 @@ export const useSearchSourceConnectors = (lazy: boolean = false, searchSpaceId?:
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await authenticatedFetch(
|
const response = await authenticatedFetch(
|
||||||
`${
|
`${BACKEND_URL}/api/v1/search-source-connectors/${connectorId}/index?${params.toString()}`,
|
||||||
BACKEND_URL
|
|
||||||
}/api/v1/search-source-connectors/${connectorId}/index?${params.toString()}`,
|
|
||||||
{
|
{
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
* Authentication utilities for handling token expiration and redirects
|
* Authentication utilities for handling token expiration and redirects
|
||||||
*/
|
*/
|
||||||
import { BACKEND_URL } from "@/lib/env-config";
|
import { BACKEND_URL } from "@/lib/env-config";
|
||||||
|
|
||||||
const REDIRECT_PATH_KEY = "surfsense_redirect_path";
|
const REDIRECT_PATH_KEY = "surfsense_redirect_path";
|
||||||
const BEARER_TOKEN_KEY = "surfsense_bearer_token";
|
const BEARER_TOKEN_KEY = "surfsense_bearer_token";
|
||||||
const REFRESH_TOKEN_KEY = "surfsense_refresh_token";
|
const REFRESH_TOKEN_KEY = "surfsense_refresh_token";
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
|
import type { ChatErrorKind, ChatErrorSeverity, ChatFlow } from "@/lib/chat/chat-error-classifier";
|
||||||
import type { ConnectorTelemetryMeta } from "@/lib/connector-telemetry";
|
import type { ConnectorTelemetryMeta } from "@/lib/connector-telemetry";
|
||||||
import { getConnectorTelemetryMeta } from "@/lib/connector-telemetry";
|
import { getConnectorTelemetryMeta } from "@/lib/connector-telemetry";
|
||||||
import type { ChatErrorKind, ChatErrorSeverity, ChatFlow } from "@/lib/chat/chat-error-classifier";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* PostHog Analytics Event Definitions
|
* PostHog Analytics Event Definitions
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue