From 94115a03486e322cf94b176354f5109db1debee9 Mon Sep 17 00:00:00 2001 From: Lance Martin Date: Sat, 30 Nov 2024 10:18:22 -0800 Subject: [PATCH] Test ntbk --- company_maistro.ipynb | 898 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 898 insertions(+) create mode 100644 company_maistro.ipynb diff --git a/company_maistro.ipynb b/company_maistro.ipynb new file mode 100644 index 0000000..6c916c9 --- /dev/null +++ b/company_maistro.ipynb @@ -0,0 +1,898 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 15, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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", + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "import asyncio\n", + "import operator\n", + "import json\n", + "\n", + "from tavily import TavilyClient, AsyncTavilyClient\n", + "\n", + "from langchain_anthropic import ChatAnthropic\n", + "from langchain_openai import ChatOpenAI\n", + "\n", + "from langchain_core.messages import HumanMessage, SystemMessage\n", + "from langchain_core.runnables import RunnableConfig\n", + "from langsmith import traceable\n", + "\n", + "from langgraph.constants import Send\n", + "from langgraph.graph import START, END, StateGraph\n", + "\n", + "from pydantic import BaseModel, Field\n", + "from typing_extensions import Annotated, Any, List, Optional, Literal\n", + "from dataclasses import dataclass, field\n", + "\n", + "import configuration\n", + "\n", + "# -----------------------------------------------------------------------------\n", + "# LLMs\n", + "gpt_4o = ChatOpenAI(model=\"gpt-4o\", temperature=0)\n", + "claude_3_5_sonnet = ChatAnthropic(model=\"claude-3-5-sonnet-20240620\", temperature=0)\n", + "\n", + "# -----------------------------------------------------------------------------\n", + "# Search\n", + "tavily_client = TavilyClient()\n", + "tavily_async_client = AsyncTavilyClient()\n", + "\n", + "# -----------------------------------------------------------------------------\n", + "# Utils\n", + "@traceable\n", + "async def tavily_search_async(search_queries, tavily_topic, tavily_days):\n", + " \"\"\"\n", + " Performs concurrent web searches using the Tavily API.\n", + "\n", + " Args:\n", + " search_queries (List[SearchQuery]): List of search queries to process\n", + " tavily_topic (str): Type of search to perform ('news' or 'general')\n", + " tavily_days (int): Number of days to look back for news articles (only used when tavily_topic='news')\n", + "\n", + " Returns:\n", + " List[dict]: List of search results from Tavily API, one per query\n", + "\n", + " Note:\n", + " For news searches, each result will include articles from the last `tavily_days` days.\n", + " For general searches, the time range is unrestricted.\n", + " \"\"\"\n", + "\n", + " search_tasks = []\n", + " for query in search_queries:\n", + " if tavily_topic == \"news\":\n", + " search_tasks.append(\n", + " tavily_async_client.search(\n", + " query,\n", + " max_results=5,\n", + " include_raw_content=True,\n", + " topic=\"news\",\n", + " days=tavily_days,\n", + " )\n", + " )\n", + " else:\n", + " search_tasks.append(\n", + " tavily_async_client.search(\n", + " query, max_results=5, include_raw_content=True, topic=\"general\"\n", + " )\n", + " )\n", + "\n", + " # Execute all searches concurrently\n", + " search_docs = await asyncio.gather(*search_tasks)\n", + "\n", + " return search_docs\n", + "\n", + "\n", + "def deduplicate_and_format_sources(\n", + " search_response, max_tokens_per_source, include_raw_content=True\n", + "):\n", + " \"\"\"\n", + " Takes either a single search response or list of responses from Tavily API and formats them.\n", + " Limits the raw_content to approximately max_tokens_per_source.\n", + " include_raw_content specifies whether to include the raw_content from Tavily in the formatted string.\n", + "\n", + " Args:\n", + " search_response: Either:\n", + " - A dict with a 'results' key containing a list of search results\n", + " - A list of dicts, each containing search results\n", + "\n", + " Returns:\n", + " str: Formatted string with deduplicated sources\n", + " \"\"\"\n", + " # Convert input to list of results\n", + " if isinstance(search_response, dict):\n", + " sources_list = search_response[\"results\"]\n", + " elif isinstance(search_response, list):\n", + " sources_list = []\n", + " for response in search_response:\n", + " if isinstance(response, dict) and \"results\" in response:\n", + " sources_list.extend(response[\"results\"])\n", + " else:\n", + " sources_list.extend(response)\n", + " else:\n", + " raise ValueError(\n", + " \"Input must be either a dict with 'results' or a list of search results\"\n", + " )\n", + "\n", + " # Deduplicate by URL\n", + " unique_sources = {}\n", + " for source in sources_list:\n", + " if source[\"url\"] not in unique_sources:\n", + " unique_sources[source[\"url\"]] = source\n", + "\n", + " # Format output\n", + " formatted_text = \"Sources:\\n\\n\"\n", + " for i, source in enumerate(unique_sources.values(), 1):\n", + " formatted_text += f\"Source {source['title']}:\\n===\\n\"\n", + " formatted_text += f\"URL: {source['url']}\\n===\\n\"\n", + " formatted_text += (\n", + " f\"Most relevant content from source: {source['content']}\\n===\\n\"\n", + " )\n", + " if include_raw_content:\n", + " # Using rough estimate of 4 characters per token\n", + " char_limit = max_tokens_per_source * 4\n", + " # Handle None raw_content\n", + " raw_content = source.get(\"raw_content\", \"\")\n", + " if raw_content is None:\n", + " raw_content = \"\"\n", + " print(f\"Warning: No raw_content found for source {source['url']}\")\n", + " if len(raw_content) > char_limit:\n", + " raw_content = raw_content[:char_limit] + \"... [truncated]\"\n", + " formatted_text += f\"Full source content limited to {max_tokens_per_source} tokens: {raw_content}\\n\\n\"\n", + "\n", + " return formatted_text.strip()\n", + "\n", + "\n", + "def format_all_notes(completed_notes: list[str]) -> str:\n", + " \"\"\"Format a list of notes into a string\"\"\"\n", + " formatted_str = \"\"\n", + " for idx, company_notes in enumerate(completed_notes, 1):\n", + " formatted_str += f\"\"\"\n", + "{'='*60}\n", + "Note: {idx}:\n", + "{'='*60}\n", + "Notes from research:\n", + "{company_notes}\"\"\"\n", + " return formatted_str\n", + "\n", + "\n", + "# -----------------------------------------------------------------------------\n", + "# Schema\n", + "class SearchQuery(BaseModel):\n", + " search_query: str = Field(None, description=\"Query for web search.\")\n", + "\n", + "\n", + "class Queries(BaseModel):\n", + " queries: List[SearchQuery] = Field(\n", + " description=\"List of search queries.\",\n", + " )\n", + "\n", + "\n", + "DEFAULT_EXTRACTION_SCHEMA = {\n", + " \"title\": \"CompanyInfo\",\n", + " \"description\": \"Basic information about a company\",\n", + " \"type\": \"object\",\n", + " \"properties\": {\n", + " \"company_name\": {\n", + " \"type\": \"string\",\n", + " \"description\": \"Official name of the company\"\n", + " },\n", + " \"founding_year\": {\n", + " \"type\": \"integer\",\n", + " \"description\": \"Year the company was founded\"\n", + " },\n", + " \"founder_names\": {\n", + " \"type\": \"array\",\n", + " \"items\": {\"type\": \"string\"},\n", + " \"description\": \"Names of the founding team members\"\n", + " },\n", + " \"product_description\": {\n", + " \"type\": \"string\",\n", + " \"description\": \"Brief description of the company's main product or service\"\n", + " },\n", + " \"funding_summary\": {\n", + " \"type\": \"string\",\n", + " \"description\": \"Summary of the company's funding history\"\n", + " }\n", + " },\n", + " \"required\": [\"company_name\"]\n", + "}\n", + "\n", + "@dataclass(kw_only=True)\n", + "class InputState:\n", + " \"\"\"Input state defines the interface between the graph and the user (external API).\"\"\"\n", + "\n", + " company: str\n", + " \"Company to research provided by the user.\"\n", + "\n", + " extraction_schema: dict[str, Any] = field(\n", + " default_factory=lambda: DEFAULT_EXTRACTION_SCHEMA\n", + " )\n", + " \"The json schema defines the information the agent is tasked with filling out.\"\n", + "\n", + " user_notes: Optional[dict[str, Any]] = field(default=None)\n", + " \"Any notes from the user to start the research process.\"\n", + "\n", + "\n", + "@dataclass(kw_only=True)\n", + "class OverallState:\n", + " \"\"\"Input state defines the interface between the graph and the user (external API).\"\"\"\n", + "\n", + " company: str\n", + " \"Company to research provided by the user.\"\n", + "\n", + " extraction_schema: dict[str, Any] = field(\n", + " default_factory=lambda: DEFAULT_EXTRACTION_SCHEMA\n", + " )\n", + " \"The json schema defines the information the agent is tasked with filling out.\"\n", + "\n", + " user_notes: str = field(default=None)\n", + " \"Any notes from the user to start the research process.\"\n", + "\n", + " completed_notes: Annotated[list, operator.add] = field(default_factory=list)\n", + " \"Notes from completed research related to the schema\"\n", + "\n", + " info: dict[str, Any] = field(default=None)\n", + " \"\"\"\n", + " A dictionary containing the extracted and processed information\n", + " based on the user's query and the graph's execution.\n", + " This is the primary output of the enrichment process.\n", + " \"\"\"\n", + "\n", + " is_satisfactory: bool = field(default=None)\n", + " \"True if all required fields are well populated, False otherwise\"\n", + "\n", + " reflection_search_queries: list[str] = field(default=None)\n", + " \"If is_satisfactory is False, provide targeted search queries to find the missing information\"\n", + "\n", + " reflection_steps_taken: int = field(default=0)\n", + " \"Number of times the reflection node has been executed\"\n", + "\n", + "@dataclass(kw_only=True)\n", + "class OutputState:\n", + " \"\"\"The response object for the end user.\n", + "\n", + " This class defines the structure of the output that will be provided\n", + " to the user after the graph's execution is complete.\n", + " \"\"\"\n", + "\n", + " info: dict[str, Any]\n", + " \"\"\"\n", + " A dictionary containing the extracted and processed information\n", + " based on the user's query and the graph's execution.\n", + " This is the primary output of the enrichment process.\n", + " \"\"\"\n", + "\n", + "\n", + "# -----------------------------------------------------------------------------\n", + "# Prompts\n", + "\n", + "extraction_prompt = \"\"\"Your task is to take notes gather from web research\n", + "\n", + "and extract them into the following schema. \n", + "\n", + "\n", + "{info}\n", + "\n", + "\n", + "Here are all the notes from research:\n", + "\n", + "\n", + "{notes}\n", + "\n", + " \"\"\"\n", + "\n", + "query_writer_instructions = \"\"\"You are a search query generator tasked with creating targeted search queries to gather specific company information.\n", + "\n", + "Here is the company you are researching: {company}\n", + "\n", + "Generate at most {max_search_queries} search queries that will help gather the following information:\n", + "\n", + "\n", + "{info}\n", + "\n", + "\n", + "Your query should:\n", + "1. Focus on finding factual, up-to-date company information\n", + "2. Target official sources, news, and reliable business databases\n", + "3. Prioritize finding information that matches the schema requirements\n", + "4. Include the company name and relevant business terms\n", + "5. Be specific enough to avoid irrelevant results\n", + "\n", + "Create a focused query that will maximize the chances of finding schema-relevant information.\"\"\"\n", + "\n", + "_INFO_PROMPT = \"\"\"You are doing web research on a company, {company}. \n", + "\n", + "The following schema shows the type of information we're interested in:\n", + "\n", + "\n", + "{info}\n", + "\n", + "\n", + "You have just scraped website content. Your task is to take clear, organized notes about the company, focusing on topics relevant to our interests.\n", + "\n", + "\n", + "{content}\n", + "\n", + "\n", + "Here are any additional notes from the user:\n", + "\n", + "{user_notes}\n", + "\n", + "\n", + "Please provide detailed research notes that:\n", + "1. Are well-organized and easy to read\n", + "2. Focus on topics mentioned in the schema\n", + "3. Include specific facts, dates, and figures when available\n", + "4. Maintain accuracy of the original content\n", + "5. Note when important information appears to be missing or unclear\n", + "\n", + "Remember: Don't try to format the output to match the schema - just take clear notes that capture all relevant information.\"\"\"\n", + "\n", + "REFLECTION_PROMPT = \"\"\"You are a research analyst tasked with reviewing the quality and completeness of extracted company information.\n", + "\n", + "Compare the extracted information with the required schema:\n", + "\n", + "\n", + "{schema}\n", + "\n", + "\n", + "Here is the extracted information:\n", + "\n", + "{info}\n", + "\n", + "\n", + "Analyze if all required fields are present and sufficiently populated. Consider:\n", + "1. Are any required fields missing?\n", + "2. Are any fields incomplete or containing uncertain information?\n", + "3. Are there fields with placeholder values or \"unknown\" markers?\n", + "\n", + "Return a structured response that has the following fields:\n", + "- \"is_satisfactory\": boolean, # True if all required fields are well populated, False otherwise\n", + "- \"missing_fields\": [string], # List of field names that are missing or incomplete\n", + "- \"reflection_search_queries\": [string], # If is_satisfactory is False, provide {max_search_queries} targeted search queries to find the missing information\n", + "- \"reasoning\": string # Brief explanation of your assessment\n", + "\n", + "\"\"\"\n", + "\n", + "class ReflectionOutput(BaseModel):\n", + " is_satisfactory: bool = Field(\n", + " description=\"True if all required fields are well populated, False otherwise\"\n", + " )\n", + " missing_fields: List[str] = Field(\n", + " description=\"List of field names that are missing or incomplete\"\n", + " )\n", + " reflection_search_queries: List[str] = Field(\n", + " description=\"If is_satisfactory is False, provide 1-3 targeted search queries to find the missing information\"\n", + " )\n", + " reasoning: str = Field(\n", + " description=\"Brief explanation of the assessment\"\n", + " )\n", + "\n", + "# -----------------------------------------------------------------------------\n", + "# Nodes\n", + "\n", + "async def research_company(state: OverallState, config: RunnableConfig) -> str:\n", + " \"\"\"Execute a multi-step web search and information extraction process.\n", + "\n", + " This function performs the following steps:\n", + " 1. Generates multiple search queries based on the input query\n", + " 2. Executes concurrent web searches using the Tavily API\n", + " 3. Deduplicates and formats the search results\n", + " 4. Extracts structured information based on the provided schema\n", + "\n", + " Args:\n", + " query: The initial search query string\n", + " state: Injected application state containing the extraction schema\n", + " config: Runtime configuration for the search process\n", + "\n", + " Returns:\n", + " str: Structured notes from the search results that are\n", + " relevant to the extraction schema in state.extraction_schema\n", + "\n", + " Note:\n", + " The function uses concurrent execution for multiple search queries to improve\n", + " performance and combines results from various sources for comprehensive coverage.\n", + " \"\"\"\n", + "\n", + " # Get configuration\n", + " configurable = configuration.Configuration.from_runnable_config(config)\n", + " max_search_queries = configurable.max_search_queries\n", + " max_search_results = configurable.max_search_results\n", + "\n", + " # Initialize search client\n", + " tavily_async_client = AsyncTavilyClient()\n", + "\n", + " # Generate search queries\n", + " structured_llm = claude_3_5_sonnet.with_structured_output(Queries)\n", + "\n", + " # Check reflection output - access attribute directly\n", + " reflection_output = getattr(state, \"is_satisfactory\", None)\n", + " reflection_queries = getattr(state, \"reflection_search_queries\", None)\n", + " \n", + " # If we have performed reflection and have new search queries \n", + " if reflection_output is not None and reflection_queries:\n", + " # Get generated search queries\n", + " query_list = reflection_queries\n", + " else:\n", + " # Format system instructions\n", + " query_instructions = query_writer_instructions.format(\n", + " company=state.company,\n", + " info=json.dumps(state.extraction_schema, indent=2),\n", + " max_search_queries=max_search_queries,\n", + " )\n", + "\n", + " # Generate queries\n", + " results = structured_llm.invoke(\n", + " [SystemMessage(content=query_instructions)]\n", + " + [\n", + " HumanMessage(\n", + " content=f\"Please generate a list of search queries related to the schema that you want to populate.\"\n", + " )\n", + " ]\n", + " )\n", + "\n", + " # Queries\n", + " query_list = [query.search_query for query in results.queries]\n", + "\n", + " # Search tasks\n", + " search_tasks = []\n", + " for query in query_list:\n", + " search_tasks.append(\n", + " tavily_async_client.search(\n", + " query,\n", + " max_results=max_search_results,\n", + " include_raw_content=True,\n", + " topic=\"general\",\n", + " )\n", + " )\n", + "\n", + " # Execute all searches concurrently\n", + " search_docs = await asyncio.gather(*search_tasks)\n", + "\n", + " # Deduplicate and format sources\n", + " source_str = deduplicate_and_format_sources(\n", + " search_docs, max_tokens_per_source=1000, include_raw_content=True\n", + " )\n", + "\n", + " # Generate structured notes relevant to the extraction schema\n", + " p = _INFO_PROMPT.format(\n", + " info=json.dumps(state.extraction_schema, indent=2),\n", + " content=source_str,\n", + " company=state.company,\n", + " user_notes=state.user_notes,\n", + " )\n", + " result = await claude_3_5_sonnet.ainvoke(p)\n", + " return {\"completed_notes\": [str(result.content)]}\n", + "\n", + "def gather_notes_extract_schema(state: OverallState) -> dict[str, Any]:\n", + " \"\"\"Gather notes from the web search and extract the schema fields.\"\"\"\n", + "\n", + " # Format all notes\n", + " notes = format_all_notes(state.completed_notes)\n", + "\n", + " # Extract schema fields\n", + " system_prompt = extraction_prompt.format(\n", + " info=json.dumps(state.extraction_schema, indent=2), notes=notes\n", + " )\n", + " structured_llm = claude_3_5_sonnet.with_structured_output(state.extraction_schema)\n", + " result = structured_llm.invoke(\n", + " [\n", + " SystemMessage(content=system_prompt),\n", + " HumanMessage(content=f\"Produce a structured output from these notes.\"),\n", + " ]\n", + " )\n", + " return {\"info\": result}\n", + "\n", + "def reflection(state: OverallState, config: RunnableConfig) -> dict[str, Any]:\n", + " \"\"\"Reflect on the extracted information and generate search queries to find missing information.\"\"\"\n", + "\n", + " # Get configuration\n", + " configurable = configuration.Configuration.from_runnable_config(config)\n", + "\n", + " # Generate search queries\n", + " structured_llm = claude_3_5_sonnet.with_structured_output(ReflectionOutput)\n", + "\n", + " # Format reflection prompt\n", + " system_prompt = REFLECTION_PROMPT.format(schema=json.dumps(state.extraction_schema, indent=2), \n", + " info=state.info, \n", + " max_search_queries=configurable.max_search_queries)\n", + "\n", + " # Invoke\n", + " result = structured_llm.invoke(\n", + " [\n", + " SystemMessage(content=system_prompt),\n", + " HumanMessage(content=f\"Produce a structured reflection output.\"),\n", + " ]\n", + " )\n", + "\n", + " if result.is_satisfactory:\n", + " return {\"is_satisfactory\":result.is_satisfactory}\n", + " else:\n", + " return {\"is_satisfactory\":result.is_satisfactory, \n", + " \"reflection_search_queries\": result.reflection_search_queries,\n", + " \"reflection_steps_taken\": state.reflection_steps_taken + 1}\n", + "\n", + "def route_from_reflection(state: OverallState, config: RunnableConfig) -> Literal[\"__end__\", \"research_company\"]:\n", + " \"\"\"Route the graph based on the reflection output.\"\"\"\n", + "\n", + " # Get configuration\n", + " configurable = configuration.Configuration.from_runnable_config(config)\n", + " \n", + " # If we have satisfactory results, end the process\n", + " if state.is_satisfactory:\n", + " return END\n", + " \n", + " # If results aren't satisfactory but we haven't hit max steps, continue research\n", + " if state.reflection_steps_taken <= configurable.max_reflection_steps:\n", + " return \"research_company\"\n", + " \n", + " # If we've exceeded max steps, end even if not satisfactory\n", + " return END\n", + "\n", + "# Add nodes and edges\n", + "builder = StateGraph(\n", + " OverallState,\n", + " input=InputState,\n", + " output=OutputState,\n", + " config_schema=configuration.Configuration,\n", + ")\n", + "builder.add_node(\"gather_notes_extract_schema\", gather_notes_extract_schema)\n", + "builder.add_node(\"research_company\", research_company)\n", + "builder.add_node(\"reflection\", reflection)\n", + "\n", + "builder.add_edge(START, \"research_company\")\n", + "builder.add_edge(\"research_company\", \"gather_notes_extract_schema\")\n", + "builder.add_edge(\"gather_notes_extract_schema\", \"reflection\")\n", + "builder.add_conditional_edges(\"reflection\", route_from_reflection)\n", + "\n", + "# Compile\n", + "graph = builder.compile()\n", + "\n", + "# View\n", + "from IPython.display import Image, display\n", + "display(Image(graph.get_graph(xray=1).draw_mermaid_png()))" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Warning: No raw_content found for source https://blogs.oracle.com/ateam/post/oci-generative-ai-integration-with-langchain-usecases\n" + ] + }, + { + "data": { + "text/plain": [ + "{'info': {'company_name': 'LangChain',\n", + " 'founding_year': 2022,\n", + " 'founder_names': ['Harrison Chase', 'Ankush Gola'],\n", + " 'product_description': 'LangChain is an open-source framework for developing applications using large language models (LLMs). It provides tools and APIs to simplify building LLM-driven applications like chatbots and virtual agents. The framework offers flexible abstractions and an AI-first toolkit for building context-aware reasoning applications, serving as a generic interface for nearly any LLM. It allows integration with external data sources and software workflows, and its modular approach enables developers to compare different prompts and foundation models with minimal code rewriting.',\n", + " 'funding_summary': 'LangChain has raised a total of $35 million. This includes a $25 million Series A round led by Sequoia Capital on February 15, 2024, and a $10 million Seed round led by Benchmark on April 4, 2023. The company\\'s valuation was reported to be \"at least $200 million\" as of April 2023.'}}" + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "## Test default schema \n", + "\n", + "# Create proper InputState instance\n", + "input_state = InputState(\n", + " company=\"LangChain\",\n", + ")\n", + "\n", + "# Invoke with the proper input state\n", + "result = await graph.ainvoke(input_state)\n", + "result" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'company': 'langchain',\n", + " 'completed_notes': [\"Research Notes: LangChain\\n\\nCompany Name:\\n- LangChain\\n\\nFounding Year:\\n- 2022 (mentioned in multiple sources)\\n\\nFounders:\\n- Harrison Chase (Co-Founder & CEO)\\n- Ankush Gola (Co-Founder)\\n\\nProduct Description:\\n- Open source framework for developing applications using large language models (LLMs)\\n- Provides tools and APIs to simplify building LLM-driven applications like chatbots and virtual agents\\n- Offers flexible abstractions and an AI-first toolkit\\n- Helps connect LLMs to private data sources and APIs to create context-aware, reasoning applications\\n- Includes integrations with various cloud storage, APIs, databases, and LLM providers\\n\\nFunding Summary:\\n- Total raised: $35 million (as of July 2023)\\n- Latest round: $25 million Series A (April 2023)\\n- Led by Sequoia Capital\\n- Previous round: $10 million seed round led by Benchmark (announced April 4, 2023)\\n- Valuation: At least $200 million (as of April 2023)\\n\\nAdditional Notes:\\n- Headquarters: San Francisco, California\\n- Launched in October 2022, quickly gained popularity among developers\\n- As of June 2023, had 20,000+ developers in its Discord community\\n- GitHub stats (as of July 2023): 55K stars, 7.1K forks, 13.9K users, 1.19K contributors\\n- In October 2023, introduced LangServe, a deployment tool\\n- Recently launched LangSmith, a paid LLMOps product for the entire LLM application lifecycle\\n- LangSmith had over 70,000 signups since closed beta launch in July 2023\\n- Used by over 5,000 companies monthly, including Rakuten, Elastic, Moody's, and Retool\\n\\nMissing/Unclear Information:\\n- Exact founding date not specified\\n- Complete list of founding team members not provided\\n- Detailed breakdown of funding rounds prior to the $25 million Series A is not clear\"],\n", + " 'info': {'company_name': 'LangChain',\n", + " 'founding_year': 2022,\n", + " 'founder_names': ['Harrison Chase', 'Ankush Gola'],\n", + " 'product_description': 'LangChain is an open source framework for developing applications using large language models (LLMs). It provides tools and APIs to simplify building LLM-driven applications like chatbots and virtual agents. The framework offers flexible abstractions and an AI-first toolkit, helping connect LLMs to private data sources and APIs to create context-aware, reasoning applications. It includes integrations with various cloud storage, APIs, databases, and LLM providers.',\n", + " 'funding_summary': \"LangChain has raised a total of $35 million as of July 2023. Their latest round was a $25 million Series A in April 2023, led by Sequoia Capital. This was preceded by a $10 million seed round led by Benchmark, announced on April 4, 2023. The company's valuation was at least $200 million as of April 2023.\"},\n", + " 'is_satisfactory': True}" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "## Test remote graph connected w/ Studio \n", + "\n", + "from langgraph.pregel.remote import RemoteGraph\n", + "\n", + "url = \"http://localhost:60827\"\n", + "graph_id = \"company_maistro\"\n", + "\n", + "graph = RemoteGraph(graph_id, url=url)\n", + "\n", + "# Some input to the graph\n", + "input = {\"company\":\"langchain\"}\n", + "\n", + "# Can also be a subgraph in an existing graph\n", + "response = await graph.ainvoke(input)\n", + "response" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'company': 'langchain', 'completed_notes': []}\n", + "{'company': 'langchain', 'completed_notes': [\"Research Notes: LangChain\\n\\nCompany Name:\\n- LangChain\\n\\nFounding Year:\\n- 2022 (mentioned in multiple sources)\\n\\nFounders:\\n- Harrison Chase (Co-Founder & CEO)\\n- Ankush Gola (Co-Founder)\\n\\nProduct Description:\\n- Open source framework for developing applications using large language models (LLMs)\\n- Provides tools and APIs to simplify building LLM-driven applications like chatbots and virtual agents\\n- Offers flexible abstractions and an AI-first toolkit\\n- Helps connect LLMs to private data sources and APIs to create context-aware, reasoning applications\\n- Includes integrations with various cloud storage, APIs, databases, and LLM providers\\n\\nFunding Summary:\\n- Total raised: $35 million (as of July 2023)\\n- Latest round: $25 million Series A (April 2023)\\n- Led by Sequoia Capital\\n- Previous round: $10 million seed round led by Benchmark (announced April 4, 2023)\\n- Valuation: At least $200 million (as of April 2023)\\n\\nAdditional Notes:\\n- Headquarters: San Francisco, California\\n- Launched in October 2022, quickly gained popularity among developers\\n- As of June 2023, had 20,000+ developers in its Discord community\\n- GitHub stats (as of July 2023): 55K stars, 7.1K forks, 13.9K users, 1.19K contributors\\n- In October 2023, introduced LangServe, a deployment tool\\n- Recently launched LangSmith, a paid LLMOps product for the entire LLM application lifecycle\\n- LangSmith had over 70,000 signups since closed beta launch in July 2023\\n- Used by over 5,000 companies monthly, including Rakuten, Elastic, Moody's, and Retool\\n\\nMissing/Unclear Information:\\n- Exact founding date not specified\\n- Complete list of founding team members not provided\\n- Detailed breakdown of funding rounds prior to the $25 million Series A is not clear\"]}\n", + "{'company': 'langchain', 'completed_notes': [\"Research Notes: LangChain\\n\\nCompany Name:\\n- LangChain\\n\\nFounding Year:\\n- 2022 (mentioned in multiple sources)\\n\\nFounders:\\n- Harrison Chase (Co-Founder & CEO)\\n- Ankush Gola (Co-Founder)\\n\\nProduct Description:\\n- Open source framework for developing applications using large language models (LLMs)\\n- Provides tools and APIs to simplify building LLM-driven applications like chatbots and virtual agents\\n- Offers flexible abstractions and an AI-first toolkit\\n- Helps connect LLMs to private data sources and APIs to create context-aware, reasoning applications\\n- Includes integrations with various cloud storage, APIs, databases, and LLM providers\\n\\nFunding Summary:\\n- Total raised: $35 million (as of July 2023)\\n- Latest round: $25 million Series A (April 2023)\\n- Led by Sequoia Capital\\n- Previous round: $10 million seed round led by Benchmark (announced April 4, 2023)\\n- Valuation: At least $200 million (as of April 2023)\\n\\nAdditional Notes:\\n- Headquarters: San Francisco, California\\n- Launched in October 2022, quickly gained popularity among developers\\n- As of June 2023, had 20,000+ developers in its Discord community\\n- GitHub stats (as of July 2023): 55K stars, 7.1K forks, 13.9K users, 1.19K contributors\\n- In October 2023, introduced LangServe, a deployment tool\\n- Recently launched LangSmith, a paid LLMOps product for the entire LLM application lifecycle\\n- LangSmith had over 70,000 signups since closed beta launch in July 2023\\n- Used by over 5,000 companies monthly, including Rakuten, Elastic, Moody's, and Retool\\n\\nMissing/Unclear Information:\\n- Exact founding date not specified\\n- Complete list of founding team members not provided\\n- Detailed breakdown of funding rounds prior to the $25 million Series A is not clear\"], 'info': {'company_name': 'LangChain', 'founding_year': 2022, 'founder_names': ['Harrison Chase', 'Ankush Gola'], 'product_description': 'LangChain is an open source framework for developing applications using large language models (LLMs). It provides tools and APIs to simplify building LLM-driven applications like chatbots and virtual agents. The framework offers flexible abstractions and an AI-first toolkit, helping connect LLMs to private data sources and APIs to create context-aware, reasoning applications. It includes integrations with various cloud storage, APIs, databases, and LLM providers.', 'funding_summary': \"LangChain has raised a total of $35 million as of July 2023. Their latest round was a $25 million Series A in April 2023, led by Sequoia Capital. This was preceded by a $10 million seed round led by Benchmark, announced on April 4, 2023. The company's valuation was at least $200 million as of April 2023.\"}}\n", + "{'company': 'langchain', 'completed_notes': [\"Research Notes: LangChain\\n\\nCompany Name:\\n- LangChain\\n\\nFounding Year:\\n- 2022 (mentioned in multiple sources)\\n\\nFounders:\\n- Harrison Chase (Co-Founder & CEO)\\n- Ankush Gola (Co-Founder)\\n\\nProduct Description:\\n- Open source framework for developing applications using large language models (LLMs)\\n- Provides tools and APIs to simplify building LLM-driven applications like chatbots and virtual agents\\n- Offers flexible abstractions and an AI-first toolkit\\n- Helps connect LLMs to private data sources and APIs to create context-aware, reasoning applications\\n- Includes integrations with various cloud storage, APIs, databases, and LLM providers\\n\\nFunding Summary:\\n- Total raised: $35 million (as of July 2023)\\n- Latest round: $25 million Series A (April 2023)\\n- Led by Sequoia Capital\\n- Previous round: $10 million seed round led by Benchmark (announced April 4, 2023)\\n- Valuation: At least $200 million (as of April 2023)\\n\\nAdditional Notes:\\n- Headquarters: San Francisco, California\\n- Launched in October 2022, quickly gained popularity among developers\\n- As of June 2023, had 20,000+ developers in its Discord community\\n- GitHub stats (as of July 2023): 55K stars, 7.1K forks, 13.9K users, 1.19K contributors\\n- In October 2023, introduced LangServe, a deployment tool\\n- Recently launched LangSmith, a paid LLMOps product for the entire LLM application lifecycle\\n- LangSmith had over 70,000 signups since closed beta launch in July 2023\\n- Used by over 5,000 companies monthly, including Rakuten, Elastic, Moody's, and Retool\\n\\nMissing/Unclear Information:\\n- Exact founding date not specified\\n- Complete list of founding team members not provided\\n- Detailed breakdown of funding rounds prior to the $25 million Series A is not clear\"], 'info': {'company_name': 'LangChain', 'founding_year': 2022, 'founder_names': ['Harrison Chase', 'Ankush Gola'], 'product_description': 'LangChain is an open source framework for developing applications using large language models (LLMs). It provides tools and APIs to simplify building LLM-driven applications like chatbots and virtual agents. The framework offers flexible abstractions and an AI-first toolkit, helping connect LLMs to private data sources and APIs to create context-aware, reasoning applications. It includes integrations with various cloud storage, APIs, databases, and LLM providers.', 'funding_summary': \"LangChain has raised a total of $35 million as of July 2023. Their latest round was a $25 million Series A in April 2023, led by Sequoia Capital. This was preceded by a $10 million seed round led by Benchmark, announced on April 4, 2023. The company's valuation was at least $200 million as of April 2023.\"}, 'is_satisfactory': True}\n" + ] + } + ], + "source": [ + "## Test SDK connected w/ Studio \n", + "\n", + "from langgraph_sdk import get_client\n", + "client = get_client(url=\"http://localhost:60827\")\n", + "new_thread = await client.threads.create()\n", + "async for chunk in client.runs.stream(new_thread[\"thread_id\"], \n", + " assistant_id=\"25f608e5-2c8c-565f-a5f2-39604c6d4ff4\",\n", + " input={\"company\":\"langchain\"},\n", + " stream_mode=\"values\"):\n", + "\n", + " if chunk.event == 'values':\n", + " print(chunk.data)" + ] + }, + { + "cell_type": "code", + "execution_count": 26, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'info': {'company_name': 'LangChain',\n", + " 'verified_company': True,\n", + " 'company_summary': \"LangChain is a San Francisco-based technology company founded in 2022 that develops an AI-powered large language model (LLM) framework. The company has raised approximately $30-35 million in funding and was valued at over $200 million as of April 2023. LangChain's main products include the LangChain Framework, LangGraph Cloud, LangSmith, and LangServe, all designed to simplify the creation and deployment of LLM-based applications. With a strong focus on community engagement and rapid product evolution, LangChain has gained significant traction among developers and continues to expand its offerings in the AI industry.\",\n", + " 'key_executives': [{'name': 'Harrison Chase',\n", + " 'title': 'Co-Founder & CEO',\n", + " 'verification_date': '2023-07-01',\n", + " 'confidence_level': 'high',\n", + " 'source': 'https://craft.co/langchain'},\n", + " {'name': 'Ankush Gola',\n", + " 'title': 'Co-Founder',\n", + " 'verification_date': '2023-07-01',\n", + " 'confidence_level': 'high',\n", + " 'source': 'https://craft.co/langchain'},\n", + " {'name': 'Brie Wolfson',\n", + " 'title': 'Marketing',\n", + " 'verification_date': '2023-07-01',\n", + " 'confidence_level': 'medium',\n", + " 'source': 'https://craft.co/langchain'},\n", + " {'name': 'Nuno Campos',\n", + " 'title': 'Founding Engineer',\n", + " 'verification_date': '2023-07-01',\n", + " 'confidence_level': 'medium',\n", + " 'source': 'https://craft.co/langchain'}],\n", + " 'org_chart_summary': 'Detailed information about the organizational structure is not provided in the sources, but the company appears to have a relatively flat structure with key executives and founding members leading different aspects of the business.',\n", + " 'main_products': [{'name': 'LangChain Framework',\n", + " 'description': 'Core framework designed to simplify creation of applications using large language models (LLMs). It provides tools for connecting LLMs to other data sources and environments, enables chaining commands, and offers memory components for managing chat history.',\n", + " 'launch_date': '2022-10',\n", + " 'current_status': 'Active and continuously updated'},\n", + " {'name': 'LangGraph Cloud',\n", + " 'description': 'Infrastructure for running agents at scale. Used for building full-stack generative UI apps, Discord bots, and self-corrective RAG applications.',\n", + " 'launch_date': '2023',\n", + " 'current_status': 'Active'},\n", + " {'name': 'LangSmith',\n", + " 'description': 'Tool for testing, evaluating, and optimizing LLM applications. Features self-improving evaluators with human feedback integration.',\n", + " 'launch_date': '2023',\n", + " 'current_status': 'Active'},\n", + " {'name': 'LangServe',\n", + " 'description': 'Deployment tool for transitioning from LCEL prototypes to production-ready applications.',\n", + " 'launch_date': '2023-10',\n", + " 'current_status': 'Active'}]}}" + ] + }, + "execution_count": 26, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "## Test hard schema \n", + "\n", + "HARD_EXTRACTION_SCHEMA = {\n", + " \"title\": \"CompanyInfo\",\n", + " \"description\": \"Comprehensive information about a company with confidence tracking\",\n", + " \"type\": \"object\",\n", + " \"properties\": {\n", + " \"company_name\": {\n", + " \"type\": \"string\",\n", + " \"description\": \"Official name of the company\"\n", + " },\n", + " \"verified_company\": {\n", + " \"type\": \"boolean\",\n", + " \"description\": \"Confirmation this is the intended company, not a similarly named one\"\n", + " },\n", + " \"similar_companies\": {\n", + " \"type\": \"array\",\n", + " \"items\": {\"type\": \"string\"},\n", + " \"description\": \"List of similarly named companies that could be confused with the target\"\n", + " },\n", + " \"distinguishing_features\": {\n", + " \"type\": \"string\",\n", + " \"description\": \"Key features that distinguish this company from similarly named ones\"\n", + " },\n", + " \"key_executives\": {\n", + " \"type\": \"array\",\n", + " \"items\": {\n", + " \"type\": \"object\",\n", + " \"properties\": {\n", + " \"name\": {\"type\": \"string\"},\n", + " \"title\": {\"type\": \"string\"},\n", + " \"verification_date\": {\"type\": \"string\"},\n", + " \"confidence_level\": {\n", + " \"type\": \"string\",\n", + " \"enum\": [\"high\", \"medium\", \"low\", \"uncertain\"]\n", + " },\n", + " \"source\": {\"type\": \"string\"}\n", + " }\n", + " }\n", + " },\n", + " \"org_chart_summary\": {\n", + " \"type\": \"string\",\n", + " \"description\": \"Brief description of organizational structure\"\n", + " },\n", + " \"leadership_caveats\": {\n", + " \"type\": \"string\",\n", + " \"description\": \"Any uncertainties or caveats about leadership information\"\n", + " },\n", + " \"main_products\": {\n", + " \"type\": \"array\",\n", + " \"items\": {\n", + " \"type\": \"object\",\n", + " \"properties\": {\n", + " \"name\": {\"type\": \"string\"},\n", + " \"description\": {\"type\": \"string\"},\n", + " \"launch_date\": {\"type\": \"string\"},\n", + " \"current_status\": {\"type\": \"string\"}\n", + " }\n", + " }\n", + " },\n", + " \"services\": {\n", + " \"type\": \"array\",\n", + " \"items\": {\n", + " \"type\": \"object\",\n", + " \"properties\": {\n", + " \"name\": {\"type\": \"string\"},\n", + " \"description\": {\"type\": \"string\"},\n", + " \"target_market\": {\"type\": \"string\"}\n", + " }\n", + " }\n", + " },\n", + " \"recent_developments\": {\n", + " \"type\": \"array\",\n", + " \"items\": {\n", + " \"type\": \"object\",\n", + " \"properties\": {\n", + " \"date\": {\"type\": \"string\"},\n", + " \"title\": {\"type\": \"string\"},\n", + " \"summary\": {\"type\": \"string\"},\n", + " \"source_url\": {\"type\": \"string\"},\n", + " \"significance\": {\"type\": \"string\"}\n", + " }\n", + " },\n", + " \"description\": \"Major news and developments from the last 6 months\"\n", + " },\n", + " \"historical_challenges\": {\n", + " \"type\": \"array\",\n", + " \"items\": {\n", + " \"type\": \"object\",\n", + " \"properties\": {\n", + " \"issue_type\": {\"type\": \"string\"},\n", + " \"description\": {\"type\": \"string\"},\n", + " \"date_period\": {\"type\": \"string\"},\n", + " \"resolution\": {\"type\": \"string\"},\n", + " \"current_status\": {\"type\": \"string\"}\n", + " }\n", + " },\n", + " \"description\": \"Past challenges, issues, or controversies\"\n", + " },\n", + " \"sources\": {\n", + " \"type\": \"array\",\n", + " \"items\": {\n", + " \"type\": \"object\",\n", + " \"properties\": {\n", + " \"url\": {\"type\": \"string\"},\n", + " \"title\": {\"type\": \"string\"},\n", + " \"date_accessed\": {\"type\": \"string\"},\n", + " \"information_type\": {\n", + " \"type\": \"array\",\n", + " \"items\": {\"type\": \"string\"},\n", + " \"description\": \"Types of information sourced from this link (e.g., leadership, products, news)\"\n", + " }\n", + " }\n", + " }\n", + " },\n", + " \"company_summary\": {\n", + " \"type\": \"string\",\n", + " \"description\": \"Concise, dense summary of the most important company information (max 250 words)\"\n", + " }\n", + " },\n", + " \"required\": [\n", + " \"company_name\",\n", + " \"verified_company\",\n", + " \"company_summary\",\n", + " \"key_executives\",\n", + " \"main_products\",\n", + " \"sources\"\n", + " ]\n", + "}\n", + "\n", + "# Create proper InputState instance\n", + "input_state = InputState(\n", + " company=\"LangChain\",\n", + " extraction_schema=HARD_EXTRACTION_SCHEMA,\n", + ")\n", + "\n", + "# Invoke with the proper input state\n", + "result = await graph.ainvoke(input_state)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "company_maistro_env", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.13.0" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +}