├── README.md ├── config.py ├── falcon.py ├── google_grounding.py ├── portfolio.py ├── requirements.txt └── tools.py /README.md: -------------------------------------------------------------------------------- 1 | # Falcon: Your AI-Powered Investment Assistant 2 | 3 | Falcon is an AI-powered investment assistant built using LangGraph, LangChain, and Chainlit. It helps users make informed investment decisions by providing portfolio analysis, stock market trend analysis, and price checking. 4 | 5 | ## Features 6 | 7 | * **Portfolio Retrieval:** Access and understand your current investment holdings. 8 | * **Stock Analysis:** Analyze market trends and get insights into specific stocks. 9 | * **Price Checking:** Quickly check the current price of stocks. 10 | * **Personalized Recommendations:** Receive tailored investment advice based on your risk tolerance and goals. (Explain how personalization works, if applicable) 11 | * **Interactive Chat Interface:** A user-friendly chat interface powered by Chainlit. 12 | 13 | 14 | ## Architecture 15 | 16 | Falcon uses a modular architecture combining several key technologies: 17 | 18 | * **LangGraph:** Orchestrates the workflow and decision-making process. 19 | * **LangChain:** Provides tools and chains for interacting with external data sources and LLMs. 20 | * **Chainlit:** Creates the interactive user interface. 21 | * **Google Vertex AI:** Powers the underlying language model (Gemini). 22 | * **Finnhub:** Provides real-time stock market data. 23 | * **BigQuery:** Stores and retrieves portfolio information. 24 | 25 | ## Examples Queries 26 | Interact with Falcon: Use the chat interface to ask questions about your portfolio, analyze stocks, and get investment recommendations. 27 | 28 | * What stocks do I currently hold? 29 | * What's the current price of AAPL? 30 | * Am I making a profit or loss in my portfolio? 31 | * Should I invest in TSLA? 32 | * Should I sell off my Tesla stocks? 33 | 34 | ## Usage 35 | **Run the Chainlit App:** 36 | ```bash 37 | chainlit run falcon.py 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /config.py: -------------------------------------------------------------------------------- 1 | LLM_MODEL = "gemini-2.0-flash-exp" 2 | PROJECT_NUMBER=XXX 3 | SEARCH_ENGINE_ID="XXX" 4 | PROJECT_ID = "XXX" # @param {type:"string"} 5 | BIGQUERY_DATASET_ID = "XXX" 6 | FINNHUB_API_KEY = "XXX" 7 | # ... other configuration values 8 | 9 | 10 | -------------------------------------------------------------------------------- /falcon.py: -------------------------------------------------------------------------------- 1 | import chainlit as cl 2 | import operator 3 | from langchain_google_vertexai import ChatVertexAI 4 | from typing import Annotated, List, Tuple, Optional 5 | from typing_extensions import TypedDict 6 | from pydantic import BaseModel, Field 7 | from langgraph.prebuilt import create_react_agent 8 | from langchain_core.prompts import ChatPromptTemplate 9 | from langgraph.graph import END, StateGraph, START 10 | from tools import portfolio_retriever, stock_analyser, normal_responder, price_checker 11 | 12 | 13 | 14 | # --- Configuration and Constants --- 15 | LLM_MODEL = 'gemini-2.0-flash-exp' 16 | AGENT_PROMPT = """You are Falcon, a professional expert investment trader who can make investment recommendations and analysis. You are NOT an LLM or AI chatbot. 17 | Help to execute the task that you are assigned to, and return the response in a clear and concise manner. 18 | The user is a day trader, risk tolerance is high. time horizon for trading is usually 1-2 months. Investment goal is to maximise the opportunity cost of the funds and reap maximum returns within the time horizon. 19 | **IMPORTANT** If you are asked for guidance/advice or need to make a recommendation, you MUST provide comprehensive advice or make a recommendation based on the info that you have gathered""" 20 | 21 | 22 | # --- LLM and Tools --- 23 | llm = ChatVertexAI(model_name=LLM_MODEL, temperature=0) 24 | tools = [portfolio_retriever, stock_analyser, normal_responder, price_checker] 25 | agent_executor = create_react_agent(llm, tools, state_modifier=AGENT_PROMPT) 26 | 27 | # --- Data Models --- 28 | class Plan(BaseModel): 29 | """Plan to follow.""" 30 | steps: List[str] = Field(description="Steps to follow, in order.") 31 | 32 | class PlanExecute(TypedDict): 33 | input: str 34 | plan: List[str] 35 | past_steps: Annotated[List[Tuple[str, str]], operator.add] 36 | response: Optional[str] 37 | intermediate_responses: List[str] 38 | 39 | class Response(BaseModel): 40 | """Response to user.""" 41 | response: str 42 | 43 | class Act(BaseModel): 44 | """Action to perform (for replanning).""" 45 | response: Optional[Response] = None 46 | plan: Optional[Plan] = None 47 | 48 | 49 | # --- Prompts --- 50 | PLANNER_PROMPT = ChatPromptTemplate.from_messages( 51 | [ 52 | ( 53 | "system", 54 | """You are an expert in deciphering questions and creating step-by-step plans. 55 | Based on the given objective, create a simple plan. Each step should be a distinct task that, when executed, will lead to the correct answer. Avoid superfluous steps. 56 | Use these guidelines to choose the right tool: 57 | - Portfolio retrieval (e.g., "What's my portfolio?", "What are my holdings?", "Last trade on Nvidia?"): Use the {{portfolio_retriever}} tool. 58 | - Check the current price of a stock using the stock symbol(e.g. current price of GOOG): Use the {{price_checker}} tool. If there are multiple stocks to check, break down into multiple steps to do multiple function calls to check current price for individual stock. 59 | - Equity/market analysis (e.g., "Will Nvidia rise?", "Current stock price?", "Is Intel a buy?", "What are the risks?"): Use the {{stock_analyser}} tool. 60 | - General/non-financial questions (e.g., "Hi", "Who are you?"): Use the {{normal_responder}} tool. 61 | The final step's result should be the final answer. Ensure each step has enough information; do not skip steps. 62 | 63 | Example Qns: Should I sell off my Nvidia stocks now? 64 | Example Plan: 65 | Step 1: Check the number of Nvidia stocks and average purchase price in portfolio using {{portfolio_retriever}} tool 66 | Step 2: Check the current price of Nvidia stock using {{price_checker}} tool 67 | Step 3: Analyse how Nvidia stock is doing in today's market and is it recommended to sell or hold using the {{stock_analyser}} tool 68 | Step 4: Based on current price of Nvidia stock and purchase price, calculate much will user lose or profit if selling today? 69 | Step 5: Combine all pieces of information from prior steps to put up a recommendation 70 | """, 71 | ), 72 | ("placeholder", "{messages}"), 73 | ] 74 | ) 75 | 76 | REPLANNER_PROMPT = ChatPromptTemplate.from_template( 77 | """Create a step-by-step plan for the given objective. 78 | Each step should be a distinct task. The final step's result should be the final answer. Do not skip steps. 79 | 80 | Your objective: 81 | {input} 82 | 83 | Your original plan: 84 | {plan} 85 | 86 | Completed steps: 87 | {past_steps} 88 | 89 | Update the plan. Include only the steps that still NEED to be done, incorporating data from previous steps. Do NOT include previously completed steps.""" 90 | ) 91 | 92 | # --- Chains and Agents --- 93 | planner = PLANNER_PROMPT | llm.with_structured_output(Plan) 94 | replanner = REPLANNER_PROMPT | llm.with_structured_output(Act) 95 | 96 | # --- Mis --- 97 | def clean_newlines(text: str) -> str: 98 | """Removes extra newline characters from a string.""" 99 | return text.replace("\n\n", "\n") 100 | 101 | # --- Workflow Nodes --- 102 | async def plan_step(state: PlanExecute): 103 | plan = await planner.ainvoke({"messages": [("user", state["input"])]}) 104 | with cl.Step(name="Generated Plan"): 105 | await cl.Message(content="**Generated Plan:**").send() 106 | for i, step in enumerate(plan.steps): 107 | await cl.Message(content=f" {step}").send() 108 | return {"plan": plan.steps, "intermediate_responses": []} 109 | 110 | async def execute_step(state: PlanExecute): 111 | plan = state["plan"] 112 | if not plan: 113 | return {"response": "No more steps in the plan."} 114 | 115 | plan_str = "\n".join(f"{i+1}. {step}" for i, step in enumerate(plan)) 116 | task = plan[0] 117 | step_number = len(state.get("past_steps", [])) + 1 118 | task_formatted = f"""For the following plan: {plan_str}\n\nYou are tasked with executing step {step_number}, {task}.""" 119 | 120 | with cl.Step(name=f"Executing: {task}"): 121 | await cl.Message(content=f"**Executing** {task}").send() 122 | agent_response = await agent_executor.ainvoke({"messages": [("user", task_formatted)]}) 123 | final_response = agent_response["messages"][-1].content 124 | 125 | await cl.Message(content=final_response).send() 126 | 127 | return { 128 | "past_steps": state.get("past_steps", []) + [(task, final_response)], 129 | "plan": plan[1:], 130 | "intermediate_responses": state.get("intermediate_responses", []) + [final_response] 131 | } 132 | 133 | async def replan_step(state: PlanExecute): 134 | all_responses = "\n".join(state["intermediate_responses"]) 135 | all_steps = "\n".join([f"{step}: {response}" for step, response in state["past_steps"]]) 136 | context = f"Here is the information gathered from the previous steps:\n{all_steps}\n\nHere are the direct responses from the tools:\n{all_responses}" 137 | 138 | output = await replanner.ainvoke({**state, "input": context}) 139 | if output.response: 140 | cleaned_response = clean_newlines(output.response.response) 141 | with cl.Step(name="Final Response"): 142 | await cl.Message(content="**Final Response:**").send() 143 | await cl.Message(content=cleaned_response).send() 144 | return {"response": cleaned_response} 145 | else: 146 | return {"plan": output.plan.steps} 147 | 148 | def should_end(state: PlanExecute): 149 | return END if "response" in state and state["response"] is not None else "agent" 150 | 151 | # --- Workflow Definition --- 152 | workflow = StateGraph(PlanExecute) 153 | workflow.add_node("planner", plan_step) 154 | workflow.add_node("agent", execute_step) 155 | workflow.add_node("replan", replan_step) 156 | workflow.add_edge(START, "planner") 157 | workflow.add_edge("planner", "agent") 158 | workflow.add_edge("agent", "replan") 159 | workflow.add_conditional_edges("replan", should_end, {"agent": "agent", END: END}) 160 | app = workflow.compile() 161 | 162 | # --- Chainlit Interface --- 163 | @cl.on_chat_start 164 | async def start(): 165 | await cl.Message(content="Hello! How can I help you today?").send() 166 | 167 | @cl.on_message 168 | async def main(message: cl.Message): 169 | 170 | config = {"recursion_limit": 50} 171 | async for event in app.astream( 172 | {"input": message.content, "plan": [], "past_steps": [], "response": None, "intermediate_responses": []}, 173 | config=config, 174 | ): 175 | if "response" in event: 176 | # Final response is handled in replan_step, no action needed here 177 | pass 178 | elif "plan" in event: 179 | # Plan generation messages are handled in plan_step, no action needed here 180 | pass 181 | elif "past_steps" in event: 182 | # Execution step messages are handled in execute_step, no action needed here 183 | pass -------------------------------------------------------------------------------- /google_grounding.py: -------------------------------------------------------------------------------- 1 | from google.cloud import discoveryengine_v1 as discoveryengine 2 | from vertexai.generative_models import GenerativeModel 3 | from config import PROJECT_NUMBER, SEARCH_ENGINE_ID 4 | from typing import List 5 | import json 6 | 7 | #variables 8 | spec = discoveryengine.GenerateGroundedContentRequest.GenerationSpec( 9 | model_id="gemini-1.5-flash", 10 | temperature=0.0, 11 | top_p=1, 12 | top_k=1, 13 | ) 14 | 15 | #client initialisation 16 | google_search_client = discoveryengine.GroundedGenerationServiceClient() 17 | model = GenerativeModel( 18 | "gemini-1.5-pro", 19 | system_instruction=f"""You are Falcon, one of the most seasoned equity traders in the world. 20 | Your goal is to help to answer the user question with comprehensive analysis based on what you have been trained on, or knowledge from Google Search or knowledge from internal proprietary investment research. 21 | You need to return a response that explains how you came up with that answer, backed by evidence that you used in coming up with the answer. 22 | The user is a day trader, risk tolerance is high. time horizon for trading is usually 1-2 months. Investment goal is to maximise the opportunity cost of the funds and reap maximum returns within the time horizon. 23 | """ 24 | ) 25 | 26 | 27 | def google_ground(prompt: str) -> str: 28 | request = discoveryengine.GenerateGroundedContentRequest( 29 | location=google_search_client.common_location_path( 30 | project=PROJECT_NUMBER, location="global" 31 | ), 32 | generation_spec=spec, 33 | contents=[ 34 | discoveryengine.GroundedGenerationContent( 35 | role="user", 36 | parts=[discoveryengine.GroundedGenerationContent.Part(text=prompt)], 37 | ) 38 | ], 39 | system_instruction=discoveryengine.GroundedGenerationContent( 40 | parts=[ 41 | discoveryengine.GroundedGenerationContent.Part(text="If given a stock or option to analyse, try to find relevant news from google search to see if it's a good time to buy more or sell. Use these news to formulate your analysis and return a comprehensive response targted to the provided stock or option. Rmb that you are a seasoned investment analyst so always remove any disclaimers about this not being financial advice.") 42 | ], 43 | ), 44 | grounding_spec=discoveryengine.GenerateGroundedContentRequest.GroundingSpec( 45 | grounding_sources=[ 46 | discoveryengine.GenerateGroundedContentRequest.GroundingSource( 47 | google_search_source=discoveryengine.GenerateGroundedContentRequest.GroundingSource.GoogleSearchSource() 48 | ) 49 | ] 50 | ), 51 | ) 52 | google_responses = google_search_client.generate_grounded_content(request) 53 | 54 | print(google_responses) # Print the extracted text 55 | return_prompt=f"""Generate a natural language response based on the original question: '{prompt}' and the returned results: '{google_responses}'""" 56 | 57 | response=model.generate_content(return_prompt) 58 | return response.text 59 | 60 | -------------------------------------------------------------------------------- /portfolio.py: -------------------------------------------------------------------------------- 1 | #Connects to BQ and retrieves portfolio data 2 | 3 | from google.cloud import bigquery 4 | from vertexai.generative_models import FunctionDeclaration, GenerativeModel, Part, Tool 5 | from config import PROJECT_ID, BIGQUERY_DATASET_ID 6 | 7 | 8 | sqlGeneratorModel = GenerativeModel( 9 | 'gemini-1.5-pro', 10 | generation_config={"temperature": 0,"max_output_tokens":2048}, 11 | ) 12 | 13 | 14 | client = bigquery.Client(project=PROJECT_ID) # Replace with your project ID 15 | user_question ="" 16 | 17 | nl2sql_prompt=f""" 18 | 19 | You are an Bigquery SQL guru working in the investment industry. Your task is to write a Bigquery SQL query that answers the {user_question} and the following database schema. 20 | 21 | - Join as minimal tables as possible. 22 | - When joining tables ensure all join columns are the same data_type. 23 | - Analyze the database and the table schema provided as parameters and undestand the relations (column and table relations). 24 | - Use always SAFE_CAST. If performing a SAFE_CAST, use only Bigquery supported datatypes. 25 | - Always SAFE_CAST and then use aggregate functions 26 | - Don't include any comments in code. 27 | - Remove ```sql and ``` from the output and generate the SQL in single line. 28 | - Tables should be refered to using a fully qualified name with enclosed in ticks (`) e.g. `project_id.owner.table_name`. 29 | - Use all the non-aggregated columns from the "SELECT" statement while framing "GROUP BY" block. 30 | - Return syntactically and symantically correct SQL for BigQuery with proper relation mapping i.e project_id, owner, table and column relation. 31 | - Use ONLY the column names (column_name) mentioned in Table Schema. DO NOT USE any other column names outside of this. 32 | - Associate column_name mentioned in Table Schema only to the table_name specified under Table Schema. 33 | - Use SQL 'AS' statement to assign a new name temporarily to a table column or even a table wherever needed. 34 | - Table names are case sensitive. DO NOT uppercase or lowercase the table names. 35 | - Always enclose subqueries and union queries in brackets. 36 | - Refer to the examples provided below, if given. 37 | - You always generate SELECT queries ONLY. If asked for other statements for DELETE or MERGE etc respond with dummy SQL statement 38 | 39 | 40 | 41 | **Database Schema:** 42 | 43 | **Holdings Table:** 44 | 45 | | Column Name | Data Type | Description | 46 | |---|---|---| 47 | | symbol | STRING | Stock symbol (e.g., AAPL, GOOG) | 48 | | company_name | STRING | Name of the company | 49 | | quantity | INT64 | Number of shares held | 50 | | purchase_price | FLOAT64 | Purchase price per share | 51 | | purchase_date | DATE | Date when the shares were purchased | 52 | | currency | STRING | Currency of the holding | 53 | 54 | 55 | **Example Natural Language Question:** 56 | 57 | "What do I have in my portfolio?" 58 | 59 | **Expected SQL Query:** 60 | 61 | SELECT symbol, company_name, quantity, purchase_price, purchase_date 62 | FROM `my-vertexai-project-id.current_portfolio.holdings`; 63 | """ 64 | 65 | 66 | def query_portfolio(prompt): 67 | user_question=prompt 68 | revised_prompt = "Use these System Instructions: " + nl2sql_prompt + " to answer the provided Question: " + user_question 69 | print(revised_prompt) 70 | 71 | generated_query=sqlGeneratorModel.generate_content(revised_prompt) 72 | 73 | print(generated_query.text) 74 | job_config = bigquery.QueryJobConfig(maximum_bytes_billed=100000000) 75 | cleaned_query = ( 76 | generated_query.text 77 | .replace("\\n", " ") 78 | .replace("\n", "") 79 | .replace("\\", "") 80 | .replace("```sql", "") 81 | ) 82 | print(cleaned_query) 83 | query_job = client.query(cleaned_query, job_config=job_config) 84 | api_response = query_job.result() 85 | api_response = str([dict(row) for row in api_response]) 86 | api_response = api_response.replace("\\", "").replace( 87 | "\n", "" 88 | ) 89 | print(api_response) 90 | return_prompt=f"""Generate a natural language response based on the original question: '{user_question}' and the returned results: '{api_response}'""" 91 | #print(return_prompt) 92 | response=sqlGeneratorModel.generate_content(return_prompt) 93 | print(response.text) 94 | return response.text 95 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | langgraph 2 | langchain 3 | langchain-community 4 | langchain-google-vertexai 5 | langchain-core 6 | chainlit 7 | vertexai 8 | google-cloud-discoveryengine 9 | requests 10 | finnhub-python 11 | -------------------------------------------------------------------------------- /tools.py: -------------------------------------------------------------------------------- 1 | from langchain_core.tools import tool 2 | from portfolio import query_portfolio 3 | from google_grounding import google_ground 4 | from langchain.prompts import PromptTemplate 5 | from langchain_google_vertexai import ChatVertexAI 6 | from langchain.schema import AIMessage 7 | import os 8 | import json 9 | import re 10 | import finnhub 11 | from config import FINNHUB_API_KEY 12 | 13 | 14 | 15 | LLM_MODEL = 'gemini-1.5-flash' 16 | llm = ChatVertexAI(model_name=LLM_MODEL, temperature=0) 17 | 18 | 19 | def get_stock_symbols(prompt: str) -> list: 20 | """Uses LLM to extract company names and convert them to ticker symbols.""" 21 | 22 | prompt_template = """ 23 | Extract the stock ticker symbols from the given text. If a company name is provided, convert it to its most common ticker symbol. If no symbols or company names are found, return an empty list in valid JSON format. 24 | 25 | Example 1: 26 | Text: "Get me the prices of Apple and Microsoft." 27 | Symbols: ["AAPL", "MSFT"] 28 | 29 | Example 2: 30 | Text: "What about Tesla, Google, and Amazon?" 31 | Symbols: ["TSLA", "GOOG", "AMZN"] 32 | 33 | Example 3: 34 | Text: "I want to know about IBM and Berkshire Hathaway." 35 | Symbols: ["IBM", "BRK-B"] 36 | 37 | Example 4: 38 | Text: "No stocks here." 39 | Symbols: [] 40 | 41 | Return ONLY the valid JSON string representing the list of symbols. Do not include any other text or formatting like code blocks. 42 | 43 | Text: "{text}" 44 | Symbols: 45 | """ 46 | 47 | PROMPT = PromptTemplate(template=prompt_template, input_variables=["text"]) 48 | llm_output = llm.invoke(PROMPT.format(text=prompt)) 49 | 50 | try: 51 | if isinstance(llm_output, AIMessage): 52 | llm_text = llm_output.content 53 | else: 54 | llm_text = str(llm_output) 55 | 56 | # More robust JSON extraction using regex 57 | match = re.search(r"\[.*\]", llm_text, re.DOTALL) # Find JSON array using regex 58 | if match: 59 | json_string = match.group(0) 60 | symbols = json.loads(json_string) 61 | if isinstance(symbols, list): 62 | return [symbol.upper() for symbol in symbols] 63 | else: 64 | print(f"LLM did not return a list: {llm_text}") 65 | return [] 66 | else: 67 | print(f"No JSON found in LLM output: {llm_text}") 68 | return [] 69 | 70 | except json.JSONDecodeError as e: 71 | print(f"JSON decoding error: {e}. Full LLM Output: {llm_text}") 72 | return [] 73 | except Exception as e: 74 | print(f"An unexpected error occurred: {e}") 75 | return [] 76 | 77 | @tool 78 | def price_checker(prompt: str) -> str: 79 | """Check the current price of one or more stocks using Finnhub. Accepts company names or ticker symbols.""" 80 | print("Using Price Checker tool now") 81 | symbols = get_stock_symbols(prompt) 82 | 83 | if not symbols: 84 | print(f"No valid stock symbols or company names found in the input: {prompt}") 85 | return "No valid stock symbols or company names found in the input." 86 | 87 | results = [] 88 | client = finnhub.Client(api_key=FINNHUB_API_KEY) 89 | for ticker_symbol in symbols: 90 | try: 91 | print(ticker_symbol) 92 | quote = client.quote(ticker_symbol) 93 | 94 | if not quote or quote['c'] is None: # Check if quote and current price exist 95 | results.append(f"Could not retrieve price information for {ticker_symbol}. Check the ticker or Finnhub data.") 96 | continue 97 | 98 | current_price = quote['c'] 99 | previous_close = quote.get('pc') # Use .get to avoid KeyError if 'pc' is missing 100 | 101 | if previous_close is None: 102 | results.append(f"Could not retrieve previous close price for {ticker_symbol}.") 103 | continue 104 | 105 | change = current_price - previous_close 106 | percent_change = (change / previous_close) * 100 if previous_close != 0 else 0 # avoid division by zero 107 | 108 | results.append( 109 | f"{ticker_symbol}: Current Price: ${current_price:.2f}, Change: ${change:.2f} ({percent_change:.2f}%)" 110 | ) 111 | 112 | except Exception as e: 113 | results.append(f"An error occurred for {ticker_symbol}: {e}") 114 | 115 | return "\n".join(results) 116 | 117 | @tool 118 | def portfolio_retriever(prompt: str) -> str: 119 | """Retrieves portfolio information. Information returned must be information on the portfolio. E.g. 100 units of TSLA stock, purchased at an avg price of $200""" 120 | print("Using Portfolio Retriever tool now") 121 | return query_portfolio(prompt) 122 | 123 | 124 | @tool 125 | def stock_analyser(prompt: str) -> str: 126 | """Analyzes stock market trends.""" 127 | print("Using Stock Analyser tool now") 128 | return google_ground(prompt) 129 | 130 | 131 | @tool 132 | def normal_responder(qns: str) -> str: 133 | """Answer normal Question/Generic Question (e.g. Hi or who are you?)""" 134 | print("Using Normal Responder tool now") 135 | prompt_template=f"""You are Falcon, one of the most seasoned equity traders in the world. 136 | Your goal is to help to answer the user {qns} with comprehensive analysis based on what you have been trained on, or knowledge from Google Search or knowledge from internal proprietary investment research. 137 | You need to return a response that explains how you came up with that answer, backed by evidence that you used in coming up with the answer. 138 | The user is a day trader, risk tolerance is high. time horizon for trading is usually 1-2 months. Investment goal is to maximise the opportunity cost of the funds and reap maximum returns within the time horizon. 139 | """, 140 | PROMPT = PromptTemplate(template=prompt_template, input_variables=["text"]) 141 | llm_output = llm.invoke(PROMPT.format(qns=qns)) 142 | return(llm_output.content) 143 | 144 | 145 | 146 | --------------------------------------------------------------------------------