mirror of
https://github.com/MODSetter/SurfSense.git
synced 2025-09-10 14:28:57 +00:00
feat: Added LinkUP Search Engine Connector
This commit is contained in:
parent
273c16a611
commit
3675505eb1
18 changed files with 492 additions and 27 deletions
45
surfsense_backend/alembic/versions/4_add_linkup_api_enum.py
Normal file
45
surfsense_backend/alembic/versions/4_add_linkup_api_enum.py
Normal 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 ###
|
|
@ -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)}"
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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
|
||||||
|
|
17
surfsense_backend/app/temp_test.py
Normal file
17
surfsense_backend/app/temp_test.py
Normal file
File diff suppressed because one or more lines are too long
|
@ -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": [],
|
||||||
|
}, []
|
||||||
|
|
|
@ -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",
|
||||||
|
|
15
surfsense_backend/uv.lock
generated
15
surfsense_backend/uv.lock
generated
|
@ -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" },
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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">
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
|
@ -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",
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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':
|
||||||
|
|
|
@ -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>;
|
||||||
|
|
|
@ -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') {
|
||||||
|
|
|
@ -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;
|
||||||
};
|
};
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue