feat: Added LinkUP Search Engine Connector

This commit is contained in:
DESKTOP-RTLN3BA\$punk 2025-04-27 15:53:33 -07:00
parent 273c16a611
commit 3675505eb1
18 changed files with 492 additions and 27 deletions

View file

@ -0,0 +1,45 @@
"""Add LINKUP_API to SearchSourceConnectorType enum
Revision ID: 4
Revises: 3
Create Date: 2025-04-18 10:00:00.000000
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = '4'
down_revision: Union[str, None] = '3'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
# Manually add the command to add the enum value
op.execute("ALTER TYPE searchsourceconnectortype ADD VALUE 'LINKUP_API'")
# Pass for the rest, as autogenerate didn't run to add other schema details
pass
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
# Downgrading removal of an enum value requires recreating the type
op.execute("ALTER TYPE searchsourceconnectortype RENAME TO searchsourceconnectortype_old")
op.execute("CREATE TYPE searchsourceconnectortype AS ENUM('SERPER_API', 'TAVILY_API', 'SLACK_CONNECTOR', 'NOTION_CONNECTOR', 'GITHUB_CONNECTOR', 'LINEAR_CONNECTOR')")
op.execute((
"ALTER TABLE search_source_connectors ALTER COLUMN connector_type TYPE searchsourceconnectortype USING "
"connector_type::text::searchsourceconnectortype"
))
op.execute("DROP TYPE searchsourceconnectortype_old")
pass
# ### end Alembic commands ###

View file

@ -143,7 +143,7 @@ async def fetch_relevant_documents(
connectors_to_search: List[str], connectors_to_search: List[str],
writer: StreamWriter = None, writer: StreamWriter = None,
state: State = None, state: State = None,
top_k: int = 20 top_k: int = 10
) -> List[Dict[str, Any]]: ) -> List[Dict[str, Any]]:
""" """
Fetch relevant documents for research questions using the provided connectors. Fetch relevant documents for research questions using the provided connectors.
@ -264,22 +264,6 @@ async def fetch_relevant_documents(
streaming_service.only_update_terminal(f"Found {len(files_chunks)} file chunks relevant to the query") streaming_service.only_update_terminal(f"Found {len(files_chunks)} file chunks relevant to the query")
writer({"yeild_value": streaming_service._format_annotations()}) writer({"yeild_value": streaming_service._format_annotations()})
elif connector == "TAVILY_API":
source_object, tavily_chunks = await connector_service.search_tavily(
user_query=reformulated_query,
user_id=user_id,
top_k=top_k
)
# Add to sources and raw documents
if source_object:
all_sources.append(source_object)
all_raw_documents.extend(tavily_chunks)
# Stream found document count
if streaming_service and writer:
streaming_service.only_update_terminal(f"Found {len(tavily_chunks)} web search results relevant to the query")
writer({"yeild_value": streaming_service._format_annotations()})
elif connector == "SLACK_CONNECTOR": elif connector == "SLACK_CONNECTOR":
source_object, slack_chunks = await connector_service.search_slack( source_object, slack_chunks = await connector_service.search_slack(
@ -352,6 +336,47 @@ async def fetch_relevant_documents(
if streaming_service and writer: if streaming_service and writer:
streaming_service.only_update_terminal(f"Found {len(linear_chunks)} Linear issues relevant to the query") streaming_service.only_update_terminal(f"Found {len(linear_chunks)} Linear issues relevant to the query")
writer({"yeild_value": streaming_service._format_annotations()}) writer({"yeild_value": streaming_service._format_annotations()})
elif connector == "TAVILY_API":
source_object, tavily_chunks = await connector_service.search_tavily(
user_query=reformulated_query,
user_id=user_id,
top_k=top_k
)
# Add to sources and raw documents
if source_object:
all_sources.append(source_object)
all_raw_documents.extend(tavily_chunks)
# Stream found document count
if streaming_service and writer:
streaming_service.only_update_terminal(f"Found {len(tavily_chunks)} web search results relevant to the query")
writer({"yeild_value": streaming_service._format_annotations()})
elif connector == "LINKUP_API":
if top_k > 10:
linkup_mode = "deep"
else:
linkup_mode = "standard"
source_object, linkup_chunks = await connector_service.search_linkup(
user_query=reformulated_query,
user_id=user_id,
mode=linkup_mode
)
# Add to sources and raw documents
if source_object:
all_sources.append(source_object)
all_raw_documents.extend(linkup_chunks)
# Stream found document count
if streaming_service and writer:
streaming_service.only_update_terminal(f"Found {len(linkup_chunks)} Linkup chunks relevant to the query")
writer({"yeild_value": streaming_service._format_annotations()})
except Exception as e: except Exception as e:
error_message = f"Error searching connector {connector}: {str(e)}" error_message = f"Error searching connector {connector}: {str(e)}"
print(error_message) print(error_message)
@ -462,6 +487,14 @@ async def process_sections(state: State, config: RunnableConfig, writer: StreamW
streaming_service.only_update_terminal("Searching for relevant information across all connectors...") streaming_service.only_update_terminal("Searching for relevant information across all connectors...")
writer({"yeild_value": streaming_service._format_annotations()}) writer({"yeild_value": streaming_service._format_annotations()})
if configuration.num_sections == 1:
TOP_K = 10
elif configuration.num_sections == 3:
TOP_K = 20
elif configuration.num_sections == 6:
TOP_K = 30
relevant_documents = [] relevant_documents = []
async with async_session_maker() as db_session: async with async_session_maker() as db_session:
try: try:
@ -472,7 +505,8 @@ async def process_sections(state: State, config: RunnableConfig, writer: StreamW
db_session=db_session, db_session=db_session,
connectors_to_search=configuration.connectors_to_search, connectors_to_search=configuration.connectors_to_search,
writer=writer, writer=writer,
state=state state=state,
top_k=TOP_K
) )
except Exception as e: except Exception as e:
error_message = f"Error fetching relevant documents: {str(e)}" error_message = f"Error fetching relevant documents: {str(e)}"

View file

@ -44,8 +44,9 @@ class DocumentType(str, Enum):
LINEAR_CONNECTOR = "LINEAR_CONNECTOR" LINEAR_CONNECTOR = "LINEAR_CONNECTOR"
class SearchSourceConnectorType(str, Enum): class SearchSourceConnectorType(str, Enum):
SERPER_API = "SERPER_API" SERPER_API = "SERPER_API" # NOT IMPLEMENTED YET : DON'T REMEMBER WHY : MOST PROBABLY BECAUSE WE NEED TO CRAWL THE RESULTS RETURNED BY IT
TAVILY_API = "TAVILY_API" TAVILY_API = "TAVILY_API"
LINKUP_API = "LINKUP_API"
SLACK_CONNECTOR = "SLACK_CONNECTOR" SLACK_CONNECTOR = "SLACK_CONNECTOR"
NOTION_CONNECTOR = "NOTION_CONNECTOR" NOTION_CONNECTOR = "NOTION_CONNECTOR"
GITHUB_CONNECTOR = "GITHUB_CONNECTOR" GITHUB_CONNECTOR = "GITHUB_CONNECTOR"

View file

@ -36,6 +36,16 @@ class SearchSourceConnectorBase(BaseModel):
# Ensure the API key is not empty # Ensure the API key is not empty
if not config.get("TAVILY_API_KEY"): if not config.get("TAVILY_API_KEY"):
raise ValueError("TAVILY_API_KEY cannot be empty") raise ValueError("TAVILY_API_KEY cannot be empty")
elif connector_type == SearchSourceConnectorType.LINKUP_API:
# For LINKUP_API, only allow LINKUP_API_KEY
allowed_keys = ["LINKUP_API_KEY"]
if set(config.keys()) != set(allowed_keys):
raise ValueError(f"For LINKUP_API connector type, config must only contain these keys: {allowed_keys}")
# Ensure the API key is not empty
if not config.get("LINKUP_API_KEY"):
raise ValueError("LINKUP_API_KEY cannot be empty")
elif connector_type == SearchSourceConnectorType.SLACK_CONNECTOR: elif connector_type == SearchSourceConnectorType.SLACK_CONNECTOR:
# For SLACK_CONNECTOR, only allow SLACK_BOT_TOKEN # For SLACK_CONNECTOR, only allow SLACK_BOT_TOKEN

File diff suppressed because one or more lines are too long

View file

@ -5,6 +5,7 @@ from sqlalchemy.future import select
from app.retriver.chunks_hybrid_search import ChucksHybridSearchRetriever from app.retriver.chunks_hybrid_search import ChucksHybridSearchRetriever
from app.db import SearchSourceConnector, SearchSourceConnectorType from app.db import SearchSourceConnector, SearchSourceConnectorType
from tavily import TavilyClient from tavily import TavilyClient
from linkup import LinkupClient
class ConnectorService: class ConnectorService:
@ -643,3 +644,97 @@ class ConnectorService:
} }
return result_object, linear_chunks return result_object, linear_chunks
async def search_linkup(self, user_query: str, user_id: str, mode: str = "standard") -> tuple:
"""
Search using Linkup API and return both the source information and documents
Args:
user_query: The user's query
user_id: The user's ID
mode: Search depth mode, can be "standard" or "deep"
Returns:
tuple: (sources_info, documents)
"""
# Get Linkup connector configuration
linkup_connector = await self.get_connector_by_type(user_id, SearchSourceConnectorType.LINKUP_API)
if not linkup_connector:
# Return empty results if no Linkup connector is configured
return {
"id": 10,
"name": "Linkup Search",
"type": "LINKUP_API",
"sources": [],
}, []
# Initialize Linkup client with API key from connector config
linkup_api_key = linkup_connector.config.get("LINKUP_API_KEY")
linkup_client = LinkupClient(api_key=linkup_api_key)
# Perform search with Linkup
try:
response = linkup_client.search(
query=user_query,
depth=mode, # Use the provided mode ("standard" or "deep")
output_type="searchResults", # Default to search results
)
# Extract results from Linkup response - access as attribute instead of using .get()
linkup_results = response.results if hasattr(response, 'results') else []
# Process each result and create sources directly without deduplication
sources_list = []
documents = []
for i, result in enumerate(linkup_results):
# Fix for UI
linkup_results[i]['document']['id'] = self.source_id_counter
# Create a source entry
source = {
"id": self.source_id_counter,
"title": result.name if hasattr(result, 'name') else "Linkup Result",
"description": result.content[:100] if hasattr(result, 'content') else "",
"url": result.url if hasattr(result, 'url') else ""
}
sources_list.append(source)
# Create a document entry
document = {
"chunk_id": f"linkup_chunk_{i}",
"content": result.content if hasattr(result, 'content') else "",
"score": 1.0, # Default score since not provided by Linkup
"document": {
"id": self.source_id_counter,
"title": result.name if hasattr(result, 'name') else "Linkup Result",
"document_type": "LINKUP_API",
"metadata": {
"url": result.url if hasattr(result, 'url') else "",
"type": result.type if hasattr(result, 'type') else "",
"source": "LINKUP_API"
}
}
}
documents.append(document)
self.source_id_counter += 1
# Create result object
result_object = {
"id": 10,
"name": "Linkup Search",
"type": "LINKUP_API",
"sources": sources_list,
}
return result_object, documents
except Exception as e:
# Log the error and return empty results
print(f"Error searching with Linkup: {str(e)}")
return {
"id": 10,
"name": "Linkup Search",
"type": "LINKUP_API",
"sources": [],
}, []

View file

@ -15,6 +15,7 @@ dependencies = [
"langchain-community>=0.3.17", "langchain-community>=0.3.17",
"langchain-unstructured>=0.1.6", "langchain-unstructured>=0.1.6",
"langgraph>=0.3.29", "langgraph>=0.3.29",
"linkup-sdk>=0.2.4",
"litellm>=1.61.4", "litellm>=1.61.4",
"markdownify>=0.14.1", "markdownify>=0.14.1",
"notion-client>=2.3.0", "notion-client>=2.3.0",

View file

@ -1413,6 +1413,19 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/8b/e4/5380e8229c442e406404977d2ec71a9db6a3e6a89fce7791c6ad7cd2bdbe/langsmith-0.3.8-py3-none-any.whl", hash = "sha256:fbb9dd97b0f090219447fca9362698d07abaeda1da85aa7cc6ec6517b36581b1", size = 332800 }, { url = "https://files.pythonhosted.org/packages/8b/e4/5380e8229c442e406404977d2ec71a9db6a3e6a89fce7791c6ad7cd2bdbe/langsmith-0.3.8-py3-none-any.whl", hash = "sha256:fbb9dd97b0f090219447fca9362698d07abaeda1da85aa7cc6ec6517b36581b1", size = 332800 },
] ]
[[package]]
name = "linkup-sdk"
version = "0.2.4"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "httpx" },
{ name = "pydantic" },
]
sdist = { url = "https://files.pythonhosted.org/packages/c2/c7/d9a85331bf2611ecac67f1ad92a6ced641b2e2e93eea26b17a9af701b3d1/linkup_sdk-0.2.4.tar.gz", hash = "sha256:2b8fd1894b9b4715bc14aabcbf53df6def9024f2cc426f234cc59e1807ec4c12", size = 9392 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/18/d8/bb9e01328fe5ad979e3e459c0f76321d295663906deef56eeaa5ce0cf269/linkup_sdk-0.2.4-py3-none-any.whl", hash = "sha256:8bc4c4f34de93529136a14e42441d803868d681c2bf3fd59be51923e44f1f1d4", size = 8325 },
]
[[package]] [[package]]
name = "litellm" name = "litellm"
version = "1.61.4" version = "1.61.4"
@ -3078,6 +3091,7 @@ dependencies = [
{ name = "langchain-community" }, { name = "langchain-community" },
{ name = "langchain-unstructured" }, { name = "langchain-unstructured" },
{ name = "langgraph" }, { name = "langgraph" },
{ name = "linkup-sdk" },
{ name = "litellm" }, { name = "litellm" },
{ name = "markdownify" }, { name = "markdownify" },
{ name = "notion-client" }, { name = "notion-client" },
@ -3106,6 +3120,7 @@ requires-dist = [
{ name = "langchain-community", specifier = ">=0.3.17" }, { name = "langchain-community", specifier = ">=0.3.17" },
{ name = "langchain-unstructured", specifier = ">=0.1.6" }, { name = "langchain-unstructured", specifier = ">=0.1.6" },
{ name = "langgraph", specifier = ">=0.3.29" }, { name = "langgraph", specifier = ">=0.3.29" },
{ name = "linkup-sdk", specifier = ">=0.2.4" },
{ name = "litellm", specifier = ">=1.61.4" }, { name = "litellm", specifier = ">=1.61.4" },
{ name = "markdownify", specifier = ">=0.14.1" }, { name = "markdownify", specifier = ">=0.14.1" },
{ name = "notion-client", specifier = ">=2.3.0" }, { name = "notion-client", specifier = ">=2.3.0" },

View file

@ -46,6 +46,7 @@ const getConnectorTypeDisplay = (type: string): string => {
"NOTION_CONNECTOR": "Notion", "NOTION_CONNECTOR": "Notion",
"GITHUB_CONNECTOR": "GitHub", "GITHUB_CONNECTOR": "GitHub",
"LINEAR_CONNECTOR": "Linear", "LINEAR_CONNECTOR": "Linear",
"LINKUP_API": "Linkup",
// Add other connector types here as needed // Add other connector types here as needed
}; };
return typeMap[type] || type; return typeMap[type] || type;

View file

@ -160,6 +160,17 @@ export default function EditConnectorPage() {
/> />
)} )}
{/* == Linkup == */}
{connector.connector_type === 'LINKUP_API' && (
<EditSimpleTokenForm
control={editForm.control}
fieldName="LINKUP_API_KEY"
fieldLabel="Linkup API Key"
fieldDescription="Update your Linkup API Key if needed."
placeholder="Begins with linkup_..."
/>
)}
</CardContent> </CardContent>
<CardFooter className="border-t pt-6"> <CardFooter className="border-t pt-6">
<Button type="submit" disabled={isSaving} className="w-full sm:w-auto"> <Button type="submit" disabled={isSaving} className="w-full sm:w-auto">

View file

@ -52,6 +52,7 @@ const getConnectorTypeDisplay = (type: string): string => {
"SLACK_CONNECTOR": "Slack Connector", "SLACK_CONNECTOR": "Slack Connector",
"NOTION_CONNECTOR": "Notion Connector", "NOTION_CONNECTOR": "Notion Connector",
"GITHUB_CONNECTOR": "GitHub Connector", "GITHUB_CONNECTOR": "GitHub Connector",
"LINKUP_API": "Linkup",
// Add other connector types here as needed // Add other connector types here as needed
}; };
return typeMap[type] || type; return typeMap[type] || type;
@ -87,7 +88,8 @@ export default function EditConnectorPage() {
"TAVILY_API": "TAVILY_API_KEY", "TAVILY_API": "TAVILY_API_KEY",
"SLACK_CONNECTOR": "SLACK_BOT_TOKEN", "SLACK_CONNECTOR": "SLACK_BOT_TOKEN",
"NOTION_CONNECTOR": "NOTION_INTEGRATION_TOKEN", "NOTION_CONNECTOR": "NOTION_INTEGRATION_TOKEN",
"GITHUB_CONNECTOR": "GITHUB_PAT" "GITHUB_CONNECTOR": "GITHUB_PAT",
"LINKUP_API": "LINKUP_API_KEY"
}; };
return fieldMap[connectorType] || ""; return fieldMap[connectorType] || "";
}; };
@ -229,7 +231,9 @@ export default function EditConnectorPage() {
? "Notion Integration Token" ? "Notion Integration Token"
: connector?.connector_type === "GITHUB_CONNECTOR" : connector?.connector_type === "GITHUB_CONNECTOR"
? "GitHub Personal Access Token (PAT)" ? "GitHub Personal Access Token (PAT)"
: "API Key"} : connector?.connector_type === "LINKUP_API"
? "Linkup API Key"
: "API Key"}
</FormLabel> </FormLabel>
<FormControl> <FormControl>
<Input <Input
@ -241,7 +245,9 @@ export default function EditConnectorPage() {
? "Enter new Notion Token (optional)" ? "Enter new Notion Token (optional)"
: connector?.connector_type === "GITHUB_CONNECTOR" : connector?.connector_type === "GITHUB_CONNECTOR"
? "Enter new GitHub PAT (optional)" ? "Enter new GitHub PAT (optional)"
: "Enter new API key (optional)" : connector?.connector_type === "LINKUP_API"
? "Enter new Linkup API Key (optional)"
: "Enter new API key (optional)"
} }
{...field} {...field}
/> />
@ -253,7 +259,9 @@ export default function EditConnectorPage() {
? "Enter a new Notion Integration Token or leave blank to keep your existing token." ? "Enter a new Notion Integration Token or leave blank to keep your existing token."
: connector?.connector_type === "GITHUB_CONNECTOR" : connector?.connector_type === "GITHUB_CONNECTOR"
? "Enter a new GitHub PAT or leave blank to keep your existing token." ? "Enter a new GitHub PAT or leave blank to keep your existing token."
: "Enter a new API key or leave blank to keep your existing key."} : connector?.connector_type === "LINKUP_API"
? "Enter a new Linkup API Key or leave blank to keep your existing key."
: "Enter a new API key or leave blank to keep your existing key."}
</FormDescription> </FormDescription>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>

View file

@ -0,0 +1,207 @@
"use client";
import { useState } from "react";
import { useRouter, useParams } from "next/navigation";
import { motion } from "framer-motion";
import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form";
import * as z from "zod";
import { toast } from "sonner";
import { ArrowLeft, Check, Info, Loader2 } from "lucide-react";
import { useSearchSourceConnectors } from "@/hooks/useSearchSourceConnectors";
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import {
Alert,
AlertDescription,
AlertTitle,
} from "@/components/ui/alert";
// Define the form schema with Zod
const linkupApiFormSchema = z.object({
name: z.string().min(3, {
message: "Connector name must be at least 3 characters.",
}),
api_key: z.string().min(10, {
message: "API key is required and must be valid.",
}),
});
// Define the type for the form values
type LinkupApiFormValues = z.infer<typeof linkupApiFormSchema>;
export default function LinkupApiPage() {
const router = useRouter();
const params = useParams();
const searchSpaceId = params.search_space_id as string;
const [isSubmitting, setIsSubmitting] = useState(false);
const { createConnector } = useSearchSourceConnectors();
// Initialize the form
const form = useForm<LinkupApiFormValues>({
resolver: zodResolver(linkupApiFormSchema),
defaultValues: {
name: "Linkup API Connector",
api_key: "",
},
});
// Handle form submission
const onSubmit = async (values: LinkupApiFormValues) => {
setIsSubmitting(true);
try {
await createConnector({
name: values.name,
connector_type: "LINKUP_API",
config: {
LINKUP_API_KEY: values.api_key,
},
is_indexable: false,
last_indexed_at: null,
});
toast.success("Linkup API connector created successfully!");
// Navigate back to connectors page
router.push(`/dashboard/${searchSpaceId}/connectors`);
} catch (error) {
console.error("Error creating connector:", error);
toast.error(error instanceof Error ? error.message : "Failed to create connector");
} finally {
setIsSubmitting(false);
}
};
return (
<div className="container mx-auto py-8 max-w-3xl">
<Button
variant="ghost"
className="mb-6"
onClick={() => router.push(`/dashboard/${searchSpaceId}/connectors/add`)}
>
<ArrowLeft className="mr-2 h-4 w-4" />
Back to Connectors
</Button>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }}
>
<Card className="border-2 border-border">
<CardHeader>
<CardTitle className="text-2xl font-bold">Connect Linkup API</CardTitle>
<CardDescription>
Integrate with Linkup API to enhance your search capabilities with AI-powered search results.
</CardDescription>
</CardHeader>
<CardContent>
<Alert className="mb-6 bg-muted">
<Info className="h-4 w-4" />
<AlertTitle>API Key Required</AlertTitle>
<AlertDescription>
You'll need a Linkup API key to use this connector. You can get one by signing up at{" "}
<a
href="https://linkup.so"
target="_blank"
rel="noopener noreferrer"
className="font-medium underline underline-offset-4"
>
linkup.so
</a>
</AlertDescription>
</Alert>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Connector Name</FormLabel>
<FormControl>
<Input placeholder="My Linkup API Connector" {...field} />
</FormControl>
<FormDescription>
A friendly name to identify this connector.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="api_key"
render={({ field }) => (
<FormItem>
<FormLabel>Linkup API Key</FormLabel>
<FormControl>
<Input
type="password"
placeholder="Enter your Linkup API key"
{...field}
/>
</FormControl>
<FormDescription>
Your API key will be encrypted and stored securely.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<div className="flex justify-end">
<Button
type="submit"
disabled={isSubmitting}
className="w-full sm:w-auto"
>
{isSubmitting ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Connecting...
</>
) : (
<>
<Check className="mr-2 h-4 w-4" />
Connect Linkup API
</>
)}
</Button>
</div>
</form>
</Form>
</CardContent>
<CardFooter className="flex flex-col items-start border-t bg-muted/50 px-6 py-4">
<h4 className="text-sm font-medium">What you get with Linkup API:</h4>
<ul className="mt-2 list-disc pl-5 text-sm text-muted-foreground">
<li>AI-powered search results tailored to your queries</li>
<li>Real-time information from the web</li>
<li>Enhanced search capabilities for your projects</li>
</ul>
</CardFooter>
</Card>
</motion.div>
</div>
);
}

View file

@ -16,6 +16,7 @@ import {
IconWorldWww, IconWorldWww,
IconTicket, IconTicket,
IconLayoutKanban, IconLayoutKanban,
IconLinkPlus,
} from "@tabler/icons-react"; } from "@tabler/icons-react";
import { AnimatePresence, motion } from "framer-motion"; import { AnimatePresence, motion } from "framer-motion";
import Link from "next/link"; import Link from "next/link";
@ -50,7 +51,13 @@ const connectorCategories: ConnectorCategory[] = [
icon: <IconWorldWww className="h-6 w-6" />, icon: <IconWorldWww className="h-6 w-6" />,
status: "available", status: "available",
}, },
// Add other search engine connectors like Tavily, Serper if they have UI config {
id: "linkup-api",
title: "Linkup API",
description: "Search the web using the Linkup API",
icon: <IconLinkPlus className="h-6 w-6" />,
status: "available",
},
], ],
}, },
{ {

View file

@ -36,7 +36,7 @@ export function ModernHeroWithGradients() {
</h1> </h1>
</div> </div>
<p className="mx-auto max-w-3xl py-6 text-center text-base text-gray-600 dark:text-neutral-300 md:text-lg lg:text-xl"> <p className="mx-auto max-w-3xl py-6 text-center text-base text-gray-600 dark:text-neutral-300 md:text-lg lg:text-xl">
A Customizable AI Research Agent just like NotebookLM or Perplexity, but connected to external sources such as search engines (Tavily), Slack, Linear, Notion, YouTube, GitHub and more. A Customizable AI Research Agent just like NotebookLM or Perplexity, but connected to external sources such as search engines (Tavily, LinkUp), Slack, Linear, Notion, YouTube, GitHub and more.
</p> </p>
<div className="flex flex-col items-center gap-6 py-6 sm:flex-row"> <div className="flex flex-col items-center gap-6 py-6 sm:flex-row">
<Link <Link

View file

@ -11,7 +11,7 @@ import {
Link, Link,
Webhook, Webhook,
} from 'lucide-react'; } from 'lucide-react';
import { IconBrandNotion, IconBrandSlack, IconBrandYoutube, IconBrandGithub, IconLayoutKanban } from "@tabler/icons-react"; import { IconBrandNotion, IconBrandSlack, IconBrandYoutube, IconBrandGithub, IconLayoutKanban, IconLinkPlus } from "@tabler/icons-react";
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Connector, ResearchMode } from './types'; import { Connector, ResearchMode } from './types';
@ -20,6 +20,8 @@ export const getConnectorIcon = (connectorType: string) => {
const iconProps = { className: "h-4 w-4" }; const iconProps = { className: "h-4 w-4" };
switch(connectorType) { switch(connectorType) {
case 'LINKUP_API':
return <IconLinkPlus {...iconProps} />;
case 'LINEAR_CONNECTOR': case 'LINEAR_CONNECTOR':
return <IconLayoutKanban {...iconProps} />; return <IconLayoutKanban {...iconProps} />;
case 'GITHUB_CONNECTOR': case 'GITHUB_CONNECTOR':

View file

@ -30,5 +30,6 @@ export const editConnectorSchema = z.object({
SERPER_API_KEY: z.string().optional(), SERPER_API_KEY: z.string().optional(),
TAVILY_API_KEY: z.string().optional(), TAVILY_API_KEY: z.string().optional(),
LINEAR_API_KEY: z.string().optional(), LINEAR_API_KEY: z.string().optional(),
LINKUP_API_KEY: z.string().optional(),
}); });
export type EditConnectorFormValues = z.infer<typeof editConnectorSchema>; export type EditConnectorFormValues = z.infer<typeof editConnectorSchema>;

View file

@ -59,7 +59,8 @@ export function useConnectorEditPage(connectorId: number, searchSpaceId: string)
NOTION_INTEGRATION_TOKEN: config.NOTION_INTEGRATION_TOKEN || "", NOTION_INTEGRATION_TOKEN: config.NOTION_INTEGRATION_TOKEN || "",
SERPER_API_KEY: config.SERPER_API_KEY || "", SERPER_API_KEY: config.SERPER_API_KEY || "",
TAVILY_API_KEY: config.TAVILY_API_KEY || "", TAVILY_API_KEY: config.TAVILY_API_KEY || "",
LINEAR_API_KEY: config.LINEAR_API_KEY || "" LINEAR_API_KEY: config.LINEAR_API_KEY || "",
LINKUP_API_KEY: config.LINKUP_API_KEY || ""
}); });
if (currentConnector.connector_type === 'GITHUB_CONNECTOR') { if (currentConnector.connector_type === 'GITHUB_CONNECTOR') {
const savedRepos = config.repo_full_names || []; const savedRepos = config.repo_full_names || [];
@ -164,6 +165,12 @@ export function useConnectorEditPage(connectorId: number, searchSpaceId: string)
newConfig = { LINEAR_API_KEY: formData.LINEAR_API_KEY }; newConfig = { LINEAR_API_KEY: formData.LINEAR_API_KEY };
} }
break; break;
case 'LINKUP_API':
if (formData.LINKUP_API_KEY !== originalConfig.LINKUP_API_KEY) {
if (!formData.LINKUP_API_KEY) { toast.error("Linkup API Key cannot be empty."); setIsSaving(false); return; }
newConfig = { LINKUP_API_KEY: formData.LINKUP_API_KEY };
}
break;
} }
if (newConfig !== null) { if (newConfig !== null) {
@ -203,6 +210,8 @@ export function useConnectorEditPage(connectorId: number, searchSpaceId: string)
editForm.setValue('TAVILY_API_KEY', newlySavedConfig.TAVILY_API_KEY || ""); editForm.setValue('TAVILY_API_KEY', newlySavedConfig.TAVILY_API_KEY || "");
} else if(connector.connector_type === 'LINEAR_CONNECTOR') { } else if(connector.connector_type === 'LINEAR_CONNECTOR') {
editForm.setValue('LINEAR_API_KEY', newlySavedConfig.LINEAR_API_KEY || ""); editForm.setValue('LINEAR_API_KEY', newlySavedConfig.LINEAR_API_KEY || "");
} else if(connector.connector_type === 'LINKUP_API') {
editForm.setValue('LINKUP_API_KEY', newlySavedConfig.LINKUP_API_KEY || "");
} }
} }
if (connector.connector_type === 'GITHUB_CONNECTOR') { if (connector.connector_type === 'GITHUB_CONNECTOR') {

View file

@ -7,6 +7,7 @@ export const getConnectorTypeDisplay = (type: string): string => {
"NOTION_CONNECTOR": "Notion", "NOTION_CONNECTOR": "Notion",
"GITHUB_CONNECTOR": "GitHub", "GITHUB_CONNECTOR": "GitHub",
"LINEAR_CONNECTOR": "Linear", "LINEAR_CONNECTOR": "Linear",
"LINKUP_API": "Linkup",
}; };
return typeMap[type] || type; return typeMap[type] || type;
}; };