finta-aurora-mcp 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- finta_aurora_mcp/__init__.py +3 -0
- finta_aurora_mcp/auth.py +105 -0
- finta_aurora_mcp/fix_org_info.py +44 -0
- finta_aurora_mcp/mcp.py +498 -0
- finta_aurora_mcp-1.0.0.dist-info/METADATA +216 -0
- finta_aurora_mcp-1.0.0.dist-info/RECORD +19 -0
- finta_aurora_mcp-1.0.0.dist-info/WHEEL +5 -0
- finta_aurora_mcp-1.0.0.dist-info/top_level.txt +2 -0
- tools/__init__.py +23 -0
- tools/addInvestorToTrackerTool.py +114 -0
- tools/contactSupportTool.py +7 -0
- tools/editInvestorTool.py +653 -0
- tools/getInvestorTool.py +109 -0
- tools/imageTool.py +77 -0
- tools/pdfScraperTool.py +31 -0
- tools/pineconeKnowledgeTool.py +35 -0
- tools/pineconeResourceTool.py +27 -0
- tools/serpAPITool.py +16 -0
- tools/webScraperTool.py +30 -0
tools/getInvestorTool.py
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
from langchain_core.tools import StructuredTool, ToolException
|
|
2
|
+
import requests
|
|
3
|
+
from pydantic.v1 import BaseModel, Field, ValidationError
|
|
4
|
+
from typing import Optional
|
|
5
|
+
import os
|
|
6
|
+
from langchain_core.runnables.config import RunnableConfig
|
|
7
|
+
|
|
8
|
+
API_BASE_URL = os.environ.get('API_BASE_URL', '')
|
|
9
|
+
# API_BASE_URL = "https://us-central1-equity-token-1.cloudfunctions.net/"
|
|
10
|
+
|
|
11
|
+
class getInvestorInfoInput(BaseModel):
|
|
12
|
+
investorId: Optional[str] = Field(default="", description="Investor's ID. Use this if you have the ID.")
|
|
13
|
+
investorName: Optional[str] = Field(default="", description="Investor's name (e.g., 'David Rose', 'John Smith'). Use this to search by name if you don't have the ID.")
|
|
14
|
+
investorEmail: Optional[str] = Field(default="", description="Investor's email address. Use this to search by email if you don't have the ID or name.")
|
|
15
|
+
|
|
16
|
+
def call_get_investor_info(config: RunnableConfig, investorId: str = "", investorName: str = "", investorEmail: str = ""):
|
|
17
|
+
"""Get investor information by ID, name, or email. This is for READ-ONLY queries to retrieve investor data."""
|
|
18
|
+
org_info = config.get('configurable', {}).get("org_info", {})
|
|
19
|
+
handle = org_info.get("handle")
|
|
20
|
+
|
|
21
|
+
if not handle:
|
|
22
|
+
raise ToolException("Organization handle not found in config")
|
|
23
|
+
|
|
24
|
+
# If ID provided, use direct lookup
|
|
25
|
+
if investorId:
|
|
26
|
+
url = API_BASE_URL + 'fintaAI/tools/get-investor-info'
|
|
27
|
+
params = {
|
|
28
|
+
"investorId": investorId,
|
|
29
|
+
"organizationHandle": handle
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
try:
|
|
33
|
+
response = requests.get(url, params=params, timeout=10)
|
|
34
|
+
response.raise_for_status()
|
|
35
|
+
return response.json()
|
|
36
|
+
except requests.exceptions.RequestException as e:
|
|
37
|
+
raise ToolException(f"Failed to get investor info: {str(e)}")
|
|
38
|
+
|
|
39
|
+
# If name or email provided, search first
|
|
40
|
+
elif investorName or investorEmail:
|
|
41
|
+
search_url = API_BASE_URL + "fintaAI/tools/search-investor"
|
|
42
|
+
search_params = {
|
|
43
|
+
"organizationHandle": handle,
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if investorEmail:
|
|
47
|
+
search_params["email"] = investorEmail
|
|
48
|
+
elif investorName:
|
|
49
|
+
search_params["name"] = investorName
|
|
50
|
+
|
|
51
|
+
try:
|
|
52
|
+
search_response = requests.get(search_url, params=search_params, timeout=10)
|
|
53
|
+
search_response.raise_for_status()
|
|
54
|
+
search_result = search_response.json()
|
|
55
|
+
except requests.exceptions.RequestException as e:
|
|
56
|
+
raise ToolException(f"Failed to search for investor: {str(e)}")
|
|
57
|
+
|
|
58
|
+
if not search_result.get("investors") or len(search_result["investors"]) == 0:
|
|
59
|
+
raise ToolException(f"No investor found with {'email: ' + investorEmail if investorEmail else 'name: ' + investorName}")
|
|
60
|
+
|
|
61
|
+
if len(search_result["investors"]) > 1:
|
|
62
|
+
# Multiple matches - return info about all of them
|
|
63
|
+
investor_list = []
|
|
64
|
+
for inv in search_result["investors"][:5]:
|
|
65
|
+
inv_data = inv.get('investor', {})
|
|
66
|
+
investor_list.append({
|
|
67
|
+
'id': inv['id'],
|
|
68
|
+
'name': f"{inv_data.get('firstName', '')} {inv_data.get('lastName', '')}".strip(),
|
|
69
|
+
'email': inv_data.get('email', ''),
|
|
70
|
+
'status': inv.get('status', ''),
|
|
71
|
+
'organizationName': inv_data.get('organizationName', ''),
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
'investorInfo': {
|
|
76
|
+
'multipleMatches': True,
|
|
77
|
+
'count': len(search_result["investors"]),
|
|
78
|
+
'investors': investor_list,
|
|
79
|
+
'message': f"Found {len(search_result['investors'])} investors with the same name. Here are the details:"
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
# Found exactly one match - get full info using the ID
|
|
84
|
+
matched_id = search_result["investors"][0]["id"]
|
|
85
|
+
url = API_BASE_URL + 'fintaAI/tools/get-investor-info'
|
|
86
|
+
params = {
|
|
87
|
+
"investorId": matched_id,
|
|
88
|
+
"organizationHandle": handle
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
try:
|
|
92
|
+
response = requests.get(url, params=params, timeout=10)
|
|
93
|
+
response.raise_for_status()
|
|
94
|
+
return response.json()
|
|
95
|
+
except requests.exceptions.RequestException as e:
|
|
96
|
+
raise ToolException(f"Failed to get investor info: {str(e)}")
|
|
97
|
+
|
|
98
|
+
else:
|
|
99
|
+
raise ToolException("Either investorId, investorName, or investorEmail must be provided")
|
|
100
|
+
|
|
101
|
+
get_investor_info_tool= StructuredTool.from_function(
|
|
102
|
+
func=call_get_investor_info,
|
|
103
|
+
name="GetInvestorInfo",
|
|
104
|
+
description="Given an input of investor ID, name, or email, this function returns all relevant investor information. Can search by investorName or investorEmail if ID is not provided.",
|
|
105
|
+
args_schema=getInvestorInfoInput,
|
|
106
|
+
return_direct=True,
|
|
107
|
+
verbose=True
|
|
108
|
+
)
|
|
109
|
+
|
tools/imageTool.py
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
from openai import OpenAI
|
|
2
|
+
from langchain_core.tools import tool
|
|
3
|
+
import os
|
|
4
|
+
from dotenv import load_dotenv
|
|
5
|
+
|
|
6
|
+
if not os.getenv('GAE_APPLICATION'):
|
|
7
|
+
load_dotenv(dotenv_path='../../../../settings.ini')
|
|
8
|
+
|
|
9
|
+
OpenAI.api_key = os.environ.get('OPENAI_API_KEY', '')
|
|
10
|
+
|
|
11
|
+
client = OpenAI()
|
|
12
|
+
|
|
13
|
+
# Import additional model providers for image support
|
|
14
|
+
try:
|
|
15
|
+
from langchain_google_genai import ChatGoogleGenerativeAI
|
|
16
|
+
except ImportError:
|
|
17
|
+
ChatGoogleGenerativeAI = None
|
|
18
|
+
|
|
19
|
+
try:
|
|
20
|
+
from langchain_anthropic import ChatAnthropic
|
|
21
|
+
except ImportError:
|
|
22
|
+
ChatAnthropic = None
|
|
23
|
+
|
|
24
|
+
@tool
|
|
25
|
+
def image_tool(query, imageUrl, model_name="gpt-4o"):
|
|
26
|
+
"""Takes in images stored in Firebase URLs and returns a summary of the image.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
query: The text query about the image
|
|
30
|
+
imageUrl: URL of the image to analyze
|
|
31
|
+
model_name: Model to use for image analysis (default: gpt-4o)
|
|
32
|
+
Supported: gpt-4o, gpt-4o-mini, gemini-pro, gemini-1.5-pro,
|
|
33
|
+
claude-3-opus, claude-3-sonnet, claude-3-haiku,
|
|
34
|
+
claude-3.5-sonnet, claude-3.5-opus
|
|
35
|
+
"""
|
|
36
|
+
# Use OpenAI for image analysis (best support for vision)
|
|
37
|
+
# Note: Gemini and Claude also support images, but OpenAI GPT-4o has the best vision capabilities
|
|
38
|
+
# For now, we'll use OpenAI, but you can extend this to support other providers
|
|
39
|
+
|
|
40
|
+
# Check if it's a Gemini model
|
|
41
|
+
if model_name.startswith('gemini'):
|
|
42
|
+
if ChatGoogleGenerativeAI is None:
|
|
43
|
+
raise ValueError("langchain-google-genai package is required for Gemini models")
|
|
44
|
+
# Gemini vision support would go here
|
|
45
|
+
# For now, fallback to OpenAI for image analysis
|
|
46
|
+
model_name = "gpt-4o"
|
|
47
|
+
|
|
48
|
+
# Check if it's a Claude model
|
|
49
|
+
if model_name.startswith('claude') or model_name == 'model-garden-anthropic':
|
|
50
|
+
if ChatAnthropic is None:
|
|
51
|
+
raise ValueError("langchain-anthropic package is required for Claude models")
|
|
52
|
+
# Claude vision support would go here
|
|
53
|
+
# For now, fallback to OpenAI for image analysis
|
|
54
|
+
model_name = "gpt-4o"
|
|
55
|
+
|
|
56
|
+
# Use OpenAI for image analysis (best vision support)
|
|
57
|
+
response = client.chat.completions.create(
|
|
58
|
+
model=model_name if model_name.startswith('gpt') else "gpt-4o",
|
|
59
|
+
messages=[
|
|
60
|
+
{
|
|
61
|
+
"role": "user",
|
|
62
|
+
"content": [
|
|
63
|
+
{"type": "text", "text": query},
|
|
64
|
+
*[
|
|
65
|
+
{
|
|
66
|
+
"type": "image_url",
|
|
67
|
+
"image_url": {
|
|
68
|
+
"url": imageUrl,
|
|
69
|
+
"detail": "high"
|
|
70
|
+
},
|
|
71
|
+
}
|
|
72
|
+
]
|
|
73
|
+
],
|
|
74
|
+
}
|
|
75
|
+
], stream=False
|
|
76
|
+
)
|
|
77
|
+
return response.choices[0].message.content
|
tools/pdfScraperTool.py
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
from langchain_core.tools import tool
|
|
2
|
+
import requests
|
|
3
|
+
from PyPDF2 import PdfReader
|
|
4
|
+
import io
|
|
5
|
+
|
|
6
|
+
@tool
|
|
7
|
+
def pdf_scraper_tool(pdfUrl: str) -> str:
|
|
8
|
+
""""When given any url for a PDF, this tool downloads the PDF and returns the text"""
|
|
9
|
+
return parsePDF(pdfUrl)
|
|
10
|
+
|
|
11
|
+
def parsePDF(url):
|
|
12
|
+
pdf_stream = download_file(url)
|
|
13
|
+
if pdf_stream:
|
|
14
|
+
extracted_text = extract_text_from_pdf(pdf_stream)
|
|
15
|
+
return extracted_text
|
|
16
|
+
else:
|
|
17
|
+
return "Failed to download the file."
|
|
18
|
+
|
|
19
|
+
def extract_text_from_pdf(file_stream):
|
|
20
|
+
reader = PdfReader(file_stream)
|
|
21
|
+
text = ''
|
|
22
|
+
for page in reader.pages:
|
|
23
|
+
text += page.extract_text()
|
|
24
|
+
return text
|
|
25
|
+
|
|
26
|
+
def download_file(url):
|
|
27
|
+
response = requests.get(url)
|
|
28
|
+
if response.status_code == 200:
|
|
29
|
+
return io.BytesIO(response.content)
|
|
30
|
+
else:
|
|
31
|
+
return None
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
from openai import OpenAI
|
|
2
|
+
from langchain_openai import OpenAIEmbeddings
|
|
3
|
+
from langchain_core.tools import tool
|
|
4
|
+
from langchain_pinecone import PineconeVectorStore
|
|
5
|
+
from dotenv import load_dotenv
|
|
6
|
+
import os
|
|
7
|
+
|
|
8
|
+
client = OpenAI()
|
|
9
|
+
embeddings = OpenAIEmbeddings(model="text-embedding-ada-002")
|
|
10
|
+
embeddings.openai_api_key = os.environ.get('OPENAI_API_KEY', '')
|
|
11
|
+
|
|
12
|
+
OpenAI.api_key = os.environ.get('OPENAI_API_KEY', '')
|
|
13
|
+
PINECONE_API_KEY = os.environ.get('PINECONE_API_KEY', '')
|
|
14
|
+
PINECONE_INDEX = os.environ.get('PINECONE_INDEX', '')
|
|
15
|
+
|
|
16
|
+
phase_mapping = {
|
|
17
|
+
"Prepare Deal Materials": "Phase 1: Prepare Deal Materials",
|
|
18
|
+
"Prospect Target Investors": "Phase 2: Prospect Target",
|
|
19
|
+
"Deal Outreach": "Phase 3: Deal Outreach"
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
# Pinecone tool - using langchain-pinecone package (LangChain v1)
|
|
23
|
+
index = PineconeVectorStore(
|
|
24
|
+
index_name=PINECONE_INDEX,
|
|
25
|
+
embedding=embeddings,
|
|
26
|
+
namespace="Finta Knowledge",
|
|
27
|
+
pinecone_api_key=PINECONE_API_KEY
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@tool
|
|
32
|
+
def pinecone_finta_knowledge_tool(query):
|
|
33
|
+
"Answers any questions about the Finta website with how-to-guides for using Deal Rooms, Investor Tracker CRM, how to prospect leads, the Cap Table, and anything else regarding the Finta Website."
|
|
34
|
+
retriever = index.as_retriever(search_type="similarity")
|
|
35
|
+
return retriever.invoke(query)
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from openai import OpenAI
|
|
2
|
+
from langchain_openai import OpenAIEmbeddings
|
|
3
|
+
from langchain_core.tools import tool
|
|
4
|
+
from langchain_pinecone import PineconeVectorStore
|
|
5
|
+
import os
|
|
6
|
+
|
|
7
|
+
client = OpenAI()
|
|
8
|
+
embeddings = OpenAIEmbeddings(model="text-embedding-ada-002")
|
|
9
|
+
embeddings.openai_api_key = os.environ.get('OPENAI_API_KEY', '')
|
|
10
|
+
|
|
11
|
+
OpenAI.api_key = os.environ.get('OPENAI_API_KEY', '')
|
|
12
|
+
PINECONE_API_KEY = os.environ.get('PINECONE_API_KEY', '')
|
|
13
|
+
PINECONE_INDEX = os.environ.get('PINECONE_INDEX', '')
|
|
14
|
+
|
|
15
|
+
# Pinecone tool - using langchain-pinecone package (LangChain v1)
|
|
16
|
+
index = PineconeVectorStore(
|
|
17
|
+
index_name=PINECONE_INDEX,
|
|
18
|
+
embedding=embeddings,
|
|
19
|
+
namespace="Finta Resources",
|
|
20
|
+
pinecone_api_key=PINECONE_API_KEY
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
@tool
|
|
24
|
+
def pinecone_finta_resource_tool(query):
|
|
25
|
+
"Answers any questions about the Finta website with how-to-guides for using Deal Rooms, Investor Tracker CRM, how to prospect leads, the Cap Table, and anything else regarding the Finta Website."
|
|
26
|
+
retriever = index.as_retriever(search_type="similarity")
|
|
27
|
+
return retriever.invoke(query)
|
tools/serpAPITool.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from langchain_core.tools import tool
|
|
2
|
+
from langchain_community.utilities import SerpAPIWrapper
|
|
3
|
+
import os
|
|
4
|
+
|
|
5
|
+
os.environ['SERPAPI_API_KEY'] = '4978c95bcf7b0f61006102929068f302898894019ccd11e0c318094223f40a0f'
|
|
6
|
+
#Serp API search
|
|
7
|
+
@tool
|
|
8
|
+
def serpAPITool(query: str) -> str:
|
|
9
|
+
"""Searches Google for web responses"""
|
|
10
|
+
try:
|
|
11
|
+
search = SerpAPIWrapper()
|
|
12
|
+
return search.run(query)
|
|
13
|
+
except ValueError as e:
|
|
14
|
+
return f"No results found for query: {query}"
|
|
15
|
+
except Exception as e:
|
|
16
|
+
return f"Error searching Google: {str(e)}"
|
tools/webScraperTool.py
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
from langchain_core.tools import tool
|
|
2
|
+
import requests
|
|
3
|
+
import os
|
|
4
|
+
import logging
|
|
5
|
+
|
|
6
|
+
logger = logging.getLogger(__name__)
|
|
7
|
+
|
|
8
|
+
API_BASE_URL = os.environ.get('API_BASE_URL', '')
|
|
9
|
+
|
|
10
|
+
cloudFunctionOrigin = API_BASE_URL + 'fintaAI/tools/scrape-website';
|
|
11
|
+
#cloudFunctionOrigin ='https://us-central1-equity-token-stage.cloudfunctions.net/fintaAI/tools/scrape-website'
|
|
12
|
+
#cloudFunctionOrigin = "http://localhost:5001/equity-token-stage/us-central1/fintaAI/tools/scrape-website"
|
|
13
|
+
|
|
14
|
+
@tool
|
|
15
|
+
def scrape_website_from_link_tool(website: str) -> str:
|
|
16
|
+
""""When given any url, this tool searches the link and returns the text"""
|
|
17
|
+
url =cloudFunctionOrigin
|
|
18
|
+
data = {
|
|
19
|
+
"url": website,
|
|
20
|
+
}
|
|
21
|
+
try:
|
|
22
|
+
response = requests.post(url, json=data)
|
|
23
|
+
response.raise_for_status()
|
|
24
|
+
return response.json().get('content', 'No content found')
|
|
25
|
+
except requests.exceptions.RequestException as e:
|
|
26
|
+
logger.error(f"Exception occurred: {e}", exc_info=True)
|
|
27
|
+
return {"error": str(e)}
|
|
28
|
+
except ValueError as e:
|
|
29
|
+
logger.error(f"JSON decode error: {e}", exc_info=True)
|
|
30
|
+
return {"error": "Invalid JSON response"}
|