ozellik: google oauth, gmail-drive araclari ve admin durum kartlarini ekle
This commit is contained in:
110
backend/app/tools/gmail.py
Normal file
110
backend/app/tools/gmail.py
Normal file
@@ -0,0 +1,110 @@
|
||||
import asyncio
|
||||
from typing import Any
|
||||
|
||||
from googleapiclient.discovery import build
|
||||
|
||||
from app.google.auth import GoogleAuthError, GoogleAuthManager
|
||||
from app.tools.base import Tool
|
||||
|
||||
|
||||
class GmailTool(Tool):
|
||||
name = "gmail"
|
||||
description = "List and search Gmail messages for the connected Google account."
|
||||
|
||||
def __init__(self, auth_manager: GoogleAuthManager) -> None:
|
||||
self.auth_manager = auth_manager
|
||||
|
||||
def parameters_schema(self) -> dict[str, Any]:
|
||||
return {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"query": {
|
||||
"type": "string",
|
||||
"description": "Optional Gmail search query such as from:someone newer_than:7d.",
|
||||
},
|
||||
"max_results": {
|
||||
"type": "integer",
|
||||
"description": "Maximum number of messages to return, from 1 to 20.",
|
||||
"minimum": 1,
|
||||
"maximum": 20,
|
||||
},
|
||||
"label_ids": {
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
"description": "Optional Gmail label filters. Defaults to INBOX.",
|
||||
},
|
||||
},
|
||||
"additionalProperties": False,
|
||||
}
|
||||
|
||||
async def run(self, payload: dict[str, Any]) -> dict[str, Any]:
|
||||
query = str(payload.get("query", "")).strip()
|
||||
max_results = max(1, min(20, int(payload.get("max_results", 10) or 10)))
|
||||
label_ids = payload.get("label_ids")
|
||||
if not isinstance(label_ids, list) or not label_ids:
|
||||
label_ids = ["INBOX"]
|
||||
|
||||
try:
|
||||
creds = await self.auth_manager.get_credentials()
|
||||
except GoogleAuthError as exc:
|
||||
return {"tool": self.name, "status": "error", "message": str(exc)}
|
||||
|
||||
return await asyncio.to_thread(
|
||||
self._list_messages,
|
||||
creds,
|
||||
query,
|
||||
max_results,
|
||||
[str(label) for label in label_ids],
|
||||
)
|
||||
|
||||
def _list_messages(
|
||||
self,
|
||||
credentials: Any,
|
||||
query: str,
|
||||
max_results: int,
|
||||
label_ids: list[str],
|
||||
) -> dict[str, Any]:
|
||||
service = build("gmail", "v1", credentials=credentials, cache_discovery=False)
|
||||
response = (
|
||||
service.users()
|
||||
.messages()
|
||||
.list(userId="me", q=query or None, labelIds=label_ids, maxResults=max_results)
|
||||
.execute()
|
||||
)
|
||||
message_refs = response.get("messages", [])
|
||||
messages = []
|
||||
for item in message_refs:
|
||||
detail = (
|
||||
service.users()
|
||||
.messages()
|
||||
.get(
|
||||
userId="me",
|
||||
id=item["id"],
|
||||
format="metadata",
|
||||
metadataHeaders=["From", "Subject", "Date"],
|
||||
)
|
||||
.execute()
|
||||
)
|
||||
headers = {
|
||||
header.get("name", "").lower(): header.get("value", "")
|
||||
for header in detail.get("payload", {}).get("headers", [])
|
||||
}
|
||||
messages.append(
|
||||
{
|
||||
"id": detail.get("id", ""),
|
||||
"thread_id": detail.get("threadId", ""),
|
||||
"from": headers.get("from", ""),
|
||||
"subject": headers.get("subject", "(no subject)"),
|
||||
"date": headers.get("date", ""),
|
||||
"snippet": detail.get("snippet", ""),
|
||||
"label_ids": detail.get("labelIds", []),
|
||||
}
|
||||
)
|
||||
|
||||
return {
|
||||
"tool": self.name,
|
||||
"status": "ok",
|
||||
"query": query,
|
||||
"count": len(messages),
|
||||
"messages": messages,
|
||||
}
|
||||
167
backend/app/tools/google_drive.py
Normal file
167
backend/app/tools/google_drive.py
Normal file
@@ -0,0 +1,167 @@
|
||||
import asyncio
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from googleapiclient.discovery import build
|
||||
from googleapiclient.errors import HttpError
|
||||
from googleapiclient.http import MediaFileUpload
|
||||
|
||||
from app.google.auth import GoogleAuthError, GoogleAuthManager
|
||||
from app.tools.base import Tool
|
||||
|
||||
|
||||
class GoogleDriveTool(Tool):
|
||||
name = "google_drive"
|
||||
description = "List, search, and upload files to the connected Google Drive account."
|
||||
|
||||
def __init__(self, auth_manager: GoogleAuthManager) -> None:
|
||||
self.auth_manager = auth_manager
|
||||
|
||||
def parameters_schema(self) -> dict[str, Any]:
|
||||
return {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"action": {
|
||||
"type": "string",
|
||||
"enum": ["list", "upload"],
|
||||
"description": "Drive action to perform. Defaults to list.",
|
||||
},
|
||||
"query": {
|
||||
"type": "string",
|
||||
"description": "Optional Drive API query or free-text filename search.",
|
||||
},
|
||||
"max_results": {
|
||||
"type": "integer",
|
||||
"description": "Maximum number of files to return, from 1 to 20.",
|
||||
"minimum": 1,
|
||||
"maximum": 20,
|
||||
},
|
||||
"local_path": {
|
||||
"type": "string",
|
||||
"description": "Absolute local file path to upload when action is upload.",
|
||||
},
|
||||
"filename": {
|
||||
"type": "string",
|
||||
"description": "Optional destination filename for uploads.",
|
||||
},
|
||||
"mime_type": {
|
||||
"type": "string",
|
||||
"description": "Optional MIME type for uploads.",
|
||||
},
|
||||
},
|
||||
"additionalProperties": False,
|
||||
}
|
||||
|
||||
async def run(self, payload: dict[str, Any]) -> dict[str, Any]:
|
||||
action = str(payload.get("action", "list") or "list").strip().lower()
|
||||
query = str(payload.get("query", "")).strip()
|
||||
max_results = max(1, min(20, int(payload.get("max_results", 10) or 10)))
|
||||
local_path = str(payload.get("local_path", "")).strip()
|
||||
filename = str(payload.get("filename", "")).strip()
|
||||
mime_type = str(payload.get("mime_type", "")).strip()
|
||||
|
||||
try:
|
||||
creds = await self.auth_manager.get_credentials()
|
||||
except GoogleAuthError as exc:
|
||||
return {"tool": self.name, "status": "error", "message": str(exc)}
|
||||
|
||||
if action == "upload":
|
||||
if not local_path:
|
||||
return {"tool": self.name, "status": "error", "message": "local_path is required for uploads."}
|
||||
try:
|
||||
return await asyncio.to_thread(self._upload_file, creds, local_path, filename, mime_type)
|
||||
except HttpError as exc:
|
||||
return {"tool": self.name, "status": "error", "message": self._format_http_error(exc)}
|
||||
|
||||
return await asyncio.to_thread(self._list_files, creds, query, max_results)
|
||||
|
||||
def _list_files(self, credentials: Any, query: str, max_results: int) -> dict[str, Any]:
|
||||
service = build("drive", "v3", credentials=credentials, cache_discovery=False)
|
||||
api_query = ""
|
||||
if query:
|
||||
if any(token in query for token in ("name contains", "mimeType", "trashed", "modifiedTime")):
|
||||
api_query = query
|
||||
else:
|
||||
escaped = query.replace("'", "\\'")
|
||||
api_query = f"name contains '{escaped}' and trashed = false"
|
||||
else:
|
||||
api_query = "trashed = false"
|
||||
|
||||
response = (
|
||||
service.files()
|
||||
.list(
|
||||
q=api_query,
|
||||
pageSize=max_results,
|
||||
orderBy="modifiedTime desc",
|
||||
fields="files(id,name,mimeType,modifiedTime,webViewLink,owners(displayName))",
|
||||
)
|
||||
.execute()
|
||||
)
|
||||
files = []
|
||||
for item in response.get("files", []):
|
||||
owners = item.get("owners", [])
|
||||
files.append(
|
||||
{
|
||||
"id": item.get("id", ""),
|
||||
"name": item.get("name", ""),
|
||||
"mime_type": item.get("mimeType", ""),
|
||||
"modified_time": item.get("modifiedTime", ""),
|
||||
"web_view_link": item.get("webViewLink", ""),
|
||||
"owners": [owner.get("displayName", "") for owner in owners],
|
||||
}
|
||||
)
|
||||
|
||||
return {
|
||||
"tool": self.name,
|
||||
"status": "ok",
|
||||
"query": query,
|
||||
"count": len(files),
|
||||
"files": files,
|
||||
}
|
||||
|
||||
def _upload_file(self, credentials: Any, local_path: str, filename: str, mime_type: str) -> dict[str, Any]:
|
||||
path = Path(local_path)
|
||||
if not path.exists() or not path.is_file():
|
||||
return {
|
||||
"tool": self.name,
|
||||
"status": "error",
|
||||
"message": f"Upload file was not found: {path}",
|
||||
}
|
||||
|
||||
service = build("drive", "v3", credentials=credentials, cache_discovery=False)
|
||||
final_name = filename or path.name
|
||||
media = MediaFileUpload(str(path), mimetype=mime_type or None, resumable=False)
|
||||
created = (
|
||||
service.files()
|
||||
.create(
|
||||
body={"name": final_name},
|
||||
media_body=media,
|
||||
fields="id,name,mimeType,webViewLink,webContentLink",
|
||||
)
|
||||
.execute()
|
||||
)
|
||||
|
||||
return {
|
||||
"tool": self.name,
|
||||
"status": "ok",
|
||||
"action": "upload",
|
||||
"file": {
|
||||
"id": created.get("id", ""),
|
||||
"name": created.get("name", final_name),
|
||||
"mime_type": created.get("mimeType", mime_type),
|
||||
"web_view_link": created.get("webViewLink", ""),
|
||||
"web_content_link": created.get("webContentLink", ""),
|
||||
},
|
||||
}
|
||||
|
||||
def _format_http_error(self, exc: HttpError) -> str:
|
||||
content = getattr(exc, "content", b"")
|
||||
if isinstance(content, bytes):
|
||||
text = content.decode("utf-8", errors="ignore").strip()
|
||||
else:
|
||||
text = str(content).strip()
|
||||
if "insufficientPermissions" in text or "Insufficient Permission" in text:
|
||||
return (
|
||||
"Google Drive upload izni yok. Google'i yeniden baglayip Drive yukleme iznini onaylaman gerekiyor."
|
||||
)
|
||||
return text or str(exc)
|
||||
@@ -4,11 +4,14 @@ from sqlalchemy.orm import Session
|
||||
|
||||
from app.config import get_settings
|
||||
from app.db import SecretORM
|
||||
from app.google.auth import GoogleAuthManager
|
||||
from app.models import RuntimeSettings
|
||||
from app.tools.apple_notes import AppleNotesTool
|
||||
from app.tools.browser_use import BrowserUseTool
|
||||
from app.tools.brave_search import BraveSearchTool
|
||||
from app.tools.files import FilesTool
|
||||
from app.tools.gmail import GmailTool
|
||||
from app.tools.google_drive import GoogleDriveTool
|
||||
from app.tools.second_brain import SecondBrainTool
|
||||
from app.tools.terminal import TerminalTool
|
||||
from app.tools.web_fetch import WebFetchTool
|
||||
@@ -18,6 +21,7 @@ def build_tools(runtime: RuntimeSettings, workspace_root: Path, session: Session
|
||||
enabled = {tool.name for tool in runtime.tools if tool.enabled}
|
||||
tools: dict[str, object] = {}
|
||||
settings = get_settings()
|
||||
google_auth = GoogleAuthManager(settings, Path(__file__).resolve().parents[2])
|
||||
|
||||
if "files" in enabled:
|
||||
tools["files"] = FilesTool(workspace_root)
|
||||
@@ -39,6 +43,10 @@ def build_tools(runtime: RuntimeSettings, workspace_root: Path, session: Session
|
||||
workspace_slug=runtime.anythingllm_workspace_slug,
|
||||
api_key=api_key,
|
||||
)
|
||||
if "gmail" in enabled:
|
||||
tools["gmail"] = GmailTool(google_auth)
|
||||
if "google_drive" in enabled:
|
||||
tools["google_drive"] = GoogleDriveTool(google_auth)
|
||||
if "web_fetch" in enabled:
|
||||
tools["web_fetch"] = WebFetchTool()
|
||||
if "terminal" in enabled:
|
||||
|
||||
@@ -113,6 +113,49 @@ class SecondBrainTool(Tool):
|
||||
"raw": data,
|
||||
}
|
||||
|
||||
async def status(self) -> dict[str, Any]:
|
||||
if not self.base_url:
|
||||
return {"reachable": False, "workspace_found": False, "message": "AnythingLLM base URL is not configured."}
|
||||
if not self.workspace_slug:
|
||||
return {
|
||||
"reachable": False,
|
||||
"workspace_found": False,
|
||||
"message": "AnythingLLM workspace slug is not configured.",
|
||||
}
|
||||
if not self.api_key:
|
||||
return {"reachable": False, "workspace_found": False, "message": "AnythingLLM API key is not configured."}
|
||||
|
||||
endpoint = f"{self.base_url}/api/v1/workspaces"
|
||||
headers = {
|
||||
"Authorization": f"Bearer {self.api_key}",
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=15.0) as client:
|
||||
response = await client.get(endpoint, headers=headers)
|
||||
response.raise_for_status()
|
||||
except httpx.HTTPError as exc:
|
||||
return {"reachable": False, "workspace_found": False, "message": str(exc)}
|
||||
|
||||
try:
|
||||
data = response.json()
|
||||
except ValueError:
|
||||
return {"reachable": False, "workspace_found": False, "message": "AnythingLLM returned invalid JSON."}
|
||||
|
||||
workspaces = self._extract_workspaces(data)
|
||||
slug_match = next((item for item in workspaces if item == self.workspace_slug), None)
|
||||
if slug_match:
|
||||
return {
|
||||
"reachable": True,
|
||||
"workspace_found": True,
|
||||
"message": f"Workspace {self.workspace_slug} is reachable.",
|
||||
}
|
||||
return {
|
||||
"reachable": True,
|
||||
"workspace_found": False,
|
||||
"message": f"Workspace {self.workspace_slug} was not found.",
|
||||
}
|
||||
|
||||
def _build_query_prompt(self, query: str, mode: str) -> str:
|
||||
if mode == "query":
|
||||
return (
|
||||
@@ -128,13 +171,18 @@ class SecondBrainTool(Tool):
|
||||
try:
|
||||
payload = response.json()
|
||||
except ValueError:
|
||||
return f"HTTP {response.status_code}"
|
||||
text = response.text.strip()
|
||||
return text or f"HTTP {response.status_code}"
|
||||
if isinstance(payload, dict):
|
||||
for key in ("error", "message"):
|
||||
value = payload.get(key)
|
||||
if isinstance(value, str) and value.strip():
|
||||
return value.strip()
|
||||
return f"HTTP {response.status_code}"
|
||||
detail = payload.get("detail")
|
||||
if isinstance(detail, str) and detail.strip():
|
||||
return detail.strip()
|
||||
text = response.text.strip()
|
||||
return text or f"HTTP {response.status_code}"
|
||||
|
||||
def _extract_text_response(self, data: Any) -> str:
|
||||
if isinstance(data, dict):
|
||||
@@ -162,3 +210,24 @@ class SecondBrainTool(Tool):
|
||||
}
|
||||
)
|
||||
return sources
|
||||
|
||||
def _extract_workspaces(self, data: Any) -> list[str]:
|
||||
if isinstance(data, dict):
|
||||
for key in ("workspaces", "data"):
|
||||
value = data.get(key)
|
||||
if isinstance(value, list):
|
||||
return self._workspace_slugs_from_list(value)
|
||||
if isinstance(data, list):
|
||||
return self._workspace_slugs_from_list(data)
|
||||
return []
|
||||
|
||||
def _workspace_slugs_from_list(self, items: list[Any]) -> list[str]:
|
||||
slugs: list[str] = []
|
||||
for item in items:
|
||||
if isinstance(item, dict):
|
||||
for key in ("slug", "name"):
|
||||
value = item.get(key)
|
||||
if isinstance(value, str) and value.strip():
|
||||
slugs.append(value.strip())
|
||||
break
|
||||
return slugs
|
||||
|
||||
Reference in New Issue
Block a user