import os import json import csv import time import asyncio from pydantic import BaseModel, Field from google import genai from google.genai import types # Define the structured output schema for Gemini class Vertex(BaseModel): id: str = Field(description="The unique identifier or name of the entity.") type: str = Field(description="Must be one of: Company, Executive, RiskFactor, Subsidiary") class Edge(BaseModel): source: str = Field(description="The id of the source vertex.") target: str = Field(description="The id of the target vertex.") type: str = Field(description="Must be one of: EMPLOYS, FACES_RISK, OWNS, COMPETES_WITH") class GraphExtraction(BaseModel): vertices: list[Vertex] edges: list[Edge] # Setup Gemini Client (Async) if "GEMINI_API_KEY" not in os.environ: print("ERROR: GEMINI_API_KEY environment variable not set.") exit(1) client = genai.Client() async def process_document(doc, sem): """ Process a single document through Gemini 1.5 Flash to extract graph entities. Uses a semaphore to limit concurrent API calls and prevent 429 errors. """ async with sem: text = doc.get("text", "") company = doc.get("company", "Unknown") # If the text is too large, we truncate it for extraction to save costs # (For 100M tokens, we might process full texts, but let's limit to 15k chars per doc for the demo) text_chunk = text[:15000] prompt = f""" Analyze the following SEC 10-K filing excerpt for {company}. Extract all relevant entities (Company, Executive, RiskFactor, Subsidiary) and their relationships (EMPLOYS, FACES_RISK, OWNS, COMPETES_WITH). Text excerpt: {text_chunk} """ try: # Note: We use the async client `client.aio` response = await client.aio.models.generate_content( model='gemini-2.5-flash', contents=prompt, config=types.GenerateContentConfig( response_mime_type="application/json", response_schema=GraphExtraction, temperature=0.1 ) ) # The response text will be a JSON string conforming to the GraphExtraction schema return json.loads(response.text) except Exception as e: print(f"Error extracting graph for {company}: {e}") return None async def main(): input_file = "financial_corpus.jsonl" if not os.path.exists(input_file): print(f"ERROR: {input_file} not found. Run count_tokens.py first.") return # Prepare CSV writers vertices_file = open("vertices.csv", "w", newline="", encoding="utf-8") edges_file = open("edges.csv", "w", newline="", encoding="utf-8") v_writer = csv.writer(vertices_file) e_writer = csv.writer(edges_file) # Write headers v_writer.writerow(["v_id", "v_type"]) e_writer.writerow(["source", "target", "e_type"]) # Read corpus print("Loading corpus...") docs = [] with open(input_file, 'r', encoding='utf-8') as f: for line in f: docs.append(json.loads(line)) if len(docs) >= 50: break print(f"Loaded {len(docs)} documents for extraction.") # We use a Semaphore to allow max 5 concurrent requests to Gemini to respect standard tier rate limits sem = asyncio.Semaphore(5) # Batch processing BATCH_SIZE = 50 total_vertices = 0 total_edges = 0 for i in range(0, len(docs), BATCH_SIZE): batch = docs[i:i+BATCH_SIZE] print(f"Processing batch {i//BATCH_SIZE + 1} ({len(batch)} docs)...") tasks = [process_document(doc, sem) for doc in batch] results = await asyncio.gather(*tasks) for result in results: if not result: continue # Write vertices for v in result.get("vertices", []): # Basic cleaning v_id = v.get("id", "").strip().replace("\n", " ") v_type = v.get("type", "Unknown") if v_id: v_writer.writerow([v_id, v_type]) total_vertices += 1 # Write edges for e in result.get("edges", []): source = e.get("source", "").strip().replace("\n", " ") target = e.get("target", "").strip().replace("\n", " ") e_type = e.get("type", "Unknown") if source and target: e_writer.writerow([source, target, e_type]) total_edges += 1 # Flush to disk periodically vertices_file.flush() edges_file.flush() # Sleep slightly between batches to cool down rate limits await asyncio.sleep(2) vertices_file.close() edges_file.close() print("\n--- GRAPH EXTRACTION COMPLETE ---") print(f"Total Vertices Extracted: {total_vertices:,}") print(f"Total Edges Extracted: {total_edges:,}") print("Saved to vertices.csv and edges.csv") print("Ready for TigerGraph Bulk Loading!") if __name__ == "__main__": asyncio.run(main())