diff --git a/app/__pycache__/main.cpython-312.pyc b/app/__pycache__/main.cpython-312.pyc index b7576a7..b5a03bd 100644 Binary files a/app/__pycache__/main.cpython-312.pyc and b/app/__pycache__/main.cpython-312.pyc differ diff --git a/app/db/__pycache__/models.cpython-312.pyc b/app/db/__pycache__/models.cpython-312.pyc index e7219a5..b636913 100644 Binary files a/app/db/__pycache__/models.cpython-312.pyc and b/app/db/__pycache__/models.cpython-312.pyc differ diff --git a/app/db/models.py b/app/db/models.py index a99459a..dd4af74 100644 --- a/app/db/models.py +++ b/app/db/models.py @@ -1,10 +1,11 @@ import enum -from db.db import Base from sqlalchemy import Column, DateTime, ForeignKey, Integer, String, Table, Text, func from sqlalchemy.orm import declarative_mixin, relationship from sqlalchemy.types import Enum +from db.db import Base + @declarative_mixin class TimestampMixin: @@ -48,6 +49,27 @@ company_sector_association = Table( Column("sector_id", Integer, ForeignKey("sectors.id")), ) +project_sector_association = Table( + "project_sector", + Base.metadata, + Column("project_id", Integer, ForeignKey("projects.id")), + Column("sector_id", Integer, ForeignKey("sectors.id")), +) + +project_investor_association = Table( + "project_investors", + Base.metadata, + Column("project_id", Integer, ForeignKey("projects.id")), + Column("investor_id", Integer, ForeignKey("investors.id")), +) + +project_company_association = Table( + "project_companies", + Base.metadata, + Column("project_id", Integer, ForeignKey("projects.id")), + Column("company_id", Integer, ForeignKey("companies.id")), +) + class InvestorTable(Base, TimestampMixin): __tablename__ = "investors" @@ -62,19 +84,27 @@ class InvestorTable(Base, TimestampMixin): stage_focus = Column(Enum(InvestmentStage), nullable=True) number_of_investments = Column(Integer, default=0, nullable=True) + team_members = relationship("InvestorMember", back_populates="investor") + # Relationship to portfolio companies portfolio_companies = relationship( "CompanyTable", secondary=investor_company_association, back_populates="investors", ) - team_members = relationship("InvestorMember", back_populates="investor") + sectors = relationship( "SectorTable", secondary=investor_sector_association, back_populates="investors", ) + projects = relationship( + "ProjectTable", + secondary=project_investor_association, + back_populates="investors", + ) + class InvestorMember(Base, TimestampMixin): __tablename__ = "investor_members" @@ -110,6 +140,12 @@ class CompanyTable(Base, TimestampMixin): "SectorTable", secondary=company_sector_association, back_populates="companies" ) + projects = relationship( + "ProjectTable", + secondary=project_company_association, + back_populates="companies", + ) + class CompanyMember(Base, TimestampMixin): __tablename__ = "company_members" @@ -138,3 +174,33 @@ class SectorTable(Base, TimestampMixin): companies = relationship( "CompanyTable", secondary=company_sector_association, back_populates="sectors" ) + + projects = relationship( + "ProjectTable", secondary=project_sector_association, back_populates="sector" + ) + + +class ProjectTable(Base, TimestampMixin): + __tablename__ = "projects" + + id = Column(Integer, primary_key=True, index=True) + name = Column(String, nullable=False) + valuation = Column(Integer, nullable=True) + + stage = Column(Enum(InvestmentStage), nullable=True) + location = Column(String, nullable=True) + description = Column(Text, nullable=True) + start_date = Column(DateTime, nullable=True) + end_date = Column(DateTime, nullable=True) + + sector = relationship( + "SectorTable", secondary=project_sector_association, back_populates="projects" + ) + investors = relationship( + "InvestorTable", + secondary=project_investor_association, + back_populates="projects", + ) + companies = relationship( + "CompanyTable", secondary=project_company_association, back_populates="projects" + ) diff --git a/app/main.py b/app/main.py index c32d579..14f0f39 100644 --- a/app/main.py +++ b/app/main.py @@ -5,7 +5,7 @@ from db.db import Base, db_dependency, engine from dotenv import load_dotenv from fastapi import FastAPI, File, Form, UploadFile from pydantic import BaseModel -from routers import companies, investors +from routers import companies, investors, projects from schemas.router_schemas import InvestorList from services.llm_parser import InvestorProcessor from services.querying import QueryProcessor @@ -78,6 +78,8 @@ async def query_investors(request: QueryRequest): app.include_router(investors.router) app.include_router(companies.router) +app.include_router(projects.router) + if __name__ == "__main__": import uvicorn diff --git a/app/routers/__pycache__/investors.cpython-312.pyc b/app/routers/__pycache__/investors.cpython-312.pyc index 7653798..0c21826 100644 Binary files a/app/routers/__pycache__/investors.cpython-312.pyc and b/app/routers/__pycache__/investors.cpython-312.pyc differ diff --git a/app/routers/investors.py b/app/routers/investors.py index d687b8c..d7078fc 100644 --- a/app/routers/investors.py +++ b/app/routers/investors.py @@ -231,4 +231,6 @@ def delete_investor(investor_id: int, db: Session = Depends(get_db)): db.delete(db_investor) db.commit() - return {"message": "Investor deleted successfully"} \ No newline at end of file + return {"message": "Investor deleted successfully"} + + diff --git a/app/routers/projects.py b/app/routers/projects.py new file mode 100644 index 0000000..ec49cd2 --- /dev/null +++ b/app/routers/projects.py @@ -0,0 +1,447 @@ +from typing import List, Optional + +from db.db import get_db +from db.models import ( + CompanyTable, + InvestorTable, + ProjectTable, + SectorTable, +) +from fastapi import APIRouter, Depends, HTTPException, Query +from schemas.project_schemas import ( + InvestmentStage, + ProjectCreate, + ProjectData, + ProjectUpdate, +) +from sqlalchemy.orm import Session, selectinload + +router = APIRouter(tags=["Project Routes"]) + + +@router.get("/projects", response_model=List[ProjectData]) +def read_projects(db: Session = Depends(get_db)): + """Get all projects with their related data""" + projects = ( + db.query(ProjectTable) + .options( + selectinload(ProjectTable.sector), + selectinload(ProjectTable.investors), + selectinload(ProjectTable.companies), + ) + .all() + ) + + # Transform ProjectTable objects to ProjectData format + project_data_list = [] + for project in projects: + project_data = ProjectData( + project=project, + sector=project.sector, + investors=project.investors, + companies=project.companies, + ) + project_data_list.append(project_data) + + return project_data_list + + +@router.get("/projects/{project_id}", response_model=ProjectData) +def read_project(project_id: int, db: Session = Depends(get_db)): + """Get a specific project by ID""" + project = ( + db.query(ProjectTable) + .options( + selectinload(ProjectTable.sector), + selectinload(ProjectTable.investors), + selectinload(ProjectTable.companies), + ) + .filter(ProjectTable.id == project_id) + .first() + ) + + if not project: + raise HTTPException(status_code=404, detail="Project not found") + + return ProjectData( + project=project, + sector=project.sector, + investors=project.investors, + companies=project.companies, + ) + + +@router.post("/projects", response_model=ProjectData) +def create_project(project: ProjectCreate, db: Session = Depends(get_db)): + """Create a new project""" + db_project = ProjectTable(**project.dict()) + db.add(db_project) + db.commit() + db.refresh(db_project) + + # Reload with relationships + db_project = ( + db.query(ProjectTable) + .options( + selectinload(ProjectTable.sector), + selectinload(ProjectTable.investors), + selectinload(ProjectTable.companies), + ) + .filter(ProjectTable.id == db_project.id) + .first() + ) + + return ProjectData( + project=db_project, + sector=db_project.sector, + investors=db_project.investors, + companies=db_project.companies, + ) + + +@router.put("/projects/{project_id}", response_model=ProjectData) +def update_project( + project_id: int, project: ProjectUpdate, db: Session = Depends(get_db) +): + """Update an existing project""" + db_project = db.query(ProjectTable).filter(ProjectTable.id == project_id).first() + + if not db_project: + raise HTTPException(status_code=404, detail="Project not found") + + # Update only provided fields + update_data = project.dict(exclude_unset=True) + for key, value in update_data.items(): + setattr(db_project, key, value) + + db.commit() + db.refresh(db_project) + + # Reload with relationships + db_project = ( + db.query(ProjectTable) + .options( + selectinload(ProjectTable.sector), + selectinload(ProjectTable.investors), + selectinload(ProjectTable.companies), + ) + .filter(ProjectTable.id == project_id) + .first() + ) + + return ProjectData( + project=db_project, + sector=db_project.sector, + investors=db_project.investors, + companies=db_project.companies, + ) + + +@router.delete("/projects/{project_id}") +def delete_project(project_id: int, db: Session = Depends(get_db)): + """Delete a project""" + db_project = db.query(ProjectTable).filter(ProjectTable.id == project_id).first() + + if not db_project: + raise HTTPException(status_code=404, detail="Project not found") + + db.delete(db_project) + db.commit() + + return {"message": "Project deleted successfully"} + + +@router.get("/projects/filter", response_model=List[ProjectData]) +def filter_projects( + stage: Optional[InvestmentStage] = Query( + None, description="Filter by project stage" + ), + min_valuation: Optional[int] = Query(None, description="Minimum valuation"), + max_valuation: Optional[int] = Query(None, description="Maximum valuation"), + location: Optional[str] = Query(None, description="Location (partial match)"), + sector: Optional[str] = Query(None, description="Sector name (partial match)"), + investor_name: Optional[str] = Query( + None, description="Investor name (partial match)" + ), + company_name: Optional[str] = Query( + None, description="Company name (partial match)" + ), + db: Session = Depends(get_db), +): + """Filter projects based on various criteria""" + + # Start with base query + query = db.query(ProjectTable).options( + selectinload(ProjectTable.sector), + selectinload(ProjectTable.investors), + selectinload(ProjectTable.companies), + ) + + # Apply filters + if stage: + query = query.filter(ProjectTable.stage == stage) + + if min_valuation is not None: + query = query.filter(ProjectTable.valuation >= min_valuation) + + if max_valuation is not None: + query = query.filter(ProjectTable.valuation <= max_valuation) + + if location: + query = query.filter(ProjectTable.location.ilike(f"%{location}%")) + + if sector: + query = query.join(ProjectTable.sector).filter( + SectorTable.name.ilike(f"%{sector}%") + ) + + if investor_name: + query = query.join(ProjectTable.investors).filter( + InvestorTable.name.ilike(f"%{investor_name}%") + ) + + if company_name: + query = query.join(ProjectTable.companies).filter( + CompanyTable.name.ilike(f"%{company_name}%") + ) + + projects = query.all() + + # Transform to ProjectData format + project_data_list = [] + for project in projects: + project_data = ProjectData( + project=project, + sector=project.sector, + investors=project.investors, + companies=project.companies, + ) + project_data_list.append(project_data) + + return project_data_list + + +# Association management routes +@router.post("/projects/{project_id}/investors/{investor_id}") +def add_investor_to_project( + project_id: int, investor_id: int, db: Session = Depends(get_db) +): + """Add an investor to a project""" + # Check if project exists + project = db.query(ProjectTable).filter(ProjectTable.id == project_id).first() + if not project: + raise HTTPException(status_code=404, detail="Project not found") + + # Check if investor exists + investor = db.query(InvestorTable).filter(InvestorTable.id == investor_id).first() + if not investor: + raise HTTPException(status_code=404, detail="Investor not found") + + # Check if association already exists + if investor in project.investors: + raise HTTPException( + status_code=400, detail="Investor already associated with project" + ) + + # Add association + project.investors.append(investor) + db.commit() + + return {"message": "Investor added to project successfully"} + + +@router.delete("/projects/{project_id}/investors/{investor_id}") +def remove_investor_from_project( + project_id: int, investor_id: int, db: Session = Depends(get_db) +): + """Remove an investor from a project""" + # Check if project exists + project = db.query(ProjectTable).filter(ProjectTable.id == project_id).first() + if not project: + raise HTTPException(status_code=404, detail="Project not found") + + # Check if investor exists + investor = db.query(InvestorTable).filter(InvestorTable.id == investor_id).first() + if not investor: + raise HTTPException(status_code=404, detail="Investor not found") + + # Check if association exists + if investor not in project.investors: + raise HTTPException( + status_code=400, detail="Investor not associated with project" + ) + + # Remove association + project.investors.remove(investor) + db.commit() + + return {"message": "Investor removed from project successfully"} + + +@router.post("/projects/{project_id}/companies/{company_id}") +def add_company_to_project( + project_id: int, company_id: int, db: Session = Depends(get_db) +): + """Add a company to a project""" + # Check if project exists + project = db.query(ProjectTable).filter(ProjectTable.id == project_id).first() + if not project: + raise HTTPException(status_code=404, detail="Project not found") + + # Check if company exists + company = db.query(CompanyTable).filter(CompanyTable.id == company_id).first() + if not company: + raise HTTPException(status_code=404, detail="Company not found") + + # Check if association already exists + if company in project.companies: + raise HTTPException( + status_code=400, detail="Company already associated with project" + ) + + # Add association + project.companies.append(company) + db.commit() + + return {"message": "Company added to project successfully"} + + +@router.delete("/projects/{project_id}/companies/{company_id}") +def remove_company_from_project( + project_id: int, company_id: int, db: Session = Depends(get_db) +): + """Remove a company from a project""" + # Check if project exists + project = db.query(ProjectTable).filter(ProjectTable.id == project_id).first() + if not project: + raise HTTPException(status_code=404, detail="Project not found") + + # Check if company exists + company = db.query(CompanyTable).filter(CompanyTable.id == company_id).first() + if not company: + raise HTTPException(status_code=404, detail="Company not found") + + # Check if association exists + if company not in project.companies: + raise HTTPException( + status_code=400, detail="Company not associated with project" + ) + + # Remove association + project.companies.remove(company) + db.commit() + + return {"message": "Company removed from project successfully"} + + +@router.post("/projects/{project_id}/sectors/{sector_id}") +def add_sector_to_project( + project_id: int, sector_id: int, db: Session = Depends(get_db) +): + """Add a sector to a project""" + # Check if project exists + project = db.query(ProjectTable).filter(ProjectTable.id == project_id).first() + if not project: + raise HTTPException(status_code=404, detail="Project not found") + + # Check if sector exists + sector = db.query(SectorTable).filter(SectorTable.id == sector_id).first() + if not sector: + raise HTTPException(status_code=404, detail="Sector not found") + + # Check if association already exists + if sector in project.sector: + raise HTTPException( + status_code=400, detail="Sector already associated with project" + ) + + # Add association + project.sector.append(sector) + db.commit() + + return {"message": "Sector added to project successfully"} + + +@router.delete("/projects/{project_id}/sectors/{sector_id}") +def remove_sector_from_project( + project_id: int, sector_id: int, db: Session = Depends(get_db) +): + """Remove a sector from a project""" + # Check if project exists + project = db.query(ProjectTable).filter(ProjectTable.id == project_id).first() + if not project: + raise HTTPException(status_code=404, detail="Project not found") + + # Check if sector exists + sector = db.query(SectorTable).filter(SectorTable.id == sector_id).first() + if not sector: + raise HTTPException(status_code=404, detail="Sector not found") + + # Check if association exists + if sector not in project.sector: + raise HTTPException( + status_code=400, detail="Sector not associated with project" + ) + + # Remove association + project.sector.remove(sector) + db.commit() + + return {"message": "Sector removed from project successfully"} + + +# Bulk association management +@router.post("/projects/{project_id}/investors") +def add_multiple_investors_to_project( + project_id: int, investor_ids: List[int], db: Session = Depends(get_db) +): + """Add multiple investors to a project""" + # Check if project exists + project = db.query(ProjectTable).filter(ProjectTable.id == project_id).first() + if not project: + raise HTTPException(status_code=404, detail="Project not found") + + # Get all investors + investors = db.query(InvestorTable).filter(InvestorTable.id.in_(investor_ids)).all() + + if len(investors) != len(investor_ids): + raise HTTPException(status_code=404, detail="One or more investors not found") + + # Add associations (only if not already associated) + added_count = 0 + for investor in investors: + if investor not in project.investors: + project.investors.append(investor) + added_count += 1 + + db.commit() + + return {"message": f"Added {added_count} investors to project successfully"} + + +@router.post("/projects/{project_id}/companies") +def add_multiple_companies_to_project( + project_id: int, company_ids: List[int], db: Session = Depends(get_db) +): + """Add multiple companies to a project""" + # Check if project exists + project = db.query(ProjectTable).filter(ProjectTable.id == project_id).first() + if not project: + raise HTTPException(status_code=404, detail="Project not found") + + # Get all companies + companies = db.query(CompanyTable).filter(CompanyTable.id.in_(company_ids)).all() + + if len(companies) != len(company_ids): + raise HTTPException(status_code=404, detail="One or more companies not found") + + # Add associations (only if not already associated) + added_count = 0 + for company in companies: + if company not in project.companies: + project.companies.append(company) + added_count += 1 + + db.commit() + + return {"message": f"Added {added_count} companies to project successfully"} diff --git a/app/schemas/__pycache__/py_schemas.cpython-312.pyc b/app/schemas/__pycache__/py_schemas.cpython-312.pyc index e89ff8e..9dd3893 100644 Binary files a/app/schemas/__pycache__/py_schemas.cpython-312.pyc and b/app/schemas/__pycache__/py_schemas.cpython-312.pyc differ diff --git a/app/schemas/project_schemas.py b/app/schemas/project_schemas.py new file mode 100644 index 0000000..51663a3 --- /dev/null +++ b/app/schemas/project_schemas.py @@ -0,0 +1,117 @@ +from datetime import datetime +from enum import Enum +from typing import List, Optional + +from pydantic import BaseModel + + +class InvestmentStage(str, Enum): + SEED = "SEED" + SERIES_A = "SERIES_A" + SERIES_B = "SERIES_B" + SERIES_C = "SERIES_C" + GROWTH = "GROWTH" + LATE_STAGE = "LATE_STAGE" + + +class SectorSchema(BaseModel): + id: int + name: str + + class Config: + from_attributes = True + + +class InvestorSchema(BaseModel): + id: int + name: str + description: Optional[str] + aum: int | None + check_size_lower: int | None + check_size_upper: int | None + geographic_focus: str | None + stage_focus: InvestmentStage + number_of_investments: int | None + created_at: Optional[datetime] = None + updated_at: Optional[datetime] = None + + class Config: + from_attributes = True + + +class CompanySchema(BaseModel): + id: int + name: str + industry: str | None + location: str | None + description: Optional[str] + founded_year: Optional[int] + website: Optional[str] + created_at: Optional[datetime] = None + updated_at: Optional[datetime] = None + + class Config: + from_attributes = True + + +class ProjectSchema(BaseModel): + id: int + name: str + valuation: int | None + stage: InvestmentStage | None + location: str | None + description: Optional[str] + start_date: Optional[datetime] + end_date: Optional[datetime] + created_at: Optional[datetime] = None + updated_at: Optional[datetime] = None + + class Config: + from_attributes = True + + +class ProjectCreate(BaseModel): + name: str + valuation: Optional[int] = None + stage: Optional[InvestmentStage] = None + location: Optional[str] = None + description: Optional[str] = None + start_date: Optional[datetime] = None + end_date: Optional[datetime] = None + + +class ProjectUpdate(BaseModel): + name: Optional[str] = None + valuation: Optional[int] = None + stage: Optional[InvestmentStage] = None + location: Optional[str] = None + description: Optional[str] = None + start_date: Optional[datetime] = None + end_date: Optional[datetime] = None + + +class ProjectData(BaseModel): + """Comprehensive project data schema""" + + project: ProjectSchema + sector: List[SectorSchema] + investors: List[InvestorSchema] + companies: List[CompanySchema] + + class Config: + from_attributes = True + + +class ProjectInvestorAssociation(BaseModel): + project_id: int + investor_id: int + + +class ProjectCompanyAssociation(BaseModel): + project_id: int + company_id: int + + +class ProjectSectorAssociation(BaseModel): + project_id: int + sector_id: int diff --git a/app/services/__pycache__/llm_parser.cpython-312.pyc b/app/services/__pycache__/llm_parser.cpython-312.pyc index 17400ff..e64b117 100644 Binary files a/app/services/__pycache__/llm_parser.cpython-312.pyc and b/app/services/__pycache__/llm_parser.cpython-312.pyc differ diff --git a/app/services/llm_parser.py b/app/services/llm_parser.py index 4e5dc19..0387a6c 100644 --- a/app/services/llm_parser.py +++ b/app/services/llm_parser.py @@ -21,7 +21,7 @@ class InvestorProcessor: self.llm = ChatOpenAI( api_key=os.getenv("OPENROUTER_API_KEY"), base_url="https://openrouter.ai/api/v1", - model="openai/gpt-5-nano", + model="google/gemini-2.5-flash-lite", temperature=0, )