mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-05-15 18:25:18 +02:00
fix: resolve graphql complexity, json extraction, kb sync, and ui prefill for linear HIL
This commit is contained in:
parent
d4e2ebb99f
commit
c8413ee2bf
12 changed files with 269 additions and 213 deletions
|
|
@ -5,7 +5,7 @@ from langchain_core.tools import tool
|
|||
from langgraph.types import interrupt
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.connectors.linear_connector import LinearConnector
|
||||
from app.connectors.linear_connector import LinearAPIError, LinearConnector
|
||||
from app.services.linear import LinearToolMetadataService
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
|
@ -230,11 +230,10 @@ def create_create_linear_issue_tool(
|
|||
raise
|
||||
|
||||
logger.error(f"Error creating Linear issue: {e}", exc_info=True)
|
||||
return {
|
||||
"status": "error",
|
||||
"message": str(e)
|
||||
if isinstance(e, ValueError)
|
||||
else f"Unexpected error: {e!s}",
|
||||
}
|
||||
if isinstance(e, (ValueError, LinearAPIError)):
|
||||
message = str(e)
|
||||
else:
|
||||
message = "Something went wrong while creating the issue. Please try again."
|
||||
return {"status": "error", "message": message}
|
||||
|
||||
return create_linear_issue
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ from langchain_core.tools import tool
|
|||
from langgraph.types import interrupt
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.connectors.linear_connector import LinearConnector
|
||||
from app.connectors.linear_connector import LinearAPIError, LinearConnector
|
||||
from app.services.linear import LinearToolMetadataService
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
|
@ -253,11 +253,10 @@ def create_delete_linear_issue_tool(
|
|||
raise
|
||||
|
||||
logger.error(f"Error deleting Linear issue: {e}", exc_info=True)
|
||||
return {
|
||||
"status": "error",
|
||||
"message": str(e)
|
||||
if isinstance(e, ValueError)
|
||||
else f"Unexpected error: {e!s}",
|
||||
}
|
||||
if isinstance(e, (ValueError, LinearAPIError)):
|
||||
message = str(e)
|
||||
else:
|
||||
message = "Something went wrong while deleting the issue. Please try again."
|
||||
return {"status": "error", "message": message}
|
||||
|
||||
return delete_linear_issue
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ from langchain_core.tools import tool
|
|||
from langgraph.types import interrupt
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.connectors.linear_connector import LinearConnector
|
||||
from app.connectors.linear_connector import LinearAPIError, LinearConnector
|
||||
from app.services.linear import LinearKBSyncService, LinearToolMetadataService
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
|
@ -290,12 +290,11 @@ def create_update_linear_issue_tool(
|
|||
raise
|
||||
|
||||
logger.error(f"Error updating Linear issue: {e}", exc_info=True)
|
||||
return {
|
||||
"status": "error",
|
||||
"message": str(e)
|
||||
if isinstance(e, ValueError)
|
||||
else f"Unexpected error: {e!s}",
|
||||
}
|
||||
if isinstance(e, (ValueError, LinearAPIError)):
|
||||
message = str(e)
|
||||
else:
|
||||
message = "Something went wrong while updating the issue. Please try again."
|
||||
return {"status": "error", "message": message}
|
||||
|
||||
return update_linear_issue
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ from langchain_core.tools import tool
|
|||
from langgraph.types import interrupt
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.connectors.notion_history import NotionHistoryConnector
|
||||
from app.connectors.notion_history import NotionAPIError, NotionHistoryConnector
|
||||
from app.services.notion import NotionToolMetadataService
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
|
@ -224,11 +224,10 @@ def create_create_notion_page_tool(
|
|||
raise
|
||||
|
||||
logger.error(f"Error creating Notion page: {e}", exc_info=True)
|
||||
return {
|
||||
"status": "error",
|
||||
"message": str(e)
|
||||
if isinstance(e, ValueError)
|
||||
else f"Unexpected error: {e!s}",
|
||||
}
|
||||
if isinstance(e, (ValueError, NotionAPIError)):
|
||||
message = str(e)
|
||||
else:
|
||||
message = "Something went wrong while creating the page. Please try again."
|
||||
return {"status": "error", "message": message}
|
||||
|
||||
return create_notion_page
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ from langchain_core.tools import tool
|
|||
from langgraph.types import interrupt
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.connectors.notion_history import NotionHistoryConnector
|
||||
from app.connectors.notion_history import NotionAPIError, NotionHistoryConnector
|
||||
from app.services.notion.tool_metadata_service import NotionToolMetadataService
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
|
@ -262,11 +262,10 @@ def create_delete_notion_page_tool(
|
|||
raise
|
||||
|
||||
logger.error(f"Error deleting Notion page: {e}", exc_info=True)
|
||||
return {
|
||||
"status": "error",
|
||||
"message": str(e)
|
||||
if isinstance(e, ValueError)
|
||||
else f"Unexpected error: {e!s}",
|
||||
}
|
||||
if isinstance(e, (ValueError, NotionAPIError)):
|
||||
message = str(e)
|
||||
else:
|
||||
message = "Something went wrong while deleting the page. Please try again."
|
||||
return {"status": "error", "message": message}
|
||||
|
||||
return delete_notion_page
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ from langchain_core.tools import tool
|
|||
from langgraph.types import interrupt
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.connectors.notion_history import NotionHistoryConnector
|
||||
from app.connectors.notion_history import NotionAPIError, NotionHistoryConnector
|
||||
from app.services.notion import NotionToolMetadataService
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
|
@ -261,11 +261,10 @@ def create_update_notion_page_tool(
|
|||
raise
|
||||
|
||||
logger.error(f"Error updating Notion page: {e}", exc_info=True)
|
||||
return {
|
||||
"status": "error",
|
||||
"message": str(e)
|
||||
if isinstance(e, ValueError)
|
||||
else f"Unexpected error: {e!s}",
|
||||
}
|
||||
if isinstance(e, (ValueError, NotionAPIError)):
|
||||
message = str(e)
|
||||
else:
|
||||
message = "Something went wrong while updating the page. Please try again."
|
||||
return {"status": "error", "message": message}
|
||||
|
||||
return update_notion_page
|
||||
|
|
|
|||
|
|
@ -22,6 +22,14 @@ logger = logging.getLogger(__name__)
|
|||
|
||||
LINEAR_GRAPHQL_URL = "https://api.linear.app/graphql"
|
||||
|
||||
|
||||
class LinearAPIError(Exception):
|
||||
"""Raised when the Linear API returns a non-200 response.
|
||||
|
||||
The message is always user-presentable; callers should surface it directly
|
||||
without any additional prefix or wrapping.
|
||||
"""
|
||||
|
||||
ORGANIZATION_QUERY = """
|
||||
query {
|
||||
organization {
|
||||
|
|
@ -243,6 +251,37 @@ class LinearConnector:
|
|||
"Authorization": f"Bearer {self._credentials.access_token}",
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _raise_api_error(status_code: int, body: str) -> None:
|
||||
"""Parse a non-200 Linear API response and raise a clean exception.
|
||||
|
||||
Translates known Linear error codes into user-readable messages so that
|
||||
raw GraphQL payloads never reach the end user.
|
||||
"""
|
||||
import json as _json
|
||||
|
||||
friendly = None
|
||||
try:
|
||||
payload = _json.loads(body)
|
||||
errors = payload.get("errors", [])
|
||||
if errors:
|
||||
ext = errors[0].get("extensions", {})
|
||||
code = ext.get("code", "")
|
||||
if code == "INPUT_ERROR" and "too complex" in errors[0].get("message", "").lower():
|
||||
friendly = (
|
||||
"Linear rejected the request because the workspace is too large "
|
||||
"to fetch in one query. Please try again — if the problem persists, "
|
||||
"contact support."
|
||||
)
|
||||
elif ext.get("userPresentableMessage"):
|
||||
friendly = ext["userPresentableMessage"]
|
||||
elif errors[0].get("message"):
|
||||
friendly = errors[0]["message"]
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
raise LinearAPIError(friendly or f"Linear API error (HTTP {status_code})")
|
||||
|
||||
async def execute_graphql_query(
|
||||
self, query: str, variables: dict[str, Any] | None = None
|
||||
) -> dict[str, Any]:
|
||||
|
|
@ -281,9 +320,7 @@ class LinearConnector:
|
|||
if response.status_code == 200:
|
||||
return response.json()
|
||||
else:
|
||||
raise Exception(
|
||||
f"Query failed with status code {response.status_code}: {response.text}"
|
||||
)
|
||||
self._raise_api_error(response.status_code, response.text)
|
||||
|
||||
async def get_all_issues(
|
||||
self, include_comments: bool = True
|
||||
|
|
|
|||
|
|
@ -17,6 +17,15 @@ from app.utils.oauth_security import TokenEncryption
|
|||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class NotionAPIError(Exception):
|
||||
"""Raised when the Notion API returns a non-200 response.
|
||||
|
||||
The message is always user-presentable; callers should surface it directly
|
||||
without any additional prefix or wrapping.
|
||||
"""
|
||||
|
||||
|
||||
# Type variable for generic return type
|
||||
T = TypeVar("T")
|
||||
|
||||
|
|
@ -250,8 +259,9 @@ class NotionHistoryConnector:
|
|||
logger.error(
|
||||
f"Failed to refresh Notion token for connector {self._connector_id}: {e!s}"
|
||||
)
|
||||
raise Exception(
|
||||
f"Failed to refresh Notion OAuth credentials: {e!s}"
|
||||
raise NotionAPIError(
|
||||
"Failed to refresh your Notion connection. "
|
||||
"Please try again or reconnect your Notion account."
|
||||
) from e
|
||||
|
||||
return self._credentials.access_token
|
||||
|
|
|
|||
|
|
@ -78,7 +78,7 @@ class LinearKBSyncService:
|
|||
issue_identifier = formatted_issue.get("identifier", "")
|
||||
issue_title = formatted_issue.get("title", "")
|
||||
state = formatted_issue.get("state", "Unknown")
|
||||
priority = issue_raw.get("priority", 0)
|
||||
priority = issue_raw.get("priorityLabel", "Unknown")
|
||||
comment_count = len(formatted_issue.get("comments", []))
|
||||
description = formatted_issue.get("description", "")
|
||||
|
||||
|
|
@ -125,16 +125,20 @@ class LinearKBSyncService:
|
|||
issue_content, search_space_id
|
||||
)
|
||||
document.embedding = summary_embedding
|
||||
from sqlalchemy.orm.attributes import flag_modified
|
||||
|
||||
document.document_metadata = {
|
||||
**document.document_metadata,
|
||||
**(document.document_metadata or {}),
|
||||
"issue_id": issue_id,
|
||||
"issue_identifier": issue_identifier,
|
||||
"issue_title": issue_title,
|
||||
"state": state,
|
||||
"priority": priority,
|
||||
"comment_count": comment_count,
|
||||
"indexed_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
||||
"connector_id": connector_id,
|
||||
}
|
||||
flag_modified(document, "document_metadata")
|
||||
safe_set_chunks(document, chunks)
|
||||
document.updated_at = get_current_timestamp()
|
||||
|
||||
|
|
@ -159,7 +163,7 @@ class LinearKBSyncService:
|
|||
query = """
|
||||
query LinearIssueSync($id: String!) {
|
||||
issue(id: $id) {
|
||||
id identifier title description priority
|
||||
id identifier title description priority priorityLabel
|
||||
createdAt updatedAt url
|
||||
state { id name type color }
|
||||
creator { id name email }
|
||||
|
|
|
|||
|
|
@ -237,7 +237,7 @@ class LinearToolMetadataService:
|
|||
"""Fetch all teams with their states, members, and labels."""
|
||||
query = """
|
||||
query {
|
||||
teams {
|
||||
teams(first: 25) {
|
||||
nodes {
|
||||
id name key
|
||||
states { nodes { id name type color position } }
|
||||
|
|
@ -248,7 +248,19 @@ class LinearToolMetadataService:
|
|||
}
|
||||
"""
|
||||
result = await client.execute_graphql_query(query)
|
||||
return result.get("data", {}).get("teams", {}).get("nodes", [])
|
||||
raw_teams = result.get("data", {}).get("teams", {}).get("nodes", [])
|
||||
|
||||
return [
|
||||
{
|
||||
"id": t.get("id"),
|
||||
"name": t.get("name"),
|
||||
"key": t.get("key"),
|
||||
"states": (t.get("states") or {}).get("nodes", []),
|
||||
"members": (t.get("members") or {}).get("nodes", []),
|
||||
"labels": (t.get("labels") or {}).get("nodes", []),
|
||||
}
|
||||
for t in raw_teams
|
||||
]
|
||||
|
||||
@staticmethod
|
||||
async def _fetch_issue_context(
|
||||
|
|
@ -296,11 +308,15 @@ class LinearToolMetadataService:
|
|||
SearchSourceConnector.user_id == user_id,
|
||||
or_(
|
||||
func.lower(
|
||||
Document.document_metadata["issue_title"].astext
|
||||
Document.document_metadata.op("->>")(
|
||||
"issue_title"
|
||||
)
|
||||
)
|
||||
== ref_lower,
|
||||
func.lower(
|
||||
Document.document_metadata["issue_identifier"].astext
|
||||
Document.document_metadata.op("->>")(
|
||||
"issue_identifier"
|
||||
)
|
||||
)
|
||||
== ref_lower,
|
||||
func.lower(Document.title) == ref_lower,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue