diff --git a/.gitignore b/.gitignore index 66b76c1..28d9bef 100644 --- a/.gitignore +++ b/.gitignore @@ -161,4 +161,4 @@ myvenv/ #parlant parlant-data -test.md \ No newline at end of file +test.md diff --git a/examples/Stockagent/README.md b/examples/Stockagent/README.md new file mode 100644 index 0000000..0839233 --- /dev/null +++ b/examples/Stockagent/README.md @@ -0,0 +1,68 @@ +# When AI Meets Finance (StockAgent): Large Language Model-based Stock Trading in Simulated Real-world Environments + +![workflow](fig/workflow.png) +![schematic](fig/schematic.png) + +Can AI Agents simulate real-world trading environments to investigate the impact of external factors on stock trading activities (e.g., macroeconomics, policy changes, company fundamentals, and global events)? These factors, which frequently influence trading behaviors, are critical elements in the quest for maximizing investors' profits. Our work attempts to solve this problem through large language model-based agents. We have developed a multi-agent AI system called StockAgent, driven by LLMs, designed to simulate investors' trading behaviors in response to the real stock market. The StockAgent allows users to evaluate the impact of different external factors on investor trading and to analyze trading behavior and profitability effects. Additionally, StockAgent avoids the test set leakage issue present in existing trading simulation systems based on AI Agents. Specifically, it prevents the model from leveraging prior knowledge it may have acquired related to the test data. We evaluate different LLMs under the framework of StockAgent in a stock trading environment that closely resembles real-world conditions. The experimental results demonstrate the impact of key external factors on stock market trading, including trading behavior and stock price fluctuation rules. This research explores the study of agents' free trading gaps in the context of no prior knowledge related to market data. The patterns identified through StockAgent simulations provide valuable insights for LLM-based investment advice and stock recommendation. + +## Link +ARXIV LINK: https://arxiv.org/pdf/2407.18957 +## Architecture +![architect](fig/workflow2.png) + +The Workflow of Trading Simulation Flow. There are four Phases, namely **Initial Phase**, **Trading Phase**, **Post-Trading Phase** and **Special Events Phase**. In the Post-Trading Phase, Daily events and Quarterly events occur with daily and quarterly frequency respectively. A Specific Events Phase is an event that occurs randomly and acts on a random trading day. + +## Quick Start + +#### Environment + +``` +conda create --name stockagent python=3.9 +conda activate stockagent + +git clone https://github.com/dhh1995/PromptCoder +cd PromptCoder +pip install -e . +cd .. + +git clone +cd Stockagent +pip install -r requirements.txt +``` + +#### API keys + +Use GPTs as agent LLM: + +``` +export OPENAI_API_KEY=YOUR_OPENAI_API_KEY +``` + +Use Gemini as agent LLM: + +``` +export GOOGLE_API_KEY=YOUR_GEMINI_API_KEY +``` + +#### Start simulation + +You can choose a basic LLM and start simulation in one line: + +``` +python main.py --model MODEL_NAME +``` + +We set gemini-pro for default LLM. + +#### Citation +If you find the code is valuable, please use this citation. +``` +@article{zhang2024ai, + title={When AI Meets Finance (StockAgent): Large Language Model-based Stock Trading in Simulated Real-world Environments}, + author={Zhang, Chong and Liu, Xinyi and Jin, Mingyu and Zhang, Zhongmou and Li, Lingyao and Wang, Zhengting and Hua, Wenyue and Shu, Dong and Zhu, Suiyuan and Jin, Xiaobo and others}, + journal={arXiv preprint arXiv:2407.18957}, + year={2024} +} +``` + + diff --git a/examples/Stockagent/agent.py b/examples/Stockagent/agent.py new file mode 100644 index 0000000..b3707e7 --- /dev/null +++ b/examples/Stockagent/agent.py @@ -0,0 +1,427 @@ +import math +import time +import openai +import tiktoken +import random +import requests +import google.generativeai as genai + +import util +from log.custom_logger import log + +from prompt.agent_prompt import * +from procoder.functional import format_prompt +from procoder.prompt import * +from secretary import Secretary +from stock import Stock + + +def random_init(stock_a_initial, stock_b_initial): + stock_a, stock_b, cash, debt_amount = 0.0, 0.0, 0.0, 0.0 + while stock_a * stock_a_initial + stock_b * stock_b_initial + cash < util.MIN_INITIAL_PROPERTY \ + or stock_a * stock_a_initial + stock_b * stock_b_initial + cash > util.MAX_INITIAL_PROPERTY \ + or debt_amount > stock_a * stock_a_initial + stock_b * stock_b_initial + cash: + stock_a = int(random.uniform(0, util.MAX_INITIAL_PROPERTY / stock_a_initial)) + stock_b = int(random.uniform(0, util.MAX_INITIAL_PROPERTY / stock_b_initial)) + cash = random.uniform(0, util.MAX_INITIAL_PROPERTY) + debt_amount = random.uniform(0, util.MAX_INITIAL_PROPERTY) + debt = { + "loan": "yes", + "amount": debt_amount, + "loan_type": random.randint(0, len(util.LOAN_TYPE) - 1), + "repayment_date": random.choice(util.REPAYMENT_DAYS) + } + return stock_a, stock_b, cash, debt +# def random_init(stock_initial_price): +# stock, cash, debt_amount = 0.0, 0.0, 0.0 +# while stock * stock_initial_price + cash < util.MIN_INITIAL_PROPERTY \ +# or stock * stock_initial_price + cash > util.MAX_INITIAL_PROPERTY \ +# or debt_amount > stock * stock_initial_price + cash: +# stock = int(random.uniform(0, util.MAX_INITIAL_PROPERTY / stock_initial_price)) +# cash = random.uniform(0, util.MAX_INITIAL_PROPERTY) +# debt_amount = random.uniform(0, util.MAX_INITIAL_PROPERTY) +# debt = { +# "loan": "yes", +# "amount": debt_amount, +# "loan_type": random.randint(0, len(util.LOAN_TYPE)), +# "repayment_date": random.choice(util.REPAYMENT_DAYS) +# } +# return stock, cash, debt + + +class Agent: + def __init__(self, i, stock_a_price, stock_b_price, secretary, model): + self.order = i + self.secretary = secretary + self.model = model + self.character = random.choice(["Conservative", "Aggressive", "Balanced", "Growth-Oriented"]) + + self.stock_a_amount, self.stock_b_amount, self.cash, init_debt = random_init(stock_a_price, stock_b_price) + #self.stock_b_amount = 0 # stock 以手为单位存储,一手=10股,股价其实是一手的价格 + self.init_proper = self.get_total_proper(stock_a_price, stock_b_price) # 初始资产 后续借贷不超过初始资产 + + self.action_history = [[] for _ in range(util.TOTAL_DATE)] + self.chat_history = [] + self.loans = [init_debt] + self.is_bankrupt = False + self.quit = False + + def run_api(self, prompt, temperature: float = 1): + if 'gpt' in self.model: + return self.run_api_gpt(prompt, temperature) + elif 'gemini' in self.model: + return self.run_api_gemini(prompt, temperature) + + def run_api_gemini(self, prompt, temperature: float = 1): + genai.configure(api_key=util.GOOGLE_API_KEY, transport='rest') + generation_config = genai.types.GenerationConfig( + candidate_count=1, + temperature=temperature) + model = genai.GenerativeModel(self.model) + self.chat_history.append({"role": "user", "parts": [prompt]}) + max_retry = 2 + retry = 0 + while retry < max_retry: + try: + response = model.generate_content(contents=self.chat_history, generation_config=generation_config) + new_message_dict = {"role": 'model', "parts": [response.text]} + self.chat_history.append(new_message_dict) + return response.text + except Exception as e: + log.logger.warning("Gemini api retry...{}".format(e)) + retry += 1 + time.sleep(1) + log.logger.error("ERROR: GEMINI API FAILED. SKIP THIS INTERACTION.") + return "" + + + def run_api_gpt(self, prompt, temperature: float = 1): + openai.api_key = util.OPENAI_API_KEY + client = openai.OpenAI(api_key=openai.api_key) + self.chat_history.append({"role": "user", "content": prompt}) + max_retry = 2 + retry = 0 + + # just cut off the overflow tokens + # tokens = encoding.encode(self.chat_history) + + while retry < max_retry: + try: + response = client.chat.completions.create( + model=self.model, + messages=self.chat_history, + temperature=temperature, + ) + new_message_dict = {"role": response.choices[0].message.role, + "content": response.choices[0].message.content} + self.chat_history.append(new_message_dict) + resp = response.choices[0].message.content + return resp + except openai.OpenAIError as e: + log.logger.warning("OpenAI api retry...{}".format(e)) + retry += 1 + time.sleep(1) + log.logger.error("ERROR: OPENAI API FAILED. SKIP THIS INTERACTION.") + return "" + + def get_total_proper(self, stock_a_price, stock_b_price): + return self.stock_a_amount * stock_a_price + self.stock_b_amount * stock_b_price + self.cash + + def get_proper_cash_value(self, stock_a_price, stock_b_price): + proper = self.stock_a_amount * stock_a_price + self.stock_b_amount * stock_b_price + self.cash + a_value = self.stock_a_amount * stock_a_price + b_value = self.stock_b_amount * stock_b_price + return proper, self.cash, a_value, b_value + + def get_total_loan(self): + debt = 0 + for loan in self.loans: + debt += loan["amount"] + return debt + + def plan_loan(self, date, stock_a_price, stock_b_price, lastday_forum_message): + if self.quit: + return {"loan": "no"} + # first day action : prompt with background + if date == 1: + prompt = Collection(BACKGROUND_PROMPT, + LOAN_TYPE_PROMPT, + DECIDE_IF_LOAN_PROMPT).set_indexing_method(sharp2_indexing).set_sep("\n") + max_loan = self.init_proper - self.get_total_loan() + inputs = { + 'date': date, + 'character': self.character, + 'stock_a': self.stock_a_amount, + 'stock_b': self.stock_b_amount, + 'cash': self.cash, + 'debt': self.loans, + 'max_loan': max_loan, + 'loan_rate1': util.LOAN_RATE[0], + 'loan_rate2': util.LOAN_RATE[1], + 'loan_rate3': util.LOAN_RATE[2], + } + + # other days action : prompt with last day forum message & stock price + else: + prompt = Collection(BACKGROUND_PROMPT, + LASTDAY_FORUM_AND_STOCK_PROMPT, + LOAN_TYPE_PROMPT, + DECIDE_IF_LOAN_PROMPT).set_indexing_method(sharp2_indexing).set_sep("\n") + max_loan = self.init_proper - self.get_total_loan() + inputs = { + "date": date, + "character": self.character, + "stock_a": self.stock_a_amount, + "stock_b": self.stock_b_amount, + "cash": self.cash, + "debt": self.loans, + "max_loan": max_loan, + "stock_a_price": stock_a_price, + "stock_b_price": stock_b_price, + "lastday_forum_message": lastday_forum_message, + 'loan_rate1': util.LOAN_RATE[0], + 'loan_rate2': util.LOAN_RATE[1], + 'loan_rate3': util.LOAN_RATE[2], + } + if max_loan <= 0: + return {"loan": "no"} + try_times = 0 + MAX_TRY_TIMES = 3 + resp = self.run_api(format_prompt(prompt, inputs)) + # print(resp) + if resp == "": + return {"loan": "no"} + + loan_format_check, fail_response, loan = self.secretary.check_loan(resp, + max_loan) # secretary check loan format + while not loan_format_check: + # log.logger.debug("WARNING: Loan format check failed because of these issues: {}".format(fail_response)) + try_times += 1 + if try_times > MAX_TRY_TIMES: + log.logger.warning("WARNING: Loan format try times > MAX_TRY_TIMES. Skip as no loan today.") + loan = {"loan": "no"} + break + + resp = self.run_api(format_prompt(LOAN_RETRY_PROMPT, {"fail_response": fail_response})) + if resp == "": + return {"loan": "no"} + loan_format_check, fail_response, loan = self.secretary.check_loan(date, resp) + + if loan["loan"] == "yes": + loan["repayment_date"] = date + util.LOAN_TYPE_DATE[loan["loan_type"]] # add loan repayment_date + self.loans.append(loan) + #self.action_history[date].append(loan) + self.cash += loan["amount"] + log.logger.info("INFO: Agent {} decide to loan: {}".format(self.order, loan)) + else: + log.logger.info("INFO: Agent {} decide not to loan".format(self.order)) + return loan + + # date=交易日, time=当前交易时段 + # 设置 + def plan_stock(self, date, time, stock_a, stock_b, stock_a_deals, stock_b_deals): + if self.quit: + return {"action_type": "no"} + if date in util.SEASON_REPORT_DAYS and time == 1: + index = util.SEASON_REPORT_DAYS.index(date) + prompt = Collection(FIRST_DAY_FINANCIAL_REPORT, FIRST_DAY_BACKGROUND_KNOWLEDGE, SEASONAL_FINANCIAL_REPORT, + DECIDE_BUY_STOCK_PROMPT).set_indexing_method(sharp2_indexing).set_sep("\n") + inputs = { + "date": date, + "time": time, + "stock_a": self.stock_a_amount, + "stock_b": self.stock_b_amount, + "stock_a_price": stock_a.get_price(), + "stock_b_price": stock_b.get_price(), + "stock_a_deals": stock_a_deals, + "stock_b_deals": stock_b_deals, + "cash": self.cash, + "stock_a_report": stock_a.gen_financial_report(index), + "stock_b_report": stock_b.gen_financial_report(index) + } + elif time == 1: + prompt = Collection(FIRST_DAY_FINANCIAL_REPORT, FIRST_DAY_BACKGROUND_KNOWLEDGE, + DECIDE_BUY_STOCK_PROMPT).set_indexing_method(sharp2_indexing).set_sep("\n") + inputs = { + "date": date, + "time": time, + "stock_a": self.stock_a_amount, + "stock_b": self.stock_b_amount, + "stock_a_price": stock_a.get_price(), + "stock_b_price": stock_b.get_price(), + "stock_a_deals": stock_a_deals, + "stock_b_deals": stock_b_deals, + "cash": self.cash + } + else: + prompt = DECIDE_BUY_STOCK_PROMPT + inputs = { + "date": date, + "time": time, + "stock_a": self.stock_a_amount, + "stock_b": self.stock_b_amount, + "stock_a_price": stock_a.get_price(), + "stock_b_price": stock_b.get_price(), + "stock_a_deals": stock_a_deals, + "stock_b_deals": stock_b_deals, + "cash": self.cash + } + + + try_times = 0 + MAX_TRY_TIMES = 3 + resp = self.run_api(format_prompt(prompt, inputs)) + # print(resp) + if resp == "": + return {"action_type": "no"} + + action_format_check, fail_response, action = self.secretary.check_action( + resp, self.cash, self.stock_a_amount, self.stock_b_amount, stock_a.get_price(), stock_b.get_price()) + while not action_format_check: + # log.logger.debug("Action format check failed because of these issues: {}".format(fail_response)) + try_times += 1 + if try_times > MAX_TRY_TIMES: + log.logger.warning("WARNING: Action format try times > MAX_TRY_TIMES. Skip as no loan today.") + action = {"action_type": "no"} + break + + resp = self.run_api(format_prompt(BUY_STOCK_RETRY_PROMPT, {"fail_response": fail_response})) + if resp == "": + return {"action_type": "no"} + action_format_check, fail_response, action = self.secretary.check_action( + resp, self.cash, self.stock_a_amount, self.stock_b_amount, stock_a.get_price(), stock_b.get_price()) + + if action["action_type"] == "buy": + #self.action_history[date].append(action) + log.logger.info("INFO: Agent {} decide to action: {}".format(self.order, action)) + # if action["stock"] == "stock_a": + # self.stock_a_amount += action["amount"] + # self.cash -= action["amount"] * stock_a.get_price() + # else: + # self.stock_b_amount += action["amount"] + # self.cash -= action["amount"] * stock_b.get_price() + return action + elif action["action_type"] == "sell": + #self.action_history[date].append(action) + log.logger.info("INFO: Agent {} decide to action: {}".format(self.order, action)) + # if action["stock"] == "stock_a": + # self.stock_a_amount -= action["amount"] + # self.cash += action["amount"] * stock_a.get_price() + # else: + # self.stock_b_amount -= action["amount"] + # self.cash += action["amount"] * stock_b.get_price() + return action + elif action["action_type"] == "no": + log.logger.info("INFO: Agent {} decide not to action".format(self.order)) + return action + + log.logger.error("ERROR: WRONG ACTION: {}".format(action)) + return {"action_type": "no"} + + def buy_stock(self, stock_name, price, amount): + if self.quit: + return False + if self.cash < price * amount or stock_name not in ['A', 'B']: + log.logger.warning("ILLEGAL STOCK BUY BEHAVIOR: remain cash {}".format(self.cash)) + return False + self.cash -= price * amount + if stock_name == 'A': + self.stock_a_amount += amount + elif stock_name == 'B': + self.stock_b_amount += amount + + return True + + def sell_stock(self, stock_name, price, amount): + if self.quit: + return False + if stock_name == 'B' and self.stock_b_amount < amount: + log.logger.warning("ILLEGAL STOCK SELL BEHAVIOR: remain stock_b {}, amount {}".format(self.stock_b_amount, + amount)) + return False + elif stock_name == 'A' and self.stock_a_amount < amount: + log.logger.warning("ILLEGAL STOCK SELL BEHAVIOR: remain stock_a {}, amount {}".format(self.stock_a_amount, + amount)) + return False + if stock_name == 'A': + self.stock_a_amount -= amount + elif stock_name == 'B': + self.stock_b_amount -= amount + self.cash += price * amount + return True + + def loan_repayment(self, date): + if self.quit: + return + # check是否贷款还款日,还款,破产检查 + for loan in self.loans[:]: + if loan["repayment_date"] == date: + self.cash -= loan["amount"] * (1 + util.LOAN_RATE[loan["loan_type"]]) + self.loans.remove(loan) + if self.cash < 0: + self.is_bankrupt = True + + + def interest_payment(self): + if self.quit: + return + # 贷款付息日付息 + for loan in self.loans: + self.cash -= loan["amount"] * util.LOAN_RATE[loan["loan_type"]] / 12 + if self.cash < 0: + self.is_bankrupt = True + + def bankrupt_process(self, stock_a_price, stock_b_price): + if self.quit: + return False + total_value_of_stock = self.stock_a_amount * stock_a_price + self.stock_b_amount * stock_b_price + if total_value_of_stock + self.cash < 0: + log.logger.warning(f"Agent {self.order} bankrupt. ") + #f"Action history: " + str(self.action_history)) + return True + if stock_a_price * self.stock_a_amount >= -self.cash: + sell_a = math.ceil(-self.cash / stock_a_price) + self.stock_a_amount -= sell_a + self.cash += sell_a * stock_a_price + else: + self.cash += stock_a_price * self.stock_a_amount + self.stock_a_amount = 0 + sell_b = math.ceil(-self.cash / stock_b_price) + self.stock_b_amount -= sell_b + self.cash += sell_b * stock_b_price + + if self.stock_a_amount < 0 or self.stock_b_amount < 0 or self.cash < 0: + raise RuntimeError("ERROR: WRONG BANKRUPT PROCESS") + self.is_bankrupt = False + return False + + def post_message(self): + if self.quit: + return "" + prompt = format_prompt(POST_MESSAGE_PROMPT, inputs={}) + resp = self.run_api(prompt) + return resp + + def next_day_estimate(self): + if self.quit: + return {"buy_A": "no", "buy_B": "no", "sell_A": "no", "sell_B": "no", "loan": "no"} + prompt = format_prompt(NEXT_DAY_ESTIMATE_PROMPT, inputs={}) + resp = self.run_api(prompt) + if resp == "": + return {"buy_A": "no", "buy_B": "no", "sell_A": "no", "sell_B": "no", "loan": "no"} + format_check, fail_response, estimate = self.secretary.check_estimate(resp) + try_times = 0 + MAX_TRY_TIMES = 3 + while not format_check: + try_times += 1 + if try_times > MAX_TRY_TIMES: + log.logger.warning("WARNING: Estimation format try times > MAX_TRY_TIMES. Skip as all 'no' today.") + estimate = {"buy_A": "no", "buy_B": "no", "sell_A": "no", "sell_B": "no", "loan": "no"} + break + resp = self.run_api(format_prompt(NEXT_DAY_ESTIMATE_RETRY, {"fail_response": fail_response})) + if resp == "": + return {"buy_A": "no", "buy_B": "no", "sell_A": "no", "sell_B": "no", "loan": "no"} + format_check, fail_response, estimate = self.secretary.check_estimate(resp) + return estimate + + diff --git a/examples/Stockagent/log/custom_logger.py b/examples/Stockagent/log/custom_logger.py new file mode 100644 index 0000000..c423d91 --- /dev/null +++ b/examples/Stockagent/log/custom_logger.py @@ -0,0 +1,41 @@ +import logging +from colorama import Fore, Style, Back + +class ColoredFormatter(logging.Formatter): + def format(self, record): + levelname_color = { + 'DEBUG': Fore.CYAN + Style.BRIGHT, + 'INFO': Fore.GREEN + Style.BRIGHT, + 'WARNING': Fore.YELLOW + Style.BRIGHT, + 'ERROR': Fore.RED + Style.BRIGHT, + 'CRITICAL': Fore.RED + Style.BRIGHT, + } + message = super().format(record) + if record.levelname in levelname_color: + message = levelname_color[record.levelname] + message + Style.RESET_ALL + return message + + +class CustomLogger: + def __init__(self): + self.log_file = 'log/test.txt' + self.logger = logging.getLogger('Stocklogger') + self.logger.setLevel(logging.DEBUG) + + # 创建一个handler用于写入日志文件 + file_handler = logging.FileHandler(self.log_file) + file_handler.setLevel(logging.DEBUG) + plain_formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') + file_handler.setFormatter(plain_formatter) + + # 创建一个handler用于输出到控制台(带有颜色) + console_handler = logging.StreamHandler() + console_handler.setLevel(logging.DEBUG) + colored_formatter = ColoredFormatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') + console_handler.setFormatter(colored_formatter) + + self.logger.addHandler(file_handler) + self.logger.addHandler(console_handler) + + +log = CustomLogger() diff --git a/examples/Stockagent/log/test.txt b/examples/Stockagent/log/test.txt new file mode 100644 index 0000000..3d36cd1 --- /dev/null +++ b/examples/Stockagent/log/test.txt @@ -0,0 +1,2306 @@ +2025-10-24 16:01:06,975 - Stocklogger - DEBUG - Agents initial... +2025-10-24 16:01:06,975 - Stocklogger - DEBUG - cash: 2000912.9010808768, stock a: 16319, stock b:37960, debt: [{'loan': 'yes', 'amount': 2457245.72356737, 'loan_type': 0, 'repayment_date': 88}] +2025-10-24 16:01:06,975 - Stocklogger - DEBUG - cash: 3406051.8712514797, stock a: 30502, stock b:12714, debt: [{'loan': 'yes', 'amount': 3126925.2067254465, 'loan_type': 2, 'repayment_date': 132}] +2025-10-24 16:01:06,975 - Stocklogger - DEBUG - cash: 2493616.4008787647, stock a: 71183, stock b:2228, debt: [{'loan': 'yes', 'amount': 1126548.1084998674, 'loan_type': 2, 'repayment_date': 242}] +2025-10-24 16:01:06,975 - Stocklogger - DEBUG - cash: 685371.9156391885, stock a: 10002, stock b:56018, debt: [{'loan': 'yes', 'amount': 1447427.6769661803, 'loan_type': 2, 'repayment_date': 242}] +2025-10-24 16:01:06,975 - Stocklogger - DEBUG - cash: 1038215.9319146388, stock a: 113103, stock b:3138, debt: [{'loan': 'yes', 'amount': 1661296.989551057, 'loan_type': 2, 'repayment_date': 264}] +2025-10-24 16:01:06,975 - Stocklogger - DEBUG - cash: 351762.02783490426, stock a: 16091, stock b:79435, debt: [{'loan': 'yes', 'amount': 3046867.113718262, 'loan_type': 0, 'repayment_date': 88}] +2025-10-24 16:01:06,976 - Stocklogger - DEBUG - cash: 1888245.8949236819, stock a: 10327, stock b:38953, debt: [{'loan': 'yes', 'amount': 2539684.1551537025, 'loan_type': 2, 'repayment_date': 110}] +2025-10-24 16:01:06,976 - Stocklogger - DEBUG - cash: 738758.3395750547, stock a: 37442, stock b:16356, debt: [{'loan': 'yes', 'amount': 588877.0268161764, 'loan_type': 2, 'repayment_date': 176}] +2025-10-24 16:01:06,976 - Stocklogger - DEBUG - cash: 903846.8053006931, stock a: 9840, stock b:62216, debt: [{'loan': 'yes', 'amount': 272247.0800900984, 'loan_type': 1, 'repayment_date': 154}] +2025-10-24 16:01:06,976 - Stocklogger - DEBUG - cash: 1200063.1459595796, stock a: 49322, stock b:52464, debt: [{'loan': 'yes', 'amount': 2248690.696698797, 'loan_type': 0, 'repayment_date': 198}] +2025-10-24 16:01:06,976 - Stocklogger - DEBUG - cash: 1691016.5817594025, stock a: 23382, stock b:55800, debt: [{'loan': 'yes', 'amount': 3720252.8617382795, 'loan_type': 2, 'repayment_date': 154}] +2025-10-24 16:01:06,976 - Stocklogger - DEBUG - cash: 381567.61739724263, stock a: 60473, stock b:11775, debt: [{'loan': 'yes', 'amount': 706890.974655242, 'loan_type': 1, 'repayment_date': 88}] +2025-10-24 16:01:06,976 - Stocklogger - DEBUG - cash: 1591897.2080128556, stock a: 39833, stock b:53770, debt: [{'loan': 'yes', 'amount': 2592453.6314102993, 'loan_type': 0, 'repayment_date': 22}] +2025-10-24 16:01:06,976 - Stocklogger - DEBUG - cash: 512317.6743849733, stock a: 120357, stock b:17245, debt: [{'loan': 'yes', 'amount': 3298978.1981310975, 'loan_type': 0, 'repayment_date': 220}] +2025-10-24 16:01:06,976 - Stocklogger - DEBUG - cash: 2079073.338761691, stock a: 53506, stock b:4455, debt: [{'loan': 'yes', 'amount': 1985694.0946037376, 'loan_type': 2, 'repayment_date': 154}] +2025-10-24 16:01:06,976 - Stocklogger - DEBUG - cash: 164174.82192236042, stock a: 80068, stock b:12326, debt: [{'loan': 'yes', 'amount': 1465484.5476633466, 'loan_type': 0, 'repayment_date': 66}] +2025-10-24 16:01:06,977 - Stocklogger - DEBUG - cash: 1258500.8148744646, stock a: 88904, stock b:82, debt: [{'loan': 'yes', 'amount': 251836.866664526, 'loan_type': 1, 'repayment_date': 110}] +2025-10-24 16:01:06,977 - Stocklogger - DEBUG - cash: 554365.9271883988, stock a: 80707, stock b:37935, debt: [{'loan': 'yes', 'amount': 68885.58630959607, 'loan_type': 2, 'repayment_date': 220}] +2025-10-24 16:01:06,977 - Stocklogger - DEBUG - cash: 581626.178314395, stock a: 53381, stock b:66540, debt: [{'loan': 'yes', 'amount': 225831.438117875, 'loan_type': 0, 'repayment_date': 242}] +2025-10-24 16:01:06,977 - Stocklogger - DEBUG - cash: 2795238.0381167964, stock a: 6396, stock b:25236, debt: [{'loan': 'yes', 'amount': 1018707.5825887376, 'loan_type': 2, 'repayment_date': 22}] +2025-10-24 16:01:06,977 - Stocklogger - DEBUG - cash: 1501163.0277241573, stock a: 12499, stock b:59083, debt: [{'loan': 'yes', 'amount': 4117594.054122464, 'loan_type': 2, 'repayment_date': 154}] +2025-10-24 16:01:06,977 - Stocklogger - DEBUG - cash: 1131709.6212854034, stock a: 24364, stock b:38388, debt: [{'loan': 'yes', 'amount': 1028053.0988321429, 'loan_type': 0, 'repayment_date': 88}] +2025-10-24 16:01:06,977 - Stocklogger - DEBUG - cash: 1488386.895335933, stock a: 57018, stock b:40072, debt: [{'loan': 'yes', 'amount': 2864325.1165557452, 'loan_type': 0, 'repayment_date': 154}] +2025-10-24 16:01:06,978 - Stocklogger - DEBUG - cash: 3118960.4729923964, stock a: 43961, stock b:3507, debt: [{'loan': 'yes', 'amount': 3306277.7691200636, 'loan_type': 1, 'repayment_date': 154}] +2025-10-24 16:01:06,978 - Stocklogger - DEBUG - cash: 3345689.79207097, stock a: 42090, stock b:1505, debt: [{'loan': 'yes', 'amount': 2435801.5242399005, 'loan_type': 0, 'repayment_date': 110}] +2025-10-24 16:01:06,978 - Stocklogger - DEBUG - cash: 983370.8030743587, stock a: 44256, stock b:48426, debt: [{'loan': 'yes', 'amount': 3140128.6205363916, 'loan_type': 1, 'repayment_date': 242}] +2025-10-24 16:01:06,978 - Stocklogger - DEBUG - cash: 2641141.0996722933, stock a: 8952, stock b:41074, debt: [{'loan': 'yes', 'amount': 2644648.182601583, 'loan_type': 0, 'repayment_date': 110}] +2025-10-24 16:01:06,978 - Stocklogger - DEBUG - cash: 474583.873748266, stock a: 22290, stock b:44084, debt: [{'loan': 'yes', 'amount': 1483716.1884894362, 'loan_type': 1, 'repayment_date': 154}] +2025-10-24 16:01:06,978 - Stocklogger - DEBUG - cash: 3378461.255883217, stock a: 5170, stock b:5460, debt: [{'loan': 'yes', 'amount': 3305945.3411300243, 'loan_type': 0, 'repayment_date': 154}] +2025-10-24 16:01:06,978 - Stocklogger - DEBUG - cash: 930518.2335784512, stock a: 28368, stock b:1135, debt: [{'loan': 'yes', 'amount': 1076755.003453944, 'loan_type': 0, 'repayment_date': 132}] +2025-10-24 16:01:06,978 - Stocklogger - DEBUG - cash: 2086016.07393684, stock a: 912, stock b:56287, debt: [{'loan': 'yes', 'amount': 4099283.4300486804, 'loan_type': 1, 'repayment_date': 220}] +2025-10-24 16:01:06,978 - Stocklogger - DEBUG - cash: 1160640.982651459, stock a: 42648, stock b:45833, debt: [{'loan': 'yes', 'amount': 479801.8014833111, 'loan_type': 1, 'repayment_date': 110}] +2025-10-24 16:01:06,978 - Stocklogger - DEBUG - cash: 1724148.288724745, stock a: 50885, stock b:34154, debt: [{'loan': 'yes', 'amount': 2466860.7905180384, 'loan_type': 2, 'repayment_date': 110}] +2025-10-24 16:01:06,979 - Stocklogger - DEBUG - cash: 2010208.553111809, stock a: 10858, stock b:63587, debt: [{'loan': 'yes', 'amount': 1338600.2695924938, 'loan_type': 1, 'repayment_date': 242}] +2025-10-24 16:01:06,979 - Stocklogger - DEBUG - cash: 439688.2534369012, stock a: 22717, stock b:49283, debt: [{'loan': 'yes', 'amount': 2723257.3659007237, 'loan_type': 1, 'repayment_date': 220}] +2025-10-24 16:01:06,979 - Stocklogger - DEBUG - cash: 934343.3464666179, stock a: 122876, stock b:5269, debt: [{'loan': 'yes', 'amount': 380225.68136167503, 'loan_type': 2, 'repayment_date': 242}] +2025-10-24 16:01:06,979 - Stocklogger - DEBUG - cash: 1842545.431433203, stock a: 556, stock b:74041, debt: [{'loan': 'yes', 'amount': 3663408.648505916, 'loan_type': 2, 'repayment_date': 220}] +2025-10-24 16:01:06,979 - Stocklogger - DEBUG - cash: 2086969.5165656733, stock a: 6627, stock b:25840, debt: [{'loan': 'yes', 'amount': 3135920.4672189965, 'loan_type': 2, 'repayment_date': 242}] +2025-10-24 16:01:06,979 - Stocklogger - DEBUG - cash: 3137209.210600154, stock a: 14707, stock b:14249, debt: [{'loan': 'yes', 'amount': 2308494.552392972, 'loan_type': 2, 'repayment_date': 242}] +2025-10-24 16:01:06,979 - Stocklogger - DEBUG - cash: 1677172.31994307, stock a: 35992, stock b:7840, debt: [{'loan': 'yes', 'amount': 2215768.5136948316, 'loan_type': 0, 'repayment_date': 66}] +2025-10-24 16:01:06,979 - Stocklogger - DEBUG - cash: 404120.13235807675, stock a: 59429, stock b:22196, debt: [{'loan': 'yes', 'amount': 2797066.470939952, 'loan_type': 0, 'repayment_date': 132}] +2025-10-24 16:01:06,979 - Stocklogger - DEBUG - cash: 2740665.13351626, stock a: 4524, stock b:40267, debt: [{'loan': 'yes', 'amount': 2058530.0179468675, 'loan_type': 0, 'repayment_date': 22}] +2025-10-24 16:01:06,979 - Stocklogger - DEBUG - cash: 2732821.9780123597, stock a: 41669, stock b:9311, debt: [{'loan': 'yes', 'amount': 1694554.2698690358, 'loan_type': 0, 'repayment_date': 198}] +2025-10-24 16:01:06,979 - Stocklogger - DEBUG - cash: 2938783.324487879, stock a: 7666, stock b:33973, debt: [{'loan': 'yes', 'amount': 786137.115446891, 'loan_type': 0, 'repayment_date': 44}] +2025-10-24 16:01:06,980 - Stocklogger - DEBUG - cash: 347130.02518974134, stock a: 50732, stock b:21460, debt: [{'loan': 'yes', 'amount': 1986967.7386120055, 'loan_type': 1, 'repayment_date': 176}] +2025-10-24 16:01:06,980 - Stocklogger - DEBUG - cash: 2108144.5904003354, stock a: 21849, stock b:37594, debt: [{'loan': 'yes', 'amount': 3822317.8379383706, 'loan_type': 2, 'repayment_date': 176}] +2025-10-24 16:01:06,980 - Stocklogger - DEBUG - cash: 2065479.989068924, stock a: 51412, stock b:9454, debt: [{'loan': 'yes', 'amount': 1189901.0164871477, 'loan_type': 1, 'repayment_date': 264}] +2025-10-24 16:01:06,980 - Stocklogger - DEBUG - cash: 317190.88872898207, stock a: 42815, stock b:45707, debt: [{'loan': 'yes', 'amount': 3074568.178822307, 'loan_type': 1, 'repayment_date': 88}] +2025-10-24 16:01:06,980 - Stocklogger - DEBUG - cash: 989100.3255816388, stock a: 58638, stock b:49933, debt: [{'loan': 'yes', 'amount': 2890194.5876025897, 'loan_type': 0, 'repayment_date': 176}] +2025-10-24 16:01:06,980 - Stocklogger - DEBUG - cash: 650920.7444960513, stock a: 55103, stock b:59125, debt: [{'loan': 'yes', 'amount': 4061441.757320525, 'loan_type': 0, 'repayment_date': 242}] +2025-10-24 16:01:06,980 - Stocklogger - DEBUG - --------Simulation Start!-------- +2025-10-24 16:01:06,980 - Stocklogger - DEBUG - --------DAY 1--------- +2025-10-24 16:01:06,980 - Stocklogger - DEBUG - Wrong json content in response: None +2025-10-24 16:01:06,980 - Stocklogger - DEBUG - Wrong json content in response: 1 +2025-10-24 16:01:06,980 - Stocklogger - DEBUG - Wrong json content in response: 1 +2025-10-24 16:01:06,981 - Stocklogger - DEBUG - Wrong json content in response: 1 +2025-10-24 16:01:06,981 - Stocklogger - WARNING - WARNING: Loan format try times > MAX_TRY_TIMES. Skip as no loan today. +2025-10-24 16:01:06,981 - Stocklogger - INFO - INFO: Agent 0 decide not to loan +2025-10-24 16:01:06,981 - Stocklogger - DEBUG - Wrong json content in response: None +2025-10-24 16:01:06,981 - Stocklogger - DEBUG - Wrong json content in response: 1 +2025-10-24 16:01:06,981 - Stocklogger - DEBUG - Wrong json content in response: 1 +2025-10-24 16:01:06,981 - Stocklogger - DEBUG - Wrong json content in response: 1 +2025-10-24 16:01:06,981 - Stocklogger - WARNING - WARNING: Loan format try times > MAX_TRY_TIMES. Skip as no loan today. +2025-10-24 16:01:06,981 - Stocklogger - INFO - INFO: Agent 1 decide not to loan +2025-10-24 16:01:06,981 - Stocklogger - DEBUG - Wrong json content in response: None +2025-10-24 16:01:06,981 - Stocklogger - DEBUG - Wrong json content in response: 1 +2025-10-24 16:01:06,981 - Stocklogger - DEBUG - Wrong json content in response: 1 +2025-10-24 16:01:06,981 - Stocklogger - DEBUG - Wrong json content in response: 1 +2025-10-24 16:01:06,981 - Stocklogger - WARNING - WARNING: Loan format try times > MAX_TRY_TIMES. Skip as no loan today. +2025-10-24 16:01:06,981 - Stocklogger - INFO - INFO: Agent 2 decide not to loan +2025-10-24 16:01:06,982 - Stocklogger - DEBUG - Wrong json content in response: None +2025-10-24 16:01:06,982 - Stocklogger - DEBUG - Wrong json content in response: 1 +2025-10-24 16:01:06,982 - Stocklogger - DEBUG - Wrong json content in response: 1 +2025-10-24 16:01:06,982 - Stocklogger - DEBUG - Wrong json content in response: 1 +2025-10-24 16:01:06,982 - Stocklogger - WARNING - WARNING: Loan format try times > MAX_TRY_TIMES. Skip as no loan today. +2025-10-24 16:01:06,982 - Stocklogger - INFO - INFO: Agent 3 decide not to loan +2025-10-24 16:01:06,982 - Stocklogger - DEBUG - Wrong json content in response: None +2025-10-24 16:01:06,982 - Stocklogger - DEBUG - Wrong json content in response: 1 +2025-10-24 16:01:06,982 - Stocklogger - DEBUG - Wrong json content in response: 1 +2025-10-24 16:01:06,982 - Stocklogger - DEBUG - Wrong json content in response: 1 +2025-10-24 16:01:06,982 - Stocklogger - WARNING - WARNING: Loan format try times > MAX_TRY_TIMES. Skip as no loan today. +2025-10-24 16:01:06,982 - Stocklogger - INFO - INFO: Agent 4 decide not to loan +2025-10-24 16:01:06,982 - Stocklogger - DEBUG - Wrong json content in response: None +2025-10-24 16:01:06,982 - Stocklogger - DEBUG - Wrong json content in response: 1 +2025-10-24 16:01:06,983 - Stocklogger - DEBUG - Wrong json content in response: 1 +2025-10-24 16:01:06,983 - Stocklogger - DEBUG - Wrong json content in response: 1 +2025-10-24 16:01:06,983 - Stocklogger - WARNING - WARNING: Loan format try times > MAX_TRY_TIMES. Skip as no loan today. +2025-10-24 16:01:06,983 - Stocklogger - INFO - INFO: Agent 5 decide not to loan +2025-10-24 16:01:06,983 - Stocklogger - DEBUG - Wrong json content in response: None +2025-10-24 16:01:06,983 - Stocklogger - DEBUG - Wrong json content in response: 1 +2025-10-24 16:01:06,983 - Stocklogger - DEBUG - Wrong json content in response: 1 +2025-10-24 16:01:06,983 - Stocklogger - DEBUG - Wrong json content in response: 1 +2025-10-24 16:01:06,983 - Stocklogger - WARNING - WARNING: Loan format try times > MAX_TRY_TIMES. Skip as no loan today. +2025-10-24 16:01:06,983 - Stocklogger - INFO - INFO: Agent 6 decide not to loan +2025-10-24 16:01:06,983 - Stocklogger - DEBUG - Wrong json content in response: None +2025-10-24 16:01:06,983 - Stocklogger - DEBUG - Wrong json content in response: 1 +2025-10-24 16:01:06,983 - Stocklogger - DEBUG - Wrong json content in response: 1 +2025-10-24 16:01:06,983 - Stocklogger - DEBUG - Wrong json content in response: 1 +2025-10-24 16:01:06,983 - Stocklogger - WARNING - WARNING: Loan format try times > MAX_TRY_TIMES. Skip as no loan today. +2025-10-24 16:01:06,983 - Stocklogger - INFO - INFO: Agent 7 decide not to loan +2025-10-24 16:01:06,984 - Stocklogger - DEBUG - Wrong json content in response: None +2025-10-24 16:01:06,984 - Stocklogger - DEBUG - Wrong json content in response: 1 +2025-10-24 16:01:06,984 - Stocklogger - DEBUG - Wrong json content in response: 1 +2025-10-24 16:01:06,984 - Stocklogger - DEBUG - Wrong json content in response: 1 +2025-10-24 16:01:06,984 - Stocklogger - WARNING - WARNING: Loan format try times > MAX_TRY_TIMES. Skip as no loan today. +2025-10-24 16:01:06,984 - Stocklogger - INFO - INFO: Agent 8 decide not to loan +2025-10-24 16:01:06,984 - Stocklogger - DEBUG - Wrong json content in response: None +2025-10-24 16:01:06,984 - Stocklogger - DEBUG - Wrong json content in response: 1 +2025-10-24 16:01:06,984 - Stocklogger - DEBUG - Wrong json content in response: 1 +2025-10-24 16:01:06,984 - Stocklogger - DEBUG - Wrong json content in response: 1 +2025-10-24 16:01:06,984 - Stocklogger - WARNING - WARNING: Loan format try times > MAX_TRY_TIMES. Skip as no loan today. +2025-10-24 16:01:06,984 - Stocklogger - INFO - INFO: Agent 9 decide not to loan +2025-10-24 16:01:06,984 - Stocklogger - DEBUG - Wrong json content in response: None +2025-10-24 16:01:06,984 - Stocklogger - DEBUG - Wrong json content in response: 1 +2025-10-24 16:01:06,984 - Stocklogger - DEBUG - Wrong json content in response: 1 +2025-10-24 16:01:06,984 - Stocklogger - DEBUG - Wrong json content in response: 1 +2025-10-24 16:01:06,985 - Stocklogger - WARNING - WARNING: Loan format try times > MAX_TRY_TIMES. Skip as no loan today. +2025-10-24 16:01:06,985 - Stocklogger - INFO - INFO: Agent 10 decide not to loan +2025-10-24 16:01:06,985 - Stocklogger - DEBUG - Wrong json content in response: None +2025-10-24 16:01:06,985 - Stocklogger - DEBUG - Wrong json content in response: 1 +2025-10-24 16:01:06,985 - Stocklogger - DEBUG - Wrong json content in response: 1 +2025-10-24 16:01:06,985 - Stocklogger - DEBUG - Wrong json content in response: 1 +2025-10-24 16:01:06,985 - Stocklogger - WARNING - WARNING: Loan format try times > MAX_TRY_TIMES. Skip as no loan today. +2025-10-24 16:01:06,985 - Stocklogger - INFO - INFO: Agent 11 decide not to loan +2025-10-24 16:01:06,985 - Stocklogger - DEBUG - Wrong json content in response: None +2025-10-24 16:01:06,985 - Stocklogger - DEBUG - Wrong json content in response: 1 +2025-10-24 16:01:06,985 - Stocklogger - DEBUG - Wrong json content in response: 1 +2025-10-24 16:01:06,985 - Stocklogger - DEBUG - Wrong json content in response: 1 +2025-10-24 16:01:06,985 - Stocklogger - WARNING - WARNING: Loan format try times > MAX_TRY_TIMES. Skip as no loan today. +2025-10-24 16:01:06,985 - Stocklogger - INFO - INFO: Agent 12 decide not to loan +2025-10-24 16:01:06,985 - Stocklogger - DEBUG - Wrong json content in response: None +2025-10-24 16:01:06,985 - Stocklogger - DEBUG - Wrong json content in response: 1 +2025-10-24 16:01:06,986 - Stocklogger - DEBUG - Wrong json content in response: 1 +2025-10-24 16:01:06,986 - Stocklogger - DEBUG - Wrong json content in response: 1 +2025-10-24 16:01:06,986 - Stocklogger - WARNING - WARNING: Loan format try times > MAX_TRY_TIMES. Skip as no loan today. +2025-10-24 16:01:06,986 - Stocklogger - INFO - INFO: Agent 13 decide not to loan +2025-10-24 16:01:06,986 - Stocklogger - DEBUG - Wrong json content in response: None +2025-10-24 16:01:06,986 - Stocklogger - DEBUG - Wrong json content in response: 1 +2025-10-24 16:01:06,986 - Stocklogger - DEBUG - Wrong json content in response: 1 +2025-10-24 16:01:06,986 - Stocklogger - DEBUG - Wrong json content in response: 1 +2025-10-24 16:01:06,986 - Stocklogger - WARNING - WARNING: Loan format try times > MAX_TRY_TIMES. Skip as no loan today. +2025-10-24 16:01:06,986 - Stocklogger - INFO - INFO: Agent 14 decide not to loan +2025-10-24 16:01:06,986 - Stocklogger - DEBUG - Wrong json content in response: None +2025-10-24 16:01:06,986 - Stocklogger - DEBUG - Wrong json content in response: 1 +2025-10-24 16:01:06,986 - Stocklogger - DEBUG - Wrong json content in response: 1 +2025-10-24 16:01:06,986 - Stocklogger - DEBUG - Wrong json content in response: 1 +2025-10-24 16:01:06,986 - Stocklogger - WARNING - WARNING: Loan format try times > MAX_TRY_TIMES. Skip as no loan today. +2025-10-24 16:01:06,986 - Stocklogger - INFO - INFO: Agent 15 decide not to loan +2025-10-24 16:01:06,986 - Stocklogger - DEBUG - Wrong json content in response: None +2025-10-24 16:01:06,986 - Stocklogger - DEBUG - Wrong json content in response: 1 +2025-10-24 16:01:06,987 - Stocklogger - DEBUG - Wrong json content in response: 1 +2025-10-24 16:01:06,987 - Stocklogger - DEBUG - Wrong json content in response: 1 +2025-10-24 16:01:06,987 - Stocklogger - WARNING - WARNING: Loan format try times > MAX_TRY_TIMES. Skip as no loan today. +2025-10-24 16:01:06,987 - Stocklogger - INFO - INFO: Agent 16 decide not to loan +2025-10-24 16:01:06,987 - Stocklogger - DEBUG - Wrong json content in response: None +2025-10-24 16:01:06,987 - Stocklogger - DEBUG - Wrong json content in response: 1 +2025-10-24 16:01:06,987 - Stocklogger - DEBUG - Wrong json content in response: 1 +2025-10-24 16:01:06,987 - Stocklogger - DEBUG - Wrong json content in response: 1 +2025-10-24 16:01:06,987 - Stocklogger - WARNING - WARNING: Loan format try times > MAX_TRY_TIMES. Skip as no loan today. +2025-10-24 16:01:06,987 - Stocklogger - INFO - INFO: Agent 17 decide not to loan +2025-10-24 16:01:06,987 - Stocklogger - DEBUG - Wrong json content in response: None +2025-10-24 16:01:06,987 - Stocklogger - DEBUG - Wrong json content in response: 1 +2025-10-24 16:01:06,987 - Stocklogger - DEBUG - Wrong json content in response: 1 +2025-10-24 16:01:06,987 - Stocklogger - DEBUG - Wrong json content in response: 1 +2025-10-24 16:01:06,987 - Stocklogger - WARNING - WARNING: Loan format try times > MAX_TRY_TIMES. Skip as no loan today. +2025-10-24 16:01:06,987 - Stocklogger - INFO - INFO: Agent 18 decide not to loan +2025-10-24 16:01:06,987 - Stocklogger - DEBUG - Wrong json content in response: None +2025-10-24 16:01:06,988 - Stocklogger - DEBUG - Wrong json content in response: 1 +2025-10-24 16:01:06,988 - Stocklogger - DEBUG - Wrong json content in response: 1 +2025-10-24 16:01:06,988 - Stocklogger - DEBUG - Wrong json content in response: 1 +2025-10-24 16:01:06,988 - Stocklogger - WARNING - WARNING: Loan format try times > MAX_TRY_TIMES. Skip as no loan today. +2025-10-24 16:01:06,988 - Stocklogger - INFO - INFO: Agent 19 decide not to loan +2025-10-24 16:01:06,988 - Stocklogger - DEBUG - Wrong json content in response: None +2025-10-24 16:01:06,988 - Stocklogger - DEBUG - Wrong json content in response: 1 +2025-10-24 16:01:06,988 - Stocklogger - DEBUG - Wrong json content in response: 1 +2025-10-24 16:01:06,988 - Stocklogger - DEBUG - Wrong json content in response: 1 +2025-10-24 16:01:06,988 - Stocklogger - WARNING - WARNING: Loan format try times > MAX_TRY_TIMES. Skip as no loan today. +2025-10-24 16:01:06,988 - Stocklogger - INFO - INFO: Agent 20 decide not to loan +2025-10-24 16:01:06,988 - Stocklogger - DEBUG - Wrong json content in response: None +2025-10-24 16:01:06,988 - Stocklogger - DEBUG - Wrong json content in response: 1 +2025-10-24 16:01:06,988 - Stocklogger - DEBUG - Wrong json content in response: 1 +2025-10-24 16:01:06,988 - Stocklogger - DEBUG - Wrong json content in response: 1 +2025-10-24 16:01:06,988 - Stocklogger - WARNING - WARNING: Loan format try times > MAX_TRY_TIMES. Skip as no loan today. +2025-10-24 16:01:06,988 - Stocklogger - INFO - INFO: Agent 21 decide not to loan +2025-10-24 16:01:06,989 - Stocklogger - DEBUG - Wrong json content in response: None +2025-10-24 16:01:06,989 - Stocklogger - DEBUG - Wrong json content in response: 1 +2025-10-24 16:01:06,989 - Stocklogger - DEBUG - Wrong json content in response: 1 +2025-10-24 16:01:06,989 - Stocklogger - DEBUG - Wrong json content in response: 1 +2025-10-24 16:01:06,989 - Stocklogger - WARNING - WARNING: Loan format try times > MAX_TRY_TIMES. Skip as no loan today. +2025-10-24 16:01:06,989 - Stocklogger - INFO - INFO: Agent 22 decide not to loan +2025-10-24 16:01:06,989 - Stocklogger - DEBUG - Wrong json content in response: None +2025-10-24 16:01:06,989 - Stocklogger - DEBUG - Wrong json content in response: 1 +2025-10-24 16:01:06,989 - Stocklogger - DEBUG - Wrong json content in response: 1 +2025-10-24 16:01:06,989 - Stocklogger - DEBUG - Wrong json content in response: 1 +2025-10-24 16:01:06,989 - Stocklogger - WARNING - WARNING: Loan format try times > MAX_TRY_TIMES. Skip as no loan today. +2025-10-24 16:01:06,989 - Stocklogger - INFO - INFO: Agent 23 decide not to loan +2025-10-24 16:01:06,989 - Stocklogger - DEBUG - Wrong json content in response: None +2025-10-24 16:01:06,989 - Stocklogger - DEBUG - Wrong json content in response: 1 +2025-10-24 16:01:06,989 - Stocklogger - DEBUG - Wrong json content in response: 1 +2025-10-24 16:01:06,989 - Stocklogger - DEBUG - Wrong json content in response: 1 +2025-10-24 16:01:06,990 - Stocklogger - WARNING - WARNING: Loan format try times > MAX_TRY_TIMES. Skip as no loan today. +2025-10-24 16:01:06,990 - Stocklogger - INFO - INFO: Agent 24 decide not to loan +2025-10-24 16:01:06,990 - Stocklogger - DEBUG - Wrong json content in response: None +2025-10-24 16:01:06,990 - Stocklogger - DEBUG - Wrong json content in response: 1 +2025-10-24 16:01:06,990 - Stocklogger - DEBUG - Wrong json content in response: 1 +2025-10-24 16:01:06,990 - Stocklogger - DEBUG - Wrong json content in response: 1 +2025-10-24 16:01:06,990 - Stocklogger - WARNING - WARNING: Loan format try times > MAX_TRY_TIMES. Skip as no loan today. +2025-10-24 16:01:06,990 - Stocklogger - INFO - INFO: Agent 25 decide not to loan +2025-10-24 16:01:06,990 - Stocklogger - DEBUG - Wrong json content in response: None +2025-10-24 16:01:06,990 - Stocklogger - DEBUG - Wrong json content in response: 1 +2025-10-24 16:01:06,990 - Stocklogger - DEBUG - Wrong json content in response: 1 +2025-10-24 16:01:06,990 - Stocklogger - DEBUG - Wrong json content in response: 1 +2025-10-24 16:01:06,990 - Stocklogger - WARNING - WARNING: Loan format try times > MAX_TRY_TIMES. Skip as no loan today. +2025-10-24 16:01:06,990 - Stocklogger - INFO - INFO: Agent 26 decide not to loan +2025-10-24 16:01:06,990 - Stocklogger - DEBUG - Wrong json content in response: None +2025-10-24 16:01:06,990 - Stocklogger - DEBUG - Wrong json content in response: 1 +2025-10-24 16:01:06,991 - Stocklogger - DEBUG - Wrong json content in response: 1 +2025-10-24 16:01:06,991 - Stocklogger - DEBUG - Wrong json content in response: 1 +2025-10-24 16:01:06,991 - Stocklogger - WARNING - WARNING: Loan format try times > MAX_TRY_TIMES. Skip as no loan today. +2025-10-24 16:01:06,991 - Stocklogger - INFO - INFO: Agent 27 decide not to loan +2025-10-24 16:01:06,991 - Stocklogger - DEBUG - Wrong json content in response: None +2025-10-24 16:01:06,991 - Stocklogger - DEBUG - Wrong json content in response: 1 +2025-10-24 16:01:06,991 - Stocklogger - DEBUG - Wrong json content in response: 1 +2025-10-24 16:01:06,991 - Stocklogger - DEBUG - Wrong json content in response: 1 +2025-10-24 16:01:06,991 - Stocklogger - WARNING - WARNING: Loan format try times > MAX_TRY_TIMES. Skip as no loan today. +2025-10-24 16:01:06,991 - Stocklogger - INFO - INFO: Agent 28 decide not to loan +2025-10-24 16:01:06,991 - Stocklogger - DEBUG - Wrong json content in response: None +2025-10-24 16:01:06,991 - Stocklogger - DEBUG - Wrong json content in response: 1 +2025-10-24 16:01:06,991 - Stocklogger - DEBUG - Wrong json content in response: 1 +2025-10-24 16:01:06,991 - Stocklogger - DEBUG - Wrong json content in response: 1 +2025-10-24 16:01:06,991 - Stocklogger - WARNING - WARNING: Loan format try times > MAX_TRY_TIMES. Skip as no loan today. +2025-10-24 16:01:06,991 - Stocklogger - INFO - INFO: Agent 29 decide not to loan +2025-10-24 16:01:06,991 - Stocklogger - DEBUG - Wrong json content in response: None +2025-10-24 16:01:06,992 - Stocklogger - DEBUG - Wrong json content in response: 1 +2025-10-24 16:01:06,992 - Stocklogger - DEBUG - Wrong json content in response: 1 +2025-10-24 16:01:06,992 - Stocklogger - DEBUG - Wrong json content in response: 1 +2025-10-24 16:01:06,992 - Stocklogger - WARNING - WARNING: Loan format try times > MAX_TRY_TIMES. Skip as no loan today. +2025-10-24 16:01:06,992 - Stocklogger - INFO - INFO: Agent 30 decide not to loan +2025-10-24 16:01:06,992 - Stocklogger - DEBUG - Wrong json content in response: None +2025-10-24 16:01:06,992 - Stocklogger - DEBUG - Wrong json content in response: 1 +2025-10-24 16:01:06,992 - Stocklogger - DEBUG - Wrong json content in response: 1 +2025-10-24 16:01:06,992 - Stocklogger - DEBUG - Wrong json content in response: 1 +2025-10-24 16:01:06,992 - Stocklogger - WARNING - WARNING: Loan format try times > MAX_TRY_TIMES. Skip as no loan today. +2025-10-24 16:01:06,992 - Stocklogger - INFO - INFO: Agent 31 decide not to loan +2025-10-24 16:01:06,992 - Stocklogger - DEBUG - Wrong json content in response: None +2025-10-24 16:01:06,992 - Stocklogger - DEBUG - Wrong json content in response: 1 +2025-10-24 16:01:06,992 - Stocklogger - DEBUG - Wrong json content in response: 1 +2025-10-24 16:01:06,992 - Stocklogger - DEBUG - Wrong json content in response: 1 +2025-10-24 16:01:06,992 - Stocklogger - WARNING - WARNING: Loan format try times > MAX_TRY_TIMES. Skip as no loan today. +2025-10-24 16:01:06,992 - Stocklogger - INFO - INFO: Agent 32 decide not to loan +2025-10-24 16:01:06,993 - Stocklogger - DEBUG - Wrong json content in response: None +2025-10-24 16:01:06,993 - Stocklogger - DEBUG - Wrong json content in response: 1 +2025-10-24 16:01:06,993 - Stocklogger - DEBUG - Wrong json content in response: 1 +2025-10-24 16:01:06,993 - Stocklogger - DEBUG - Wrong json content in response: 1 +2025-10-24 16:01:06,993 - Stocklogger - WARNING - WARNING: Loan format try times > MAX_TRY_TIMES. Skip as no loan today. +2025-10-24 16:01:06,993 - Stocklogger - INFO - INFO: Agent 33 decide not to loan +2025-10-24 16:01:06,993 - Stocklogger - DEBUG - Wrong json content in response: None +2025-10-24 16:01:06,993 - Stocklogger - DEBUG - Wrong json content in response: 1 +2025-10-24 16:01:06,993 - Stocklogger - DEBUG - Wrong json content in response: 1 +2025-10-24 16:01:06,993 - Stocklogger - DEBUG - Wrong json content in response: 1 +2025-10-24 16:01:06,993 - Stocklogger - WARNING - WARNING: Loan format try times > MAX_TRY_TIMES. Skip as no loan today. +2025-10-24 16:01:06,993 - Stocklogger - INFO - INFO: Agent 34 decide not to loan +2025-10-24 16:01:06,993 - Stocklogger - DEBUG - Wrong json content in response: None +2025-10-24 16:01:06,993 - Stocklogger - DEBUG - Wrong json content in response: 1 +2025-10-24 16:01:06,993 - Stocklogger - DEBUG - Wrong json content in response: 1 +2025-10-24 16:01:06,994 - Stocklogger - DEBUG - Wrong json content in response: 1 +2025-10-24 16:01:06,994 - Stocklogger - WARNING - WARNING: Loan format try times > MAX_TRY_TIMES. Skip as no loan today. +2025-10-24 16:01:06,994 - Stocklogger - INFO - INFO: Agent 35 decide not to loan +2025-10-24 16:01:06,994 - Stocklogger - DEBUG - Wrong json content in response: None +2025-10-24 16:01:06,994 - Stocklogger - DEBUG - Wrong json content in response: 1 +2025-10-24 16:01:06,994 - Stocklogger - DEBUG - Wrong json content in response: 1 +2025-10-24 16:01:06,994 - Stocklogger - DEBUG - Wrong json content in response: 1 +2025-10-24 16:01:06,994 - Stocklogger - WARNING - WARNING: Loan format try times > MAX_TRY_TIMES. Skip as no loan today. +2025-10-24 16:01:06,994 - Stocklogger - INFO - INFO: Agent 36 decide not to loan +2025-10-24 16:01:06,994 - Stocklogger - DEBUG - Wrong json content in response: None +2025-10-24 16:01:06,994 - Stocklogger - DEBUG - Wrong json content in response: 1 +2025-10-24 16:01:06,994 - Stocklogger - DEBUG - Wrong json content in response: 1 +2025-10-24 16:01:06,994 - Stocklogger - DEBUG - Wrong json content in response: 1 +2025-10-24 16:01:06,994 - Stocklogger - WARNING - WARNING: Loan format try times > MAX_TRY_TIMES. Skip as no loan today. +2025-10-24 16:01:06,994 - Stocklogger - INFO - INFO: Agent 37 decide not to loan +2025-10-24 16:01:06,994 - Stocklogger - DEBUG - Wrong json content in response: None +2025-10-24 16:01:06,994 - Stocklogger - DEBUG - Wrong json content in response: 1 +2025-10-24 16:01:06,994 - Stocklogger - DEBUG - Wrong json content in response: 1 +2025-10-24 16:01:06,994 - Stocklogger - DEBUG - Wrong json content in response: 1 +2025-10-24 16:01:06,994 - Stocklogger - WARNING - WARNING: Loan format try times > MAX_TRY_TIMES. Skip as no loan today. +2025-10-24 16:01:06,994 - Stocklogger - INFO - INFO: Agent 38 decide not to loan +2025-10-24 16:01:06,994 - Stocklogger - DEBUG - Wrong json content in response: None +2025-10-24 16:01:06,994 - Stocklogger - DEBUG - Wrong json content in response: 1 +2025-10-24 16:01:06,994 - Stocklogger - DEBUG - Wrong json content in response: 1 +2025-10-24 16:01:06,995 - Stocklogger - DEBUG - Wrong json content in response: 1 +2025-10-24 16:01:06,995 - Stocklogger - WARNING - WARNING: Loan format try times > MAX_TRY_TIMES. Skip as no loan today. +2025-10-24 16:01:06,995 - Stocklogger - INFO - INFO: Agent 39 decide not to loan +2025-10-24 16:01:06,995 - Stocklogger - DEBUG - Wrong json content in response: None +2025-10-24 16:01:06,995 - Stocklogger - DEBUG - Wrong json content in response: 1 +2025-10-24 16:01:06,995 - Stocklogger - DEBUG - Wrong json content in response: 1 +2025-10-24 16:01:06,995 - Stocklogger - DEBUG - Wrong json content in response: 1 +2025-10-24 16:01:06,995 - Stocklogger - WARNING - WARNING: Loan format try times > MAX_TRY_TIMES. Skip as no loan today. +2025-10-24 16:01:06,995 - Stocklogger - INFO - INFO: Agent 40 decide not to loan +2025-10-24 16:01:06,995 - Stocklogger - DEBUG - Wrong json content in response: None +2025-10-24 16:01:06,995 - Stocklogger - DEBUG - Wrong json content in response: 1 +2025-10-24 16:01:06,995 - Stocklogger - DEBUG - Wrong json content in response: 1 +2025-10-24 16:01:06,995 - Stocklogger - DEBUG - Wrong json content in response: 1 +2025-10-24 16:01:06,995 - Stocklogger - WARNING - WARNING: Loan format try times > MAX_TRY_TIMES. Skip as no loan today. +2025-10-24 16:01:06,995 - Stocklogger - INFO - INFO: Agent 41 decide not to loan +2025-10-24 16:01:06,995 - Stocklogger - DEBUG - Wrong json content in response: None +2025-10-24 16:01:06,995 - Stocklogger - DEBUG - Wrong json content in response: 1 +2025-10-24 16:01:06,995 - Stocklogger - DEBUG - Wrong json content in response: 1 +2025-10-24 16:01:06,995 - Stocklogger - DEBUG - Wrong json content in response: 1 +2025-10-24 16:01:06,995 - Stocklogger - WARNING - WARNING: Loan format try times > MAX_TRY_TIMES. Skip as no loan today. +2025-10-24 16:01:06,995 - Stocklogger - INFO - INFO: Agent 42 decide not to loan +2025-10-24 16:01:06,995 - Stocklogger - DEBUG - Wrong json content in response: None +2025-10-24 16:01:06,995 - Stocklogger - DEBUG - Wrong json content in response: 1 +2025-10-24 16:01:06,995 - Stocklogger - DEBUG - Wrong json content in response: 1 +2025-10-24 16:01:06,995 - Stocklogger - DEBUG - Wrong json content in response: 1 +2025-10-24 16:01:06,995 - Stocklogger - WARNING - WARNING: Loan format try times > MAX_TRY_TIMES. Skip as no loan today. +2025-10-24 16:01:06,995 - Stocklogger - INFO - INFO: Agent 43 decide not to loan +2025-10-24 16:01:06,995 - Stocklogger - DEBUG - Wrong json content in response: None +2025-10-24 16:01:06,996 - Stocklogger - DEBUG - Wrong json content in response: 1 +2025-10-24 16:01:06,996 - Stocklogger - DEBUG - Wrong json content in response: 1 +2025-10-24 16:01:06,996 - Stocklogger - DEBUG - Wrong json content in response: 1 +2025-10-24 16:01:06,996 - Stocklogger - WARNING - WARNING: Loan format try times > MAX_TRY_TIMES. Skip as no loan today. +2025-10-24 16:01:06,996 - Stocklogger - INFO - INFO: Agent 44 decide not to loan +2025-10-24 16:01:06,996 - Stocklogger - DEBUG - Wrong json content in response: None +2025-10-24 16:01:06,996 - Stocklogger - DEBUG - Wrong json content in response: 1 +2025-10-24 16:01:06,996 - Stocklogger - DEBUG - Wrong json content in response: 1 +2025-10-24 16:01:06,996 - Stocklogger - DEBUG - Wrong json content in response: 1 +2025-10-24 16:01:06,996 - Stocklogger - WARNING - WARNING: Loan format try times > MAX_TRY_TIMES. Skip as no loan today. +2025-10-24 16:01:06,996 - Stocklogger - INFO - INFO: Agent 45 decide not to loan +2025-10-24 16:01:06,996 - Stocklogger - DEBUG - Wrong json content in response: None +2025-10-24 16:01:06,996 - Stocklogger - DEBUG - Wrong json content in response: 1 +2025-10-24 16:01:06,996 - Stocklogger - DEBUG - Wrong json content in response: 1 +2025-10-24 16:01:06,996 - Stocklogger - DEBUG - Wrong json content in response: 1 +2025-10-24 16:01:06,996 - Stocklogger - WARNING - WARNING: Loan format try times > MAX_TRY_TIMES. Skip as no loan today. +2025-10-24 16:01:06,996 - Stocklogger - INFO - INFO: Agent 46 decide not to loan +2025-10-24 16:01:06,996 - Stocklogger - DEBUG - Wrong json content in response: None +2025-10-24 16:01:06,996 - Stocklogger - DEBUG - Wrong json content in response: 1 +2025-10-24 16:01:06,996 - Stocklogger - DEBUG - Wrong json content in response: 1 +2025-10-24 16:01:06,996 - Stocklogger - DEBUG - Wrong json content in response: 1 +2025-10-24 16:01:06,996 - Stocklogger - WARNING - WARNING: Loan format try times > MAX_TRY_TIMES. Skip as no loan today. +2025-10-24 16:01:06,996 - Stocklogger - INFO - INFO: Agent 47 decide not to loan +2025-10-24 16:01:06,996 - Stocklogger - DEBUG - Wrong json content in response: None +2025-10-24 16:01:06,996 - Stocklogger - DEBUG - Wrong json content in response: 1 +2025-10-24 16:01:06,996 - Stocklogger - DEBUG - Wrong json content in response: 1 +2025-10-24 16:01:06,997 - Stocklogger - DEBUG - Wrong json content in response: 1 +2025-10-24 16:01:06,997 - Stocklogger - WARNING - WARNING: Loan format try times > MAX_TRY_TIMES. Skip as no loan today. +2025-10-24 16:01:06,997 - Stocklogger - INFO - INFO: Agent 48 decide not to loan +2025-10-24 16:01:06,997 - Stocklogger - DEBUG - Wrong json content in response: None +2025-10-24 16:01:06,997 - Stocklogger - DEBUG - Wrong json content in response: 1 +2025-10-24 16:01:06,997 - Stocklogger - DEBUG - Wrong json content in response: 1 +2025-10-24 16:01:06,997 - Stocklogger - DEBUG - Wrong json content in response: 1 +2025-10-24 16:01:06,997 - Stocklogger - WARNING - WARNING: Loan format try times > MAX_TRY_TIMES. Skip as no loan today. +2025-10-24 16:01:06,997 - Stocklogger - INFO - INFO: Agent 49 decide not to loan +2025-10-24 16:01:06,997 - Stocklogger - DEBUG - SESSION 1 +2025-10-24 16:01:06,997 - Stocklogger - DEBUG - Wrong json content in response: None +2025-10-24 16:01:06,997 - Stocklogger - DEBUG - Wrong json content in response: None +2025-10-24 16:01:06,997 - Stocklogger - DEBUG - Wrong json content in response: None +2025-10-24 16:01:06,997 - Stocklogger - DEBUG - Wrong json content in response: None +2025-10-24 16:01:06,997 - Stocklogger - WARNING - WARNING: Action format try times > MAX_TRY_TIMES. Skip as no loan today. +2025-10-24 16:01:06,997 - Stocklogger - INFO - INFO: Agent 5 decide not to action +2025-10-24 16:01:46,078 - Stocklogger - DEBUG - Agents initial... +2025-10-24 16:01:46,078 - Stocklogger - DEBUG - cash: 373650.6281840257, stock a: 8562, stock b:71974, debt: [{'loan': 'yes', 'amount': 393379.9966567142, 'loan_type': 2, 'repayment_date': 176}] +2025-10-24 16:01:46,079 - Stocklogger - DEBUG - cash: 241402.44972598867, stock a: 2869, stock b:110330, debt: [{'loan': 'yes', 'amount': 3587919.5666463315, 'loan_type': 0, 'repayment_date': 198}] +2025-10-24 16:01:46,079 - Stocklogger - DEBUG - cash: 2355978.2095771716, stock a: 29239, stock b:41406, debt: [{'loan': 'yes', 'amount': 806953.6833873591, 'loan_type': 1, 'repayment_date': 132}] +2025-10-24 16:01:46,079 - Stocklogger - DEBUG - cash: 246592.78559864327, stock a: 97837, stock b:29542, debt: [{'loan': 'yes', 'amount': 2612292.006069916, 'loan_type': 2, 'repayment_date': 88}] +2025-10-24 16:01:46,079 - Stocklogger - DEBUG - cash: 2743272.1613059584, stock a: 33810, stock b:8065, debt: [{'loan': 'yes', 'amount': 3886437.10824315, 'loan_type': 1, 'repayment_date': 88}] +2025-10-24 16:01:46,079 - Stocklogger - DEBUG - cash: 1896682.917764469, stock a: 16387, stock b:51514, debt: [{'loan': 'yes', 'amount': 4278859.69823394, 'loan_type': 1, 'repayment_date': 88}] +2025-10-24 16:01:46,079 - Stocklogger - DEBUG - cash: 1607463.1981898812, stock a: 30315, stock b:47886, debt: [{'loan': 'yes', 'amount': 3279629.375595889, 'loan_type': 2, 'repayment_date': 242}] +2025-10-24 16:01:46,079 - Stocklogger - DEBUG - cash: 1185526.2911452674, stock a: 9988, stock b:66106, debt: [{'loan': 'yes', 'amount': 2186251.4824740347, 'loan_type': 2, 'repayment_date': 88}] +2025-10-24 16:01:46,079 - Stocklogger - DEBUG - cash: 784677.7632543045, stock a: 1679, stock b:59223, debt: [{'loan': 'yes', 'amount': 2056317.1522175323, 'loan_type': 1, 'repayment_date': 44}] +2025-10-24 16:01:46,079 - Stocklogger - DEBUG - cash: 1420985.357521466, stock a: 51365, stock b:43730, debt: [{'loan': 'yes', 'amount': 3553084.5086503415, 'loan_type': 1, 'repayment_date': 132}] +2025-10-24 16:01:46,079 - Stocklogger - DEBUG - cash: 513924.2446360565, stock a: 20520, stock b:63188, debt: [{'loan': 'yes', 'amount': 2115963.277558194, 'loan_type': 2, 'repayment_date': 132}] +2025-10-24 16:01:46,080 - Stocklogger - DEBUG - cash: 297805.97994902846, stock a: 6712, stock b:23504, debt: [{'loan': 'yes', 'amount': 130405.48311251732, 'loan_type': 2, 'repayment_date': 176}] +2025-10-24 16:01:46,080 - Stocklogger - DEBUG - cash: 1462754.2470701349, stock a: 20512, stock b:62773, debt: [{'loan': 'yes', 'amount': 2640215.4378540637, 'loan_type': 2, 'repayment_date': 264}] +2025-10-24 16:01:46,080 - Stocklogger - DEBUG - cash: 2625570.59952009, stock a: 31177, stock b:10708, debt: [{'loan': 'yes', 'amount': 2741625.4466739264, 'loan_type': 0, 'repayment_date': 22}] +2025-10-24 16:01:46,080 - Stocklogger - DEBUG - cash: 517183.4801187919, stock a: 80087, stock b:20175, debt: [{'loan': 'yes', 'amount': 2311587.7743840874, 'loan_type': 2, 'repayment_date': 88}] +2025-10-24 16:01:46,080 - Stocklogger - DEBUG - cash: 190820.47460838358, stock a: 50233, stock b:52773, debt: [{'loan': 'yes', 'amount': 1153712.419697656, 'loan_type': 1, 'repayment_date': 110}] +2025-10-24 16:01:46,080 - Stocklogger - DEBUG - cash: 458274.1077175917, stock a: 88567, stock b:20792, debt: [{'loan': 'yes', 'amount': 537239.8099977027, 'loan_type': 0, 'repayment_date': 88}] +2025-10-24 16:01:46,080 - Stocklogger - DEBUG - cash: 651025.957736654, stock a: 13229, stock b:83028, debt: [{'loan': 'yes', 'amount': 2106012.2692034873, 'loan_type': 2, 'repayment_date': 220}] +2025-10-24 16:01:46,080 - Stocklogger - DEBUG - cash: 895104.0363725676, stock a: 48226, stock b:7271, debt: [{'loan': 'yes', 'amount': 1801430.3294217826, 'loan_type': 2, 'repayment_date': 242}] +2025-10-24 16:01:46,080 - Stocklogger - DEBUG - cash: 41151.84400920902, stock a: 114636, stock b:26168, debt: [{'loan': 'yes', 'amount': 322959.3719096091, 'loan_type': 1, 'repayment_date': 198}] +2025-10-24 16:01:46,080 - Stocklogger - DEBUG - cash: 245976.44548938258, stock a: 25006, stock b:65069, debt: [{'loan': 'yes', 'amount': 785755.347935212, 'loan_type': 1, 'repayment_date': 176}] +2025-10-24 16:01:46,081 - Stocklogger - DEBUG - cash: 3924098.578536076, stock a: 6529, stock b:20883, debt: [{'loan': 'yes', 'amount': 1199185.2862560577, 'loan_type': 0, 'repayment_date': 242}] +2025-10-24 16:01:46,081 - Stocklogger - DEBUG - cash: 1864032.5792950368, stock a: 67494, stock b:16708, debt: [{'loan': 'yes', 'amount': 2160161.152670085, 'loan_type': 0, 'repayment_date': 176}] +2025-10-24 16:01:46,081 - Stocklogger - DEBUG - cash: 3645132.4369381582, stock a: 26415, stock b:3443, debt: [{'loan': 'yes', 'amount': 2705694.326796012, 'loan_type': 1, 'repayment_date': 88}] +2025-10-24 16:01:46,081 - Stocklogger - DEBUG - cash: 540035.6069702733, stock a: 56663, stock b:39453, debt: [{'loan': 'yes', 'amount': 2591537.9538860195, 'loan_type': 2, 'repayment_date': 66}] +2025-10-24 16:01:46,081 - Stocklogger - DEBUG - cash: 1247005.5196681258, stock a: 88440, stock b:13641, debt: [{'loan': 'yes', 'amount': 2168173.634752199, 'loan_type': 1, 'repayment_date': 242}] +2025-10-24 16:01:46,081 - Stocklogger - DEBUG - cash: 3152942.8976946585, stock a: 38498, stock b:1743, debt: [{'loan': 'yes', 'amount': 1823331.5094606855, 'loan_type': 1, 'repayment_date': 88}] +2025-10-24 16:01:46,081 - Stocklogger - DEBUG - cash: 1812938.17082467, stock a: 8491, stock b:22188, debt: [{'loan': 'yes', 'amount': 514710.7883921204, 'loan_type': 1, 'repayment_date': 264}] +2025-10-24 16:01:46,081 - Stocklogger - DEBUG - cash: 1077479.5470072434, stock a: 45312, stock b:41761, debt: [{'loan': 'yes', 'amount': 754350.1795840418, 'loan_type': 2, 'repayment_date': 88}] +2025-10-24 16:01:46,081 - Stocklogger - DEBUG - cash: 655819.3409927532, stock a: 71240, stock b:27173, debt: [{'loan': 'yes', 'amount': 3846976.516876182, 'loan_type': 0, 'repayment_date': 22}] +2025-10-24 16:01:46,081 - Stocklogger - DEBUG - cash: 1155203.7479417755, stock a: 3481, stock b:65249, debt: [{'loan': 'yes', 'amount': 190679.67763577332, 'loan_type': 1, 'repayment_date': 242}] +2025-10-24 16:01:46,082 - Stocklogger - DEBUG - cash: 7696.716478574616, stock a: 97231, stock b:14333, debt: [{'loan': 'yes', 'amount': 2630983.3009143453, 'loan_type': 0, 'repayment_date': 176}] +2025-10-24 16:01:46,082 - Stocklogger - DEBUG - cash: 1688688.400554284, stock a: 25026, stock b:53274, debt: [{'loan': 'yes', 'amount': 2382144.8613954936, 'loan_type': 1, 'repayment_date': 242}] +2025-10-24 16:01:46,082 - Stocklogger - DEBUG - cash: 405337.29915068706, stock a: 9997, stock b:61975, debt: [{'loan': 'yes', 'amount': 401645.5473985636, 'loan_type': 0, 'repayment_date': 154}] +2025-10-24 16:01:46,082 - Stocklogger - DEBUG - cash: 1184408.963595549, stock a: 64776, stock b:3214, debt: [{'loan': 'yes', 'amount': 1833516.72604896, 'loan_type': 0, 'repayment_date': 264}] +2025-10-24 16:01:46,082 - Stocklogger - DEBUG - cash: 229325.8038933721, stock a: 49082, stock b:52182, debt: [{'loan': 'yes', 'amount': 3060570.751422938, 'loan_type': 2, 'repayment_date': 22}] +2025-10-24 16:01:46,082 - Stocklogger - DEBUG - cash: 388334.3377403364, stock a: 130837, stock b:4591, debt: [{'loan': 'yes', 'amount': 1628491.3062600852, 'loan_type': 0, 'repayment_date': 198}] +2025-10-24 16:01:46,082 - Stocklogger - DEBUG - cash: 2157130.6616506064, stock a: 17795, stock b:56858, debt: [{'loan': 'yes', 'amount': 3421268.9239088586, 'loan_type': 0, 'repayment_date': 264}] +2025-10-24 16:01:46,082 - Stocklogger - DEBUG - cash: 1667411.1280422283, stock a: 43965, stock b:36124, debt: [{'loan': 'yes', 'amount': 1144734.0866081724, 'loan_type': 0, 'repayment_date': 110}] +2025-10-24 16:01:46,082 - Stocklogger - DEBUG - cash: 1807954.2023783473, stock a: 47426, stock b:25492, debt: [{'loan': 'yes', 'amount': 2943916.2474611197, 'loan_type': 2, 'repayment_date': 264}] +2025-10-24 16:01:46,082 - Stocklogger - DEBUG - cash: 1611257.9796971087, stock a: 97258, stock b:2498, debt: [{'loan': 'yes', 'amount': 1560132.5133121153, 'loan_type': 1, 'repayment_date': 176}] +2025-10-24 16:01:46,082 - Stocklogger - DEBUG - cash: 818036.7727530879, stock a: 60881, stock b:50743, debt: [{'loan': 'yes', 'amount': 2097181.035490842, 'loan_type': 1, 'repayment_date': 242}] +2025-10-24 16:01:46,083 - Stocklogger - DEBUG - cash: 86543.98272122999, stock a: 157041, stock b:871, debt: [{'loan': 'yes', 'amount': 173851.19564670813, 'loan_type': 1, 'repayment_date': 154}] +2025-10-24 16:01:46,083 - Stocklogger - DEBUG - cash: 1203702.8050833654, stock a: 65968, stock b:20781, debt: [{'loan': 'yes', 'amount': 2543327.978930707, 'loan_type': 0, 'repayment_date': 132}] +2025-10-24 16:01:46,083 - Stocklogger - DEBUG - cash: 907855.0306006272, stock a: 82599, stock b:10086, debt: [{'loan': 'yes', 'amount': 709983.316017187, 'loan_type': 0, 'repayment_date': 66}] +2025-10-24 16:01:46,083 - Stocklogger - DEBUG - cash: 1720741.707021373, stock a: 49222, stock b:33240, debt: [{'loan': 'yes', 'amount': 4187925.8575181155, 'loan_type': 2, 'repayment_date': 242}] +2025-10-24 16:01:46,083 - Stocklogger - DEBUG - cash: 274016.05641823314, stock a: 108771, stock b:5054, debt: [{'loan': 'yes', 'amount': 94739.60824852635, 'loan_type': 1, 'repayment_date': 66}] +2025-10-24 16:01:46,083 - Stocklogger - DEBUG - cash: 242426.06841314962, stock a: 85304, stock b:26285, debt: [{'loan': 'yes', 'amount': 1215167.1959775372, 'loan_type': 1, 'repayment_date': 132}] +2025-10-24 16:01:46,083 - Stocklogger - DEBUG - cash: 508525.1820933023, stock a: 109844, stock b:13694, debt: [{'loan': 'yes', 'amount': 1303598.163459006, 'loan_type': 0, 'repayment_date': 66}] +2025-10-24 16:01:46,083 - Stocklogger - DEBUG - cash: 2426292.2688790876, stock a: 2712, stock b:44215, debt: [{'loan': 'yes', 'amount': 2326498.794975514, 'loan_type': 1, 'repayment_date': 66}] +2025-10-24 16:01:46,083 - Stocklogger - DEBUG - --------Simulation Start!-------- +2025-10-24 16:01:46,083 - Stocklogger - DEBUG - --------DAY 1--------- +2025-10-24 16:02:13,866 - Stocklogger - DEBUG - Agents initial... +2025-10-24 16:02:13,866 - Stocklogger - DEBUG - cash: 3241305.7616430353, stock a: 9505, stock b:21166, debt: [{'loan': 'yes', 'amount': 2560310.425855771, 'loan_type': 2, 'repayment_date': 44}] +2025-10-24 16:02:13,867 - Stocklogger - DEBUG - cash: 2832435.2892016787, stock a: 969, stock b:14202, debt: [{'loan': 'yes', 'amount': 787973.9544929742, 'loan_type': 2, 'repayment_date': 242}] +2025-10-24 16:02:13,867 - Stocklogger - DEBUG - cash: 369326.11648743117, stock a: 56731, stock b:52949, debt: [{'loan': 'yes', 'amount': 3154064.7257214542, 'loan_type': 2, 'repayment_date': 132}] +2025-10-24 16:02:13,867 - Stocklogger - DEBUG - cash: 544110.1880215015, stock a: 42806, stock b:73956, debt: [{'loan': 'yes', 'amount': 498847.4966436812, 'loan_type': 0, 'repayment_date': 220}] +2025-10-24 16:02:13,867 - Stocklogger - DEBUG - cash: 134498.0148643371, stock a: 48103, stock b:10335, debt: [{'loan': 'yes', 'amount': 754796.2517106105, 'loan_type': 0, 'repayment_date': 264}] +2025-10-24 16:02:13,867 - Stocklogger - DEBUG - cash: 1150947.6745369968, stock a: 37448, stock b:66118, debt: [{'loan': 'yes', 'amount': 132231.59432673547, 'loan_type': 0, 'repayment_date': 66}] +2025-10-24 16:02:13,867 - Stocklogger - DEBUG - cash: 600844.274826663, stock a: 56508, stock b:48434, debt: [{'loan': 'yes', 'amount': 1451109.021552235, 'loan_type': 0, 'repayment_date': 132}] +2025-10-24 16:02:13,867 - Stocklogger - DEBUG - cash: 545933.8472576119, stock a: 64394, stock b:21647, debt: [{'loan': 'yes', 'amount': 2507069.4226144957, 'loan_type': 0, 'repayment_date': 154}] +2025-10-24 16:02:13,867 - Stocklogger - DEBUG - cash: 330349.2237047201, stock a: 77106, stock b:46885, debt: [{'loan': 'yes', 'amount': 2185456.9156079497, 'loan_type': 1, 'repayment_date': 176}] +2025-10-24 16:02:13,867 - Stocklogger - DEBUG - cash: 377771.32687154156, stock a: 107843, stock b:17112, debt: [{'loan': 'yes', 'amount': 1936674.2232387152, 'loan_type': 0, 'repayment_date': 110}] +2025-10-24 16:02:13,867 - Stocklogger - DEBUG - cash: 2610105.132765684, stock a: 20291, stock b:16206, debt: [{'loan': 'yes', 'amount': 3759936.84563272, 'loan_type': 0, 'repayment_date': 264}] +2025-10-24 16:02:13,867 - Stocklogger - DEBUG - cash: 287135.13996835524, stock a: 5158, stock b:49490, debt: [{'loan': 'yes', 'amount': 1234582.4198058592, 'loan_type': 2, 'repayment_date': 44}] +2025-10-24 16:02:13,868 - Stocklogger - DEBUG - cash: 833139.0953374524, stock a: 25751, stock b:23866, debt: [{'loan': 'yes', 'amount': 413344.198761752, 'loan_type': 2, 'repayment_date': 132}] +2025-10-24 16:02:13,868 - Stocklogger - DEBUG - cash: 121342.20616192515, stock a: 94769, stock b:15066, debt: [{'loan': 'yes', 'amount': 1474833.2386400036, 'loan_type': 2, 'repayment_date': 264}] +2025-10-24 16:02:13,868 - Stocklogger - DEBUG - cash: 1446396.6195487422, stock a: 29283, stock b:58741, debt: [{'loan': 'yes', 'amount': 3107495.3802328566, 'loan_type': 1, 'repayment_date': 220}] +2025-10-24 16:02:13,868 - Stocklogger - DEBUG - cash: 1432058.1076891397, stock a: 14382, stock b:41527, debt: [{'loan': 'yes', 'amount': 1117479.5908417378, 'loan_type': 2, 'repayment_date': 198}] +2025-10-24 16:02:13,868 - Stocklogger - DEBUG - cash: 1436571.930304665, stock a: 45005, stock b:28299, debt: [{'loan': 'yes', 'amount': 2265584.205095444, 'loan_type': 1, 'repayment_date': 66}] +2025-10-24 16:02:13,868 - Stocklogger - DEBUG - cash: 3662788.688405949, stock a: 29751, stock b:1498, debt: [{'loan': 'yes', 'amount': 2563099.017452964, 'loan_type': 1, 'repayment_date': 22}] +2025-10-24 16:02:13,868 - Stocklogger - DEBUG - cash: 2693811.0947996466, stock a: 4659, stock b:39164, debt: [{'loan': 'yes', 'amount': 1496645.6781074516, 'loan_type': 0, 'repayment_date': 66}] +2025-10-24 16:02:13,868 - Stocklogger - DEBUG - cash: 434867.0868696569, stock a: 27890, stock b:48698, debt: [{'loan': 'yes', 'amount': 701061.4565175988, 'loan_type': 0, 'repayment_date': 198}] +2025-10-24 16:02:13,868 - Stocklogger - DEBUG - cash: 861586.3225097787, stock a: 28071, stock b:47293, debt: [{'loan': 'yes', 'amount': 1824806.107061645, 'loan_type': 0, 'repayment_date': 198}] +2025-10-24 16:02:13,869 - Stocklogger - DEBUG - cash: 2603623.1442432995, stock a: 7282, stock b:5029, debt: [{'loan': 'yes', 'amount': 2320412.881864917, 'loan_type': 2, 'repayment_date': 154}] +2025-10-24 16:02:13,869 - Stocklogger - DEBUG - cash: 2081160.7587537218, stock a: 27372, stock b:31377, debt: [{'loan': 'yes', 'amount': 3885646.198576778, 'loan_type': 1, 'repayment_date': 110}] +2025-10-24 16:02:13,869 - Stocklogger - DEBUG - cash: 1599498.030754007, stock a: 44029, stock b:10527, debt: [{'loan': 'yes', 'amount': 3132463.781313026, 'loan_type': 2, 'repayment_date': 176}] +2025-10-24 16:02:13,869 - Stocklogger - DEBUG - cash: 303947.01774782874, stock a: 53987, stock b:41692, debt: [{'loan': 'yes', 'amount': 2916182.758338165, 'loan_type': 0, 'repayment_date': 44}] +2025-10-24 16:02:13,869 - Stocklogger - DEBUG - cash: 598190.598716124, stock a: 104104, stock b:30659, debt: [{'loan': 'yes', 'amount': 323036.5873980262, 'loan_type': 2, 'repayment_date': 242}] +2025-10-24 16:02:13,869 - Stocklogger - DEBUG - cash: 1765694.6073116625, stock a: 13988, stock b:70172, debt: [{'loan': 'yes', 'amount': 1400543.6569251996, 'loan_type': 1, 'repayment_date': 110}] +2025-10-24 16:02:13,869 - Stocklogger - DEBUG - cash: 3817802.1354195736, stock a: 11774, stock b:4187, debt: [{'loan': 'yes', 'amount': 4221794.728295664, 'loan_type': 2, 'repayment_date': 198}] +2025-10-24 16:02:13,869 - Stocklogger - DEBUG - cash: 329098.90992620203, stock a: 116564, stock b:11014, debt: [{'loan': 'yes', 'amount': 3028541.5386535204, 'loan_type': 1, 'repayment_date': 22}] +2025-10-24 16:02:13,869 - Stocklogger - DEBUG - cash: 1046989.0246972502, stock a: 16512, stock b:26621, debt: [{'loan': 'yes', 'amount': 2554076.011651375, 'loan_type': 1, 'repayment_date': 220}] +2025-10-24 16:02:13,869 - Stocklogger - DEBUG - cash: 938785.6325690935, stock a: 67061, stock b:23498, debt: [{'loan': 'yes', 'amount': 3379907.9567053723, 'loan_type': 2, 'repayment_date': 220}] +2025-10-24 16:02:13,869 - Stocklogger - DEBUG - cash: 2920112.95544967, stock a: 6186, stock b:352, debt: [{'loan': 'yes', 'amount': 2617331.743129435, 'loan_type': 1, 'repayment_date': 44}] +2025-10-24 16:02:13,870 - Stocklogger - DEBUG - cash: 2713728.9603891624, stock a: 58745, stock b:1506, debt: [{'loan': 'yes', 'amount': 2228674.5291780718, 'loan_type': 0, 'repayment_date': 154}] +2025-10-24 16:02:13,870 - Stocklogger - DEBUG - cash: 711018.7283961389, stock a: 47272, stock b:41119, debt: [{'loan': 'yes', 'amount': 688500.9037699802, 'loan_type': 1, 'repayment_date': 44}] +2025-10-24 16:02:13,870 - Stocklogger - DEBUG - cash: 1915227.3097308436, stock a: 3424, stock b:43453, debt: [{'loan': 'yes', 'amount': 1935710.0562703505, 'loan_type': 0, 'repayment_date': 132}] +2025-10-24 16:02:13,870 - Stocklogger - DEBUG - cash: 2668296.1877344614, stock a: 12378, stock b:34580, debt: [{'loan': 'yes', 'amount': 3108217.102113971, 'loan_type': 0, 'repayment_date': 198}] +2025-10-24 16:02:13,870 - Stocklogger - DEBUG - cash: 2969977.1524676722, stock a: 2612, stock b:33796, debt: [{'loan': 'yes', 'amount': 2556158.527789055, 'loan_type': 2, 'repayment_date': 22}] +2025-10-24 16:02:13,870 - Stocklogger - DEBUG - cash: 1057705.08316123, stock a: 93318, stock b:11624, debt: [{'loan': 'yes', 'amount': 2097374.8974524685, 'loan_type': 1, 'repayment_date': 220}] +2025-10-24 16:02:13,870 - Stocklogger - DEBUG - cash: 885489.8136799483, stock a: 76996, stock b:37849, debt: [{'loan': 'yes', 'amount': 4585834.620929422, 'loan_type': 0, 'repayment_date': 22}] +2025-10-24 16:02:13,870 - Stocklogger - DEBUG - cash: 2518385.733625818, stock a: 11019, stock b:11045, debt: [{'loan': 'yes', 'amount': 2595546.188525732, 'loan_type': 1, 'repayment_date': 154}] +2025-10-24 16:02:13,870 - Stocklogger - DEBUG - cash: 2197788.6181010846, stock a: 14825, stock b:489, debt: [{'loan': 'yes', 'amount': 688034.6094768886, 'loan_type': 2, 'repayment_date': 132}] +2025-10-24 16:02:13,870 - Stocklogger - DEBUG - cash: 762744.8267264786, stock a: 20719, stock b:57951, debt: [{'loan': 'yes', 'amount': 3144850.145011087, 'loan_type': 1, 'repayment_date': 44}] +2025-10-24 16:02:13,870 - Stocklogger - DEBUG - cash: 1419496.4962941925, stock a: 18299, stock b:57811, debt: [{'loan': 'yes', 'amount': 350051.6942597548, 'loan_type': 1, 'repayment_date': 22}] +2025-10-24 16:02:13,871 - Stocklogger - DEBUG - cash: 1005449.5745970849, stock a: 56671, stock b:31214, debt: [{'loan': 'yes', 'amount': 127641.33374126186, 'loan_type': 0, 'repayment_date': 242}] +2025-10-24 16:02:13,871 - Stocklogger - DEBUG - cash: 1023901.1513987512, stock a: 12978, stock b:88138, debt: [{'loan': 'yes', 'amount': 2779779.511352364, 'loan_type': 1, 'repayment_date': 264}] +2025-10-24 16:02:13,871 - Stocklogger - DEBUG - cash: 441186.63829025015, stock a: 44502, stock b:76726, debt: [{'loan': 'yes', 'amount': 3770768.6189312343, 'loan_type': 2, 'repayment_date': 22}] +2025-10-24 16:02:13,871 - Stocklogger - DEBUG - cash: 1171282.3210386364, stock a: 51095, stock b:44668, debt: [{'loan': 'yes', 'amount': 2872798.073698214, 'loan_type': 1, 'repayment_date': 88}] +2025-10-24 16:02:13,871 - Stocklogger - DEBUG - cash: 407827.99805787863, stock a: 50792, stock b:52348, debt: [{'loan': 'yes', 'amount': 2584397.3155022394, 'loan_type': 2, 'repayment_date': 66}] +2025-10-24 16:02:13,871 - Stocklogger - DEBUG - cash: 362215.28933724045, stock a: 63540, stock b:37169, debt: [{'loan': 'yes', 'amount': 697897.4852055626, 'loan_type': 1, 'repayment_date': 88}] +2025-10-24 16:02:13,871 - Stocklogger - DEBUG - cash: 1951277.6130588055, stock a: 25507, stock b:29626, debt: [{'loan': 'yes', 'amount': 3422287.040756295, 'loan_type': 2, 'repayment_date': 154}] +2025-10-24 16:02:13,871 - Stocklogger - DEBUG - --------Simulation Start!-------- +2025-10-24 16:02:13,871 - Stocklogger - DEBUG - --------DAY 1--------- +2025-10-24 16:02:39,741 - Stocklogger - DEBUG - Agents initial... +2025-10-24 16:02:39,741 - Stocklogger - DEBUG - cash: 1928822.4034324202, stock a: 11112, stock b:1789, debt: [{'loan': 'yes', 'amount': 1353555.2123407258, 'loan_type': 0, 'repayment_date': 88}] +2025-10-24 16:02:39,741 - Stocklogger - DEBUG - cash: 361845.6118707353, stock a: 35895, stock b:85230, debt: [{'loan': 'yes', 'amount': 2314215.76970614, 'loan_type': 2, 'repayment_date': 110}] +2025-10-24 16:02:39,741 - Stocklogger - DEBUG - cash: 733945.2243413575, stock a: 62571, stock b:21572, debt: [{'loan': 'yes', 'amount': 2731073.4008336114, 'loan_type': 1, 'repayment_date': 242}] +2025-10-24 16:02:39,741 - Stocklogger - DEBUG - cash: 1231297.450667259, stock a: 62519, stock b:11515, debt: [{'loan': 'yes', 'amount': 690314.8154552163, 'loan_type': 0, 'repayment_date': 154}] +2025-10-24 16:02:39,741 - Stocklogger - DEBUG - cash: 2477513.3729000464, stock a: 56062, stock b:12956, debt: [{'loan': 'yes', 'amount': 3836402.6562854773, 'loan_type': 0, 'repayment_date': 44}] +2025-10-24 16:02:39,741 - Stocklogger - DEBUG - cash: 1402563.9691401836, stock a: 20334, stock b:14175, debt: [{'loan': 'yes', 'amount': 2224444.169235126, 'loan_type': 2, 'repayment_date': 176}] +2025-10-24 16:02:39,742 - Stocklogger - DEBUG - cash: 1232922.842345564, stock a: 29165, stock b:55507, debt: [{'loan': 'yes', 'amount': 2767306.876232301, 'loan_type': 0, 'repayment_date': 66}] +2025-10-24 16:02:39,742 - Stocklogger - DEBUG - cash: 236880.6695627973, stock a: 46175, stock b:55737, debt: [{'loan': 'yes', 'amount': 562447.8760959578, 'loan_type': 2, 'repayment_date': 110}] +2025-10-24 16:02:39,742 - Stocklogger - DEBUG - cash: 3071607.2950842693, stock a: 5533, stock b:28193, debt: [{'loan': 'yes', 'amount': 470845.1447814238, 'loan_type': 2, 'repayment_date': 264}] +2025-10-24 16:02:39,742 - Stocklogger - DEBUG - cash: 862672.8037234566, stock a: 53466, stock b:52019, debt: [{'loan': 'yes', 'amount': 136308.27773426447, 'loan_type': 0, 'repayment_date': 264}] +2025-10-24 16:02:39,742 - Stocklogger - DEBUG - cash: 3708889.9008733523, stock a: 395, stock b:21041, debt: [{'loan': 'yes', 'amount': 4347568.40526109, 'loan_type': 1, 'repayment_date': 264}] +2025-10-24 16:02:39,742 - Stocklogger - DEBUG - cash: 1589149.391092387, stock a: 37326, stock b:43040, debt: [{'loan': 'yes', 'amount': 2352962.830493251, 'loan_type': 2, 'repayment_date': 154}] +2025-10-24 16:02:39,742 - Stocklogger - DEBUG - cash: 1637881.046243876, stock a: 79417, stock b:1850, debt: [{'loan': 'yes', 'amount': 1951959.3733816599, 'loan_type': 1, 'repayment_date': 88}] +2025-10-24 16:02:39,742 - Stocklogger - DEBUG - cash: 942378.2608059334, stock a: 25213, stock b:32181, debt: [{'loan': 'yes', 'amount': 1188278.8555647694, 'loan_type': 0, 'repayment_date': 22}] +2025-10-24 16:02:39,742 - Stocklogger - DEBUG - cash: 669747.544081728, stock a: 130328, stock b:8912, debt: [{'loan': 'yes', 'amount': 1156979.413412216, 'loan_type': 0, 'repayment_date': 242}] +2025-10-24 16:02:39,742 - Stocklogger - DEBUG - cash: 314951.6772433331, stock a: 9026, stock b:107266, debt: [{'loan': 'yes', 'amount': 1574649.0517644223, 'loan_type': 0, 'repayment_date': 22}] +2025-10-24 16:02:39,742 - Stocklogger - DEBUG - cash: 1393216.0281814076, stock a: 20109, stock b:67953, debt: [{'loan': 'yes', 'amount': 2660009.299148536, 'loan_type': 1, 'repayment_date': 198}] +2025-10-24 16:02:39,742 - Stocklogger - DEBUG - cash: 417877.8763099217, stock a: 53308, stock b:70509, debt: [{'loan': 'yes', 'amount': 4453276.269655228, 'loan_type': 0, 'repayment_date': 44}] +2025-10-24 16:02:39,743 - Stocklogger - DEBUG - cash: 3396.7675047863468, stock a: 57566, stock b:64806, debt: [{'loan': 'yes', 'amount': 2679852.7302118903, 'loan_type': 0, 'repayment_date': 220}] +2025-10-24 16:02:39,743 - Stocklogger - DEBUG - cash: 1876539.388673526, stock a: 68904, stock b:15143, debt: [{'loan': 'yes', 'amount': 1702518.6056482494, 'loan_type': 2, 'repayment_date': 66}] +2025-10-24 16:02:39,743 - Stocklogger - DEBUG - cash: 2809379.539712329, stock a: 11579, stock b:36992, debt: [{'loan': 'yes', 'amount': 4236393.538161026, 'loan_type': 0, 'repayment_date': 22}] +2025-10-24 16:02:39,743 - Stocklogger - DEBUG - cash: 1586015.006365451, stock a: 4467, stock b:33863, debt: [{'loan': 'yes', 'amount': 2484411.831169366, 'loan_type': 1, 'repayment_date': 66}] +2025-10-24 16:02:39,743 - Stocklogger - DEBUG - cash: 1338037.0327151692, stock a: 76488, stock b:18417, debt: [{'loan': 'yes', 'amount': 3408258.4264357626, 'loan_type': 0, 'repayment_date': 154}] +2025-10-24 16:02:39,743 - Stocklogger - DEBUG - cash: 332244.1627785383, stock a: 94099, stock b:35292, debt: [{'loan': 'yes', 'amount': 3016161.589419675, 'loan_type': 1, 'repayment_date': 44}] +2025-10-24 16:02:39,743 - Stocklogger - DEBUG - cash: 2285.2583999560807, stock a: 13620, stock b:67694, debt: [{'loan': 'yes', 'amount': 1763823.0358459062, 'loan_type': 0, 'repayment_date': 110}] +2025-10-24 16:02:39,743 - Stocklogger - DEBUG - cash: 2096259.919945081, stock a: 30124, stock b:39428, debt: [{'loan': 'yes', 'amount': 1054117.2070037352, 'loan_type': 1, 'repayment_date': 264}] +2025-10-24 16:02:39,743 - Stocklogger - DEBUG - cash: 3376873.5451470274, stock a: 33424, stock b:3071, debt: [{'loan': 'yes', 'amount': 2157329.0940211066, 'loan_type': 1, 'repayment_date': 242}] +2025-10-24 16:02:39,744 - Stocklogger - DEBUG - cash: 1263239.592453464, stock a: 21265, stock b:64735, debt: [{'loan': 'yes', 'amount': 423557.1320892023, 'loan_type': 0, 'repayment_date': 88}] +2025-10-24 16:02:39,744 - Stocklogger - DEBUG - cash: 442394.47514923924, stock a: 79419, stock b:19099, debt: [{'loan': 'yes', 'amount': 753041.2407047588, 'loan_type': 1, 'repayment_date': 110}] +2025-10-24 16:02:39,744 - Stocklogger - DEBUG - cash: 599404.5701718265, stock a: 89854, stock b:30588, debt: [{'loan': 'yes', 'amount': 416973.7758336245, 'loan_type': 1, 'repayment_date': 88}] +2025-10-24 16:02:39,744 - Stocklogger - DEBUG - cash: 47462.13982629477, stock a: 66100, stock b:71992, debt: [{'loan': 'yes', 'amount': 3801828.2665146706, 'loan_type': 0, 'repayment_date': 154}] +2025-10-24 16:02:39,744 - Stocklogger - DEBUG - cash: 2757536.7475505746, stock a: 17239, stock b:34942, debt: [{'loan': 'yes', 'amount': 4187268.990301033, 'loan_type': 1, 'repayment_date': 88}] +2025-10-24 16:02:39,744 - Stocklogger - DEBUG - cash: 2305475.441287667, stock a: 21188, stock b:46737, debt: [{'loan': 'yes', 'amount': 2739935.8354435484, 'loan_type': 0, 'repayment_date': 154}] +2025-10-24 16:02:39,744 - Stocklogger - DEBUG - cash: 3106332.6122098058, stock a: 50833, stock b:3993, debt: [{'loan': 'yes', 'amount': 456931.0919926589, 'loan_type': 0, 'repayment_date': 264}] +2025-10-24 16:02:39,744 - Stocklogger - DEBUG - cash: 759394.5342260561, stock a: 62891, stock b:37010, debt: [{'loan': 'yes', 'amount': 436736.7129044014, 'loan_type': 1, 'repayment_date': 198}] +2025-10-24 16:02:39,744 - Stocklogger - DEBUG - cash: 189342.83649883143, stock a: 94071, stock b:46533, debt: [{'loan': 'yes', 'amount': 2909925.5973625323, 'loan_type': 2, 'repayment_date': 66}] +2025-10-24 16:02:39,744 - Stocklogger - DEBUG - cash: 2074627.1024490504, stock a: 52798, stock b:1360, debt: [{'loan': 'yes', 'amount': 3214281.8753098696, 'loan_type': 1, 'repayment_date': 220}] +2025-10-24 16:02:39,745 - Stocklogger - DEBUG - cash: 1399110.5071865073, stock a: 5175, stock b:27125, debt: [{'loan': 'yes', 'amount': 1269482.6829855805, 'loan_type': 2, 'repayment_date': 264}] +2025-10-24 16:02:39,745 - Stocklogger - DEBUG - cash: 303905.10620089964, stock a: 91376, stock b:18106, debt: [{'loan': 'yes', 'amount': 2795740.8869800502, 'loan_type': 0, 'repayment_date': 88}] +2025-10-24 16:02:39,745 - Stocklogger - DEBUG - cash: 3097740.766596114, stock a: 13325, stock b:21589, debt: [{'loan': 'yes', 'amount': 3749074.919745185, 'loan_type': 1, 'repayment_date': 220}] +2025-10-24 16:02:39,745 - Stocklogger - DEBUG - cash: 1701829.3641118214, stock a: 7718, stock b:3571, debt: [{'loan': 'yes', 'amount': 1570709.8944799437, 'loan_type': 2, 'repayment_date': 154}] +2025-10-24 16:02:39,745 - Stocklogger - DEBUG - cash: 472185.2351556688, stock a: 628, stock b:70587, debt: [{'loan': 'yes', 'amount': 373912.35418813064, 'loan_type': 0, 'repayment_date': 198}] +2025-10-24 16:02:39,745 - Stocklogger - DEBUG - cash: 855190.8529531471, stock a: 34069, stock b:62470, debt: [{'loan': 'yes', 'amount': 1556687.2506208478, 'loan_type': 2, 'repayment_date': 88}] +2025-10-24 16:02:39,745 - Stocklogger - DEBUG - cash: 2104471.9774432546, stock a: 73778, stock b:9619, debt: [{'loan': 'yes', 'amount': 2359320.985823238, 'loan_type': 1, 'repayment_date': 242}] +2025-10-24 16:02:39,745 - Stocklogger - DEBUG - cash: 1218153.5076098903, stock a: 19747, stock b:55947, debt: [{'loan': 'yes', 'amount': 1667617.6788238923, 'loan_type': 1, 'repayment_date': 88}] +2025-10-24 16:02:39,745 - Stocklogger - DEBUG - cash: 1250893.894050185, stock a: 21212, stock b:71991, debt: [{'loan': 'yes', 'amount': 379166.4909928716, 'loan_type': 1, 'repayment_date': 110}] +2025-10-24 16:02:39,745 - Stocklogger - DEBUG - cash: 1116910.126798003, stock a: 17665, stock b:60597, debt: [{'loan': 'yes', 'amount': 1347591.783113098, 'loan_type': 0, 'repayment_date': 22}] +2025-10-24 16:02:39,745 - Stocklogger - DEBUG - cash: 2917606.6858802484, stock a: 39897, stock b:17164, debt: [{'loan': 'yes', 'amount': 1035553.4058200206, 'loan_type': 1, 'repayment_date': 154}] +2025-10-24 16:02:39,745 - Stocklogger - DEBUG - cash: 1376022.460285174, stock a: 3318, stock b:84875, debt: [{'loan': 'yes', 'amount': 4316529.520317309, 'loan_type': 2, 'repayment_date': 264}] +2025-10-24 16:02:39,746 - Stocklogger - DEBUG - cash: 1051472.9898475434, stock a: 102533, stock b:15825, debt: [{'loan': 'yes', 'amount': 4446001.431283527, 'loan_type': 2, 'repayment_date': 88}] +2025-10-24 16:02:39,746 - Stocklogger - DEBUG - --------Simulation Start!-------- +2025-10-24 16:02:39,746 - Stocklogger - DEBUG - --------DAY 1--------- +2025-10-24 16:03:08,179 - Stocklogger - DEBUG - Agents initial... +2025-10-24 16:03:08,179 - Stocklogger - DEBUG - cash: 2628014.0188357686, stock a: 10327, stock b:51450, debt: [{'loan': 'yes', 'amount': 4644250.344831375, 'loan_type': 0, 'repayment_date': 176}] +2025-10-24 16:03:08,179 - Stocklogger - DEBUG - cash: 1575628.5029605287, stock a: 55775, stock b:19139, debt: [{'loan': 'yes', 'amount': 248107.81677528704, 'loan_type': 1, 'repayment_date': 66}] +2025-10-24 16:03:08,180 - Stocklogger - DEBUG - cash: 1227637.4636867049, stock a: 59483, stock b:23775, debt: [{'loan': 'yes', 'amount': 1296563.5203279573, 'loan_type': 0, 'repayment_date': 154}] +2025-10-24 16:03:08,180 - Stocklogger - DEBUG - cash: 876039.8885004605, stock a: 62761, stock b:9595, debt: [{'loan': 'yes', 'amount': 993297.4995183136, 'loan_type': 2, 'repayment_date': 132}] +2025-10-24 16:03:08,180 - Stocklogger - DEBUG - cash: 2423664.477855701, stock a: 6994, stock b:22976, debt: [{'loan': 'yes', 'amount': 2704921.7909743655, 'loan_type': 2, 'repayment_date': 220}] +2025-10-24 16:03:08,180 - Stocklogger - DEBUG - cash: 2058925.3147623339, stock a: 5385, stock b:59647, debt: [{'loan': 'yes', 'amount': 2474371.3589736633, 'loan_type': 1, 'repayment_date': 242}] +2025-10-24 16:03:08,180 - Stocklogger - DEBUG - cash: 713992.8207120327, stock a: 78841, stock b:23863, debt: [{'loan': 'yes', 'amount': 2277298.0493941354, 'loan_type': 1, 'repayment_date': 264}] +2025-10-24 16:03:08,180 - Stocklogger - DEBUG - cash: 844368.0379330548, stock a: 86290, stock b:4977, debt: [{'loan': 'yes', 'amount': 1645818.8215394858, 'loan_type': 1, 'repayment_date': 154}] +2025-10-24 16:03:08,180 - Stocklogger - DEBUG - cash: 987615.5422011773, stock a: 86530, stock b:5124, debt: [{'loan': 'yes', 'amount': 2195899.0027785487, 'loan_type': 0, 'repayment_date': 110}] +2025-10-24 16:03:08,180 - Stocklogger - DEBUG - cash: 1347021.3634425905, stock a: 46810, stock b:20942, debt: [{'loan': 'yes', 'amount': 774982.5160154855, 'loan_type': 0, 'repayment_date': 44}] +2025-10-24 16:03:08,180 - Stocklogger - DEBUG - cash: 3586417.6998416516, stock a: 41883, stock b:1309, debt: [{'loan': 'yes', 'amount': 4044021.3525270354, 'loan_type': 2, 'repayment_date': 110}] +2025-10-24 16:03:08,180 - Stocklogger - DEBUG - cash: 706319.9926034353, stock a: 45870, stock b:54234, debt: [{'loan': 'yes', 'amount': 2337198.1439261697, 'loan_type': 2, 'repayment_date': 66}] +2025-10-24 16:03:08,181 - Stocklogger - DEBUG - cash: 438575.62570376595, stock a: 72074, stock b:29299, debt: [{'loan': 'yes', 'amount': 1794451.6248822622, 'loan_type': 0, 'repayment_date': 176}] +2025-10-24 16:03:08,181 - Stocklogger - DEBUG - cash: 2236784.1836264594, stock a: 54846, stock b:16176, debt: [{'loan': 'yes', 'amount': 2736779.6910646344, 'loan_type': 1, 'repayment_date': 264}] +2025-10-24 16:03:08,181 - Stocklogger - DEBUG - cash: 446206.26214482874, stock a: 121338, stock b:13651, debt: [{'loan': 'yes', 'amount': 2172631.857682243, 'loan_type': 0, 'repayment_date': 66}] +2025-10-24 16:03:08,181 - Stocklogger - DEBUG - cash: 719374.0988250902, stock a: 21497, stock b:39774, debt: [{'loan': 'yes', 'amount': 1145542.5654920104, 'loan_type': 1, 'repayment_date': 198}] +2025-10-24 16:03:08,181 - Stocklogger - DEBUG - cash: 1207302.9943838913, stock a: 76376, stock b:13328, debt: [{'loan': 'yes', 'amount': 1267120.38334236, 'loan_type': 0, 'repayment_date': 264}] +2025-10-24 16:03:08,181 - Stocklogger - DEBUG - cash: 816314.3075697938, stock a: 71737, stock b:18823, debt: [{'loan': 'yes', 'amount': 894327.6118896615, 'loan_type': 2, 'repayment_date': 132}] +2025-10-24 16:03:08,181 - Stocklogger - DEBUG - cash: 1107587.8722931321, stock a: 2229, stock b:88834, debt: [{'loan': 'yes', 'amount': 3738821.883965082, 'loan_type': 1, 'repayment_date': 264}] +2025-10-24 16:03:08,181 - Stocklogger - DEBUG - cash: 21716.744226327744, stock a: 4303, stock b:72189, debt: [{'loan': 'yes', 'amount': 451868.2694524656, 'loan_type': 2, 'repayment_date': 264}] +2025-10-24 16:03:08,181 - Stocklogger - DEBUG - cash: 421469.52530078887, stock a: 104721, stock b:32170, debt: [{'loan': 'yes', 'amount': 1496311.2565241754, 'loan_type': 2, 'repayment_date': 44}] +2025-10-24 16:03:08,181 - Stocklogger - DEBUG - cash: 287486.30799195654, stock a: 145918, stock b:1639, debt: [{'loan': 'yes', 'amount': 2270831.2720787823, 'loan_type': 2, 'repayment_date': 242}] +2025-10-24 16:03:08,181 - Stocklogger - DEBUG - cash: 316971.3011806985, stock a: 24446, stock b:18812, debt: [{'loan': 'yes', 'amount': 1296399.8888750693, 'loan_type': 1, 'repayment_date': 110}] +2025-10-24 16:03:08,181 - Stocklogger - DEBUG - cash: 2632833.899492248, stock a: 7636, stock b:36647, debt: [{'loan': 'yes', 'amount': 945986.2117676276, 'loan_type': 0, 'repayment_date': 242}] +2025-10-24 16:03:08,182 - Stocklogger - DEBUG - cash: 1161623.0978763031, stock a: 68785, stock b:27672, debt: [{'loan': 'yes', 'amount': 745326.2318084275, 'loan_type': 1, 'repayment_date': 66}] +2025-10-24 16:03:08,182 - Stocklogger - DEBUG - cash: 21364.550522861013, stock a: 11743, stock b:34839, debt: [{'loan': 'yes', 'amount': 300190.49708657985, 'loan_type': 2, 'repayment_date': 132}] +2025-10-24 16:03:08,182 - Stocklogger - DEBUG - cash: 612637.0268820719, stock a: 3703, stock b:67355, debt: [{'loan': 'yes', 'amount': 799038.436524987, 'loan_type': 2, 'repayment_date': 110}] +2025-10-24 16:03:08,182 - Stocklogger - DEBUG - cash: 1058622.835393662, stock a: 22116, stock b:51633, debt: [{'loan': 'yes', 'amount': 1124803.1466094942, 'loan_type': 1, 'repayment_date': 88}] +2025-10-24 16:03:08,182 - Stocklogger - DEBUG - cash: 1640874.4919648704, stock a: 92427, stock b:6819, debt: [{'loan': 'yes', 'amount': 3958827.9959644545, 'loan_type': 0, 'repayment_date': 22}] +2025-10-24 16:03:08,182 - Stocklogger - DEBUG - cash: 292805.3420100607, stock a: 19988, stock b:33463, debt: [{'loan': 'yes', 'amount': 1807345.163207969, 'loan_type': 2, 'repayment_date': 176}] +2025-10-24 16:03:08,182 - Stocklogger - DEBUG - cash: 658341.1215092916, stock a: 23446, stock b:17238, debt: [{'loan': 'yes', 'amount': 336032.5427131927, 'loan_type': 2, 'repayment_date': 176}] +2025-10-24 16:03:08,182 - Stocklogger - DEBUG - cash: 77420.85862141756, stock a: 121133, stock b:20036, debt: [{'loan': 'yes', 'amount': 1657756.0315958185, 'loan_type': 2, 'repayment_date': 44}] +2025-10-24 16:03:08,183 - Stocklogger - DEBUG - cash: 1843464.34088831, stock a: 67740, stock b:4463, debt: [{'loan': 'yes', 'amount': 3380180.7322104005, 'loan_type': 2, 'repayment_date': 88}] +2025-10-24 16:03:08,183 - Stocklogger - DEBUG - cash: 2687463.881965043, stock a: 61573, stock b:8071, debt: [{'loan': 'yes', 'amount': 1039451.3902710556, 'loan_type': 0, 'repayment_date': 44}] +2025-10-24 16:03:08,183 - Stocklogger - DEBUG - cash: 227863.87421731447, stock a: 142838, stock b:12049, debt: [{'loan': 'yes', 'amount': 701324.6652858662, 'loan_type': 1, 'repayment_date': 264}] +2025-10-24 16:03:08,183 - Stocklogger - DEBUG - cash: 202964.30698378332, stock a: 76997, stock b:58465, debt: [{'loan': 'yes', 'amount': 2512424.2159376773, 'loan_type': 2, 'repayment_date': 220}] +2025-10-24 16:03:08,183 - Stocklogger - DEBUG - cash: 2264824.37663207, stock a: 48849, stock b:19842, debt: [{'loan': 'yes', 'amount': 446011.750864555, 'loan_type': 2, 'repayment_date': 132}] +2025-10-24 16:03:08,183 - Stocklogger - DEBUG - cash: 1391693.4898696076, stock a: 72652, stock b:22587, debt: [{'loan': 'yes', 'amount': 3233433.656341099, 'loan_type': 2, 'repayment_date': 264}] +2025-10-24 16:03:08,183 - Stocklogger - DEBUG - cash: 1369311.3805324924, stock a: 37955, stock b:37921, debt: [{'loan': 'yes', 'amount': 3380428.454978519, 'loan_type': 0, 'repayment_date': 220}] +2025-10-24 16:03:08,183 - Stocklogger - DEBUG - cash: 445591.1288279618, stock a: 12342, stock b:21094, debt: [{'loan': 'yes', 'amount': 821227.6764383203, 'loan_type': 0, 'repayment_date': 242}] +2025-10-24 16:03:08,183 - Stocklogger - DEBUG - cash: 2053585.3048055856, stock a: 54001, stock b:20496, debt: [{'loan': 'yes', 'amount': 644438.019813347, 'loan_type': 2, 'repayment_date': 22}] +2025-10-24 16:03:08,183 - Stocklogger - DEBUG - cash: 2636859.4626757107, stock a: 33791, stock b:29395, debt: [{'loan': 'yes', 'amount': 1888605.025988991, 'loan_type': 2, 'repayment_date': 88}] +2025-10-24 16:03:08,183 - Stocklogger - DEBUG - cash: 906094.7229950811, stock a: 15254, stock b:45495, debt: [{'loan': 'yes', 'amount': 1281806.7440144627, 'loan_type': 2, 'repayment_date': 22}] +2025-10-24 16:03:08,183 - Stocklogger - DEBUG - cash: 2003.5962026365705, stock a: 29157, stock b:89602, debt: [{'loan': 'yes', 'amount': 2059739.337042018, 'loan_type': 2, 'repayment_date': 264}] +2025-10-24 16:03:08,184 - Stocklogger - DEBUG - cash: 2446405.2716107606, stock a: 20380, stock b:46685, debt: [{'loan': 'yes', 'amount': 3803692.8822117234, 'loan_type': 2, 'repayment_date': 110}] +2025-10-24 16:03:08,184 - Stocklogger - DEBUG - cash: 3035444.872180986, stock a: 21826, stock b:9617, debt: [{'loan': 'yes', 'amount': 1718121.5058540478, 'loan_type': 2, 'repayment_date': 154}] +2025-10-24 16:03:08,184 - Stocklogger - DEBUG - cash: 949853.0306795188, stock a: 12980, stock b:66142, debt: [{'loan': 'yes', 'amount': 1680998.287422193, 'loan_type': 0, 'repayment_date': 198}] +2025-10-24 16:03:08,184 - Stocklogger - DEBUG - cash: 1272593.154430191, stock a: 72053, stock b:18748, debt: [{'loan': 'yes', 'amount': 4148762.489183309, 'loan_type': 0, 'repayment_date': 220}] +2025-10-24 16:03:08,184 - Stocklogger - DEBUG - cash: 382586.6763392249, stock a: 35122, stock b:28133, debt: [{'loan': 'yes', 'amount': 1291883.2802241864, 'loan_type': 2, 'repayment_date': 88}] +2025-10-24 16:03:08,184 - Stocklogger - DEBUG - cash: 20784.725433406482, stock a: 61668, stock b:45837, debt: [{'loan': 'yes', 'amount': 2116301.280517322, 'loan_type': 0, 'repayment_date': 44}] +2025-10-24 16:03:08,184 - Stocklogger - DEBUG - --------Simulation Start!-------- +2025-10-24 16:03:08,184 - Stocklogger - DEBUG - --------DAY 1--------- +2025-10-24 16:03:42,052 - Stocklogger - DEBUG - Agents initial... +2025-10-24 16:03:42,052 - Stocklogger - DEBUG - cash: 164169.43992627776, stock a: 56788, stock b:45620, debt: [{'loan': 'yes', 'amount': 2302979.2312148716, 'loan_type': 0, 'repayment_date': 44}] +2025-10-24 16:03:42,052 - Stocklogger - DEBUG - cash: 147425.03728431277, stock a: 9225, stock b:99296, debt: [{'loan': 'yes', 'amount': 931912.3262590274, 'loan_type': 2, 'repayment_date': 154}] +2025-10-24 16:03:42,052 - Stocklogger - DEBUG - cash: 3094104.8734811316, stock a: 15198, stock b:27234, debt: [{'loan': 'yes', 'amount': 4432112.508195672, 'loan_type': 1, 'repayment_date': 66}] +2025-10-24 16:03:42,053 - Stocklogger - DEBUG - cash: 364325.5214978397, stock a: 19589, stock b:58627, debt: [{'loan': 'yes', 'amount': 2362218.779352973, 'loan_type': 1, 'repayment_date': 44}] +2025-10-24 16:03:42,053 - Stocklogger - DEBUG - cash: 37871.62471707573, stock a: 58514, stock b:58551, debt: [{'loan': 'yes', 'amount': 1537692.652419549, 'loan_type': 0, 'repayment_date': 22}] +2025-10-24 16:03:42,053 - Stocklogger - DEBUG - cash: 205009.80695702997, stock a: 137304, stock b:4590, debt: [{'loan': 'yes', 'amount': 4140968.69933372, 'loan_type': 0, 'repayment_date': 110}] +2025-10-24 16:03:42,053 - Stocklogger - DEBUG - cash: 1556736.2137471747, stock a: 30469, stock b:55214, debt: [{'loan': 'yes', 'amount': 2449404.859397493, 'loan_type': 1, 'repayment_date': 44}] +2025-10-24 16:03:42,053 - Stocklogger - DEBUG - cash: 180167.58321323822, stock a: 36411, stock b:86624, debt: [{'loan': 'yes', 'amount': 3015503.723155795, 'loan_type': 0, 'repayment_date': 220}] +2025-10-24 16:03:42,053 - Stocklogger - DEBUG - cash: 1770587.6965900136, stock a: 4794, stock b:58444, debt: [{'loan': 'yes', 'amount': 93760.32399082024, 'loan_type': 0, 'repayment_date': 110}] +2025-10-24 16:03:42,053 - Stocklogger - DEBUG - cash: 1322091.912241538, stock a: 105750, stock b:4274, debt: [{'loan': 'yes', 'amount': 417399.8263113121, 'loan_type': 2, 'repayment_date': 154}] +2025-10-24 16:03:42,053 - Stocklogger - DEBUG - cash: 572978.7558653044, stock a: 77931, stock b:17302, debt: [{'loan': 'yes', 'amount': 2143016.559448312, 'loan_type': 2, 'repayment_date': 264}] +2025-10-24 16:03:42,054 - Stocklogger - DEBUG - cash: 2920271.412955147, stock a: 9676, stock b:23432, debt: [{'loan': 'yes', 'amount': 1187018.929489086, 'loan_type': 1, 'repayment_date': 242}] +2025-10-24 16:03:42,054 - Stocklogger - DEBUG - cash: 772232.1508928682, stock a: 71384, stock b:47775, debt: [{'loan': 'yes', 'amount': 693721.6729892209, 'loan_type': 0, 'repayment_date': 22}] +2025-10-24 16:03:42,054 - Stocklogger - DEBUG - cash: 831409.7433917978, stock a: 54130, stock b:46613, debt: [{'loan': 'yes', 'amount': 3647464.4809543896, 'loan_type': 1, 'repayment_date': 110}] +2025-10-24 16:03:42,054 - Stocklogger - DEBUG - cash: 1499559.6082636453, stock a: 89634, stock b:50, debt: [{'loan': 'yes', 'amount': 1737644.7280586793, 'loan_type': 0, 'repayment_date': 220}] +2025-10-24 16:03:42,054 - Stocklogger - DEBUG - cash: 1104283.7836374408, stock a: 39041, stock b:47315, debt: [{'loan': 'yes', 'amount': 3465757.199407071, 'loan_type': 1, 'repayment_date': 154}] +2025-10-24 16:03:42,054 - Stocklogger - DEBUG - cash: 1926393.8639541357, stock a: 45280, stock b:7861, debt: [{'loan': 'yes', 'amount': 452395.49322963646, 'loan_type': 0, 'repayment_date': 154}] +2025-10-24 16:03:42,054 - Stocklogger - DEBUG - cash: 2304414.9497516593, stock a: 2652, stock b:845, debt: [{'loan': 'yes', 'amount': 735995.614889367, 'loan_type': 2, 'repayment_date': 66}] +2025-10-24 16:03:42,054 - Stocklogger - DEBUG - cash: 1288298.2748378485, stock a: 57709, stock b:35699, debt: [{'loan': 'yes', 'amount': 1368693.2472897212, 'loan_type': 2, 'repayment_date': 264}] +2025-10-24 16:03:42,054 - Stocklogger - DEBUG - cash: 469753.01202057453, stock a: 48649, stock b:39822, debt: [{'loan': 'yes', 'amount': 1377142.5430046453, 'loan_type': 1, 'repayment_date': 22}] +2025-10-24 16:03:42,054 - Stocklogger - DEBUG - cash: 1132654.4931622462, stock a: 21235, stock b:54255, debt: [{'loan': 'yes', 'amount': 3777183.5273830867, 'loan_type': 1, 'repayment_date': 154}] +2025-10-24 16:03:42,054 - Stocklogger - DEBUG - cash: 1287756.1838525808, stock a: 49462, stock b:33661, debt: [{'loan': 'yes', 'amount': 3416015.933733134, 'loan_type': 0, 'repayment_date': 220}] +2025-10-24 16:03:42,055 - Stocklogger - DEBUG - cash: 3342115.225360849, stock a: 20788, stock b:1151, debt: [{'loan': 'yes', 'amount': 2087875.5291276285, 'loan_type': 1, 'repayment_date': 66}] +2025-10-24 16:03:42,055 - Stocklogger - DEBUG - cash: 1325656.7820787334, stock a: 7996, stock b:30197, debt: [{'loan': 'yes', 'amount': 1733657.9017805692, 'loan_type': 1, 'repayment_date': 132}] +2025-10-24 16:03:42,055 - Stocklogger - DEBUG - cash: 3915096.3153092125, stock a: 16261, stock b:10836, debt: [{'loan': 'yes', 'amount': 1746075.3563335158, 'loan_type': 2, 'repayment_date': 88}] +2025-10-24 16:03:42,055 - Stocklogger - DEBUG - cash: 1345368.125064148, stock a: 10051, stock b:63452, debt: [{'loan': 'yes', 'amount': 568330.3858680138, 'loan_type': 2, 'repayment_date': 264}] +2025-10-24 16:03:42,055 - Stocklogger - DEBUG - cash: 1230358.450502244, stock a: 56372, stock b:27143, debt: [{'loan': 'yes', 'amount': 2261762.2188114845, 'loan_type': 2, 'repayment_date': 88}] +2025-10-24 16:03:42,055 - Stocklogger - DEBUG - cash: 1572463.092586308, stock a: 6094, stock b:46975, debt: [{'loan': 'yes', 'amount': 3402457.106264234, 'loan_type': 1, 'repayment_date': 66}] +2025-10-24 16:03:42,055 - Stocklogger - DEBUG - cash: 740900.8527880656, stock a: 45557, stock b:41248, debt: [{'loan': 'yes', 'amount': 1755039.030827667, 'loan_type': 2, 'repayment_date': 264}] +2025-10-24 16:03:42,055 - Stocklogger - DEBUG - cash: 287849.33975570416, stock a: 10104, stock b:75265, debt: [{'loan': 'yes', 'amount': 634424.9921046047, 'loan_type': 0, 'repayment_date': 176}] +2025-10-24 16:03:42,055 - Stocklogger - DEBUG - cash: 2723749.8841824476, stock a: 11163, stock b:42984, debt: [{'loan': 'yes', 'amount': 1676074.9039477368, 'loan_type': 1, 'repayment_date': 176}] +2025-10-24 16:03:42,055 - Stocklogger - DEBUG - cash: 606541.5780529315, stock a: 97461, stock b:29302, debt: [{'loan': 'yes', 'amount': 3746610.457198696, 'loan_type': 1, 'repayment_date': 154}] +2025-10-24 16:03:42,055 - Stocklogger - DEBUG - cash: 3488191.327772551, stock a: 20446, stock b:7008, debt: [{'loan': 'yes', 'amount': 3111579.4234752296, 'loan_type': 2, 'repayment_date': 22}] +2025-10-24 16:03:42,056 - Stocklogger - DEBUG - cash: 2328407.6037961226, stock a: 64926, stock b:14058, debt: [{'loan': 'yes', 'amount': 3892576.797523147, 'loan_type': 1, 'repayment_date': 198}] +2025-10-24 16:03:42,056 - Stocklogger - DEBUG - cash: 1525115.5244321574, stock a: 85066, stock b:9692, debt: [{'loan': 'yes', 'amount': 738879.1687197338, 'loan_type': 2, 'repayment_date': 198}] +2025-10-24 16:03:42,056 - Stocklogger - DEBUG - cash: 3065964.244959643, stock a: 2471, stock b:46454, debt: [{'loan': 'yes', 'amount': 4705051.91166967, 'loan_type': 2, 'repayment_date': 110}] +2025-10-24 16:03:42,056 - Stocklogger - DEBUG - cash: 1348456.2816599156, stock a: 15458, stock b:63202, debt: [{'loan': 'yes', 'amount': 617432.1558226431, 'loan_type': 1, 'repayment_date': 88}] +2025-10-24 16:03:42,056 - Stocklogger - DEBUG - cash: 387890.34412013646, stock a: 126955, stock b:14706, debt: [{'loan': 'yes', 'amount': 957033.7837665993, 'loan_type': 2, 'repayment_date': 220}] +2025-10-24 16:03:42,056 - Stocklogger - DEBUG - cash: 2655719.5028566434, stock a: 57108, stock b:10030, debt: [{'loan': 'yes', 'amount': 1879918.2143304765, 'loan_type': 1, 'repayment_date': 154}] +2025-10-24 16:03:42,056 - Stocklogger - DEBUG - cash: 1241240.6486391497, stock a: 39953, stock b:37077, debt: [{'loan': 'yes', 'amount': 3660597.4355508103, 'loan_type': 0, 'repayment_date': 154}] +2025-10-24 16:03:42,056 - Stocklogger - DEBUG - cash: 3055113.3510750546, stock a: 5022, stock b:2495, debt: [{'loan': 'yes', 'amount': 1635457.8010551168, 'loan_type': 2, 'repayment_date': 132}] +2025-10-24 16:03:42,056 - Stocklogger - DEBUG - cash: 905489.3965660332, stock a: 52451, stock b:6141, debt: [{'loan': 'yes', 'amount': 1568212.2251023506, 'loan_type': 1, 'repayment_date': 176}] +2025-10-24 16:03:42,056 - Stocklogger - DEBUG - cash: 1002691.8234353882, stock a: 10443, stock b:87239, debt: [{'loan': 'yes', 'amount': 4605164.329210034, 'loan_type': 0, 'repayment_date': 242}] +2025-10-24 16:03:42,057 - Stocklogger - DEBUG - cash: 945616.138451112, stock a: 55741, stock b:30001, debt: [{'loan': 'yes', 'amount': 660279.9804771104, 'loan_type': 1, 'repayment_date': 22}] +2025-10-24 16:03:42,057 - Stocklogger - DEBUG - cash: 572412.9387488991, stock a: 125989, stock b:15189, debt: [{'loan': 'yes', 'amount': 1273615.9639320916, 'loan_type': 1, 'repayment_date': 220}] +2025-10-24 16:03:42,057 - Stocklogger - DEBUG - cash: 3358868.820088406, stock a: 24343, stock b:18039, debt: [{'loan': 'yes', 'amount': 1143267.654184012, 'loan_type': 2, 'repayment_date': 264}] +2025-10-24 16:03:42,057 - Stocklogger - DEBUG - cash: 581015.6847952746, stock a: 135351, stock b:3091, debt: [{'loan': 'yes', 'amount': 3710674.2044452545, 'loan_type': 1, 'repayment_date': 154}] +2025-10-24 16:03:42,057 - Stocklogger - DEBUG - cash: 2075729.2461682125, stock a: 13049, stock b:56135, debt: [{'loan': 'yes', 'amount': 833882.6435993346, 'loan_type': 1, 'repayment_date': 66}] +2025-10-24 16:03:42,057 - Stocklogger - DEBUG - cash: 2463967.8883658196, stock a: 47000, stock b:25225, debt: [{'loan': 'yes', 'amount': 974588.5929889287, 'loan_type': 2, 'repayment_date': 44}] +2025-10-24 16:03:42,057 - Stocklogger - DEBUG - cash: 2370482.8722505243, stock a: 44994, stock b:25442, debt: [{'loan': 'yes', 'amount': 3809096.869968763, 'loan_type': 0, 'repayment_date': 176}] +2025-10-24 16:03:42,057 - Stocklogger - DEBUG - --------Simulation Start!-------- +2025-10-24 16:03:42,057 - Stocklogger - DEBUG - --------DAY 1--------- +2025-10-24 16:03:42,344 - Stocklogger - WARNING - OpenAI api retry...Error code: 401 - {'error': {'message': "You didn't provide an API key. You need to provide your API key in an Authorization header using Bearer auth (i.e. Authorization: Bearer YOUR_KEY), or as the password field (with blank username) if you're accessing the API from your browser and are prompted for a username and password. You can obtain an API key from https://platform.openai.com/account/api-keys.", 'type': 'invalid_request_error', 'param': None, 'code': None}} +2025-10-24 16:03:43,444 - Stocklogger - WARNING - OpenAI api retry...Error code: 401 - {'error': {'message': "You didn't provide an API key. You need to provide your API key in an Authorization header using Bearer auth (i.e. Authorization: Bearer YOUR_KEY), or as the password field (with blank username) if you're accessing the API from your browser and are prompted for a username and password. You can obtain an API key from https://platform.openai.com/account/api-keys.", 'type': 'invalid_request_error', 'param': None, 'code': None}} +2025-10-24 16:03:44,445 - Stocklogger - ERROR - ERROR: OPENAI API FAILED. SKIP THIS INTERACTION. +2025-10-24 16:03:44,561 - Stocklogger - WARNING - OpenAI api retry...Error code: 401 - {'error': {'message': "You didn't provide an API key. You need to provide your API key in an Authorization header using Bearer auth (i.e. Authorization: Bearer YOUR_KEY), or as the password field (with blank username) if you're accessing the API from your browser and are prompted for a username and password. You can obtain an API key from https://platform.openai.com/account/api-keys.", 'type': 'invalid_request_error', 'param': None, 'code': None}} +2025-10-24 16:03:45,624 - Stocklogger - WARNING - OpenAI api retry...Error code: 401 - {'error': {'message': "You didn't provide an API key. You need to provide your API key in an Authorization header using Bearer auth (i.e. Authorization: Bearer YOUR_KEY), or as the password field (with blank username) if you're accessing the API from your browser and are prompted for a username and password. You can obtain an API key from https://platform.openai.com/account/api-keys.", 'type': 'invalid_request_error', 'param': None, 'code': None}} +2025-10-24 16:03:46,625 - Stocklogger - ERROR - ERROR: OPENAI API FAILED. SKIP THIS INTERACTION. +2025-10-24 16:03:46,784 - Stocklogger - WARNING - OpenAI api retry...Error code: 401 - {'error': {'message': "You didn't provide an API key. You need to provide your API key in an Authorization header using Bearer auth (i.e. Authorization: Bearer YOUR_KEY), or as the password field (with blank username) if you're accessing the API from your browser and are prompted for a username and password. You can obtain an API key from https://platform.openai.com/account/api-keys.", 'type': 'invalid_request_error', 'param': None, 'code': None}} +2025-10-24 16:03:47,866 - Stocklogger - WARNING - OpenAI api retry...Error code: 401 - {'error': {'message': "You didn't provide an API key. You need to provide your API key in an Authorization header using Bearer auth (i.e. Authorization: Bearer YOUR_KEY), or as the password field (with blank username) if you're accessing the API from your browser and are prompted for a username and password. You can obtain an API key from https://platform.openai.com/account/api-keys.", 'type': 'invalid_request_error', 'param': None, 'code': None}} +2025-10-24 16:03:48,866 - Stocklogger - ERROR - ERROR: OPENAI API FAILED. SKIP THIS INTERACTION. +2025-10-24 16:03:48,965 - Stocklogger - WARNING - OpenAI api retry...Error code: 401 - {'error': {'message': "You didn't provide an API key. You need to provide your API key in an Authorization header using Bearer auth (i.e. Authorization: Bearer YOUR_KEY), or as the password field (with blank username) if you're accessing the API from your browser and are prompted for a username and password. You can obtain an API key from https://platform.openai.com/account/api-keys.", 'type': 'invalid_request_error', 'param': None, 'code': None}} +2025-10-24 16:03:50,049 - Stocklogger - WARNING - OpenAI api retry...Error code: 401 - {'error': {'message': "You didn't provide an API key. You need to provide your API key in an Authorization header using Bearer auth (i.e. Authorization: Bearer YOUR_KEY), or as the password field (with blank username) if you're accessing the API from your browser and are prompted for a username and password. You can obtain an API key from https://platform.openai.com/account/api-keys.", 'type': 'invalid_request_error', 'param': None, 'code': None}} +2025-10-24 16:03:51,049 - Stocklogger - ERROR - ERROR: OPENAI API FAILED. SKIP THIS INTERACTION. +2025-10-24 16:03:51,221 - Stocklogger - WARNING - OpenAI api retry...Error code: 401 - {'error': {'message': "You didn't provide an API key. You need to provide your API key in an Authorization header using Bearer auth (i.e. Authorization: Bearer YOUR_KEY), or as the password field (with blank username) if you're accessing the API from your browser and are prompted for a username and password. You can obtain an API key from https://platform.openai.com/account/api-keys.", 'type': 'invalid_request_error', 'param': None, 'code': None}} +2025-10-24 16:03:52,288 - Stocklogger - WARNING - OpenAI api retry...Error code: 401 - {'error': {'message': "You didn't provide an API key. You need to provide your API key in an Authorization header using Bearer auth (i.e. Authorization: Bearer YOUR_KEY), or as the password field (with blank username) if you're accessing the API from your browser and are prompted for a username and password. You can obtain an API key from https://platform.openai.com/account/api-keys.", 'type': 'invalid_request_error', 'param': None, 'code': None}} +2025-10-24 16:03:53,289 - Stocklogger - ERROR - ERROR: OPENAI API FAILED. SKIP THIS INTERACTION. +2025-10-24 16:03:53,390 - Stocklogger - WARNING - OpenAI api retry...Error code: 401 - {'error': {'message': "You didn't provide an API key. You need to provide your API key in an Authorization header using Bearer auth (i.e. Authorization: Bearer YOUR_KEY), or as the password field (with blank username) if you're accessing the API from your browser and are prompted for a username and password. You can obtain an API key from https://platform.openai.com/account/api-keys.", 'type': 'invalid_request_error', 'param': None, 'code': None}} +2025-10-24 16:03:54,462 - Stocklogger - WARNING - OpenAI api retry...Error code: 401 - {'error': {'message': "You didn't provide an API key. You need to provide your API key in an Authorization header using Bearer auth (i.e. Authorization: Bearer YOUR_KEY), or as the password field (with blank username) if you're accessing the API from your browser and are prompted for a username and password. You can obtain an API key from https://platform.openai.com/account/api-keys.", 'type': 'invalid_request_error', 'param': None, 'code': None}} +2025-10-24 16:03:55,463 - Stocklogger - ERROR - ERROR: OPENAI API FAILED. SKIP THIS INTERACTION. +2025-10-24 16:03:55,576 - Stocklogger - WARNING - OpenAI api retry...Error code: 401 - {'error': {'message': "You didn't provide an API key. You need to provide your API key in an Authorization header using Bearer auth (i.e. Authorization: Bearer YOUR_KEY), or as the password field (with blank username) if you're accessing the API from your browser and are prompted for a username and password. You can obtain an API key from https://platform.openai.com/account/api-keys.", 'type': 'invalid_request_error', 'param': None, 'code': None}} +2025-10-24 16:03:56,641 - Stocklogger - WARNING - OpenAI api retry...Error code: 401 - {'error': {'message': "You didn't provide an API key. You need to provide your API key in an Authorization header using Bearer auth (i.e. Authorization: Bearer YOUR_KEY), or as the password field (with blank username) if you're accessing the API from your browser and are prompted for a username and password. You can obtain an API key from https://platform.openai.com/account/api-keys.", 'type': 'invalid_request_error', 'param': None, 'code': None}} +2025-10-24 16:03:57,642 - Stocklogger - ERROR - ERROR: OPENAI API FAILED. SKIP THIS INTERACTION. +2025-10-24 16:03:57,745 - Stocklogger - WARNING - OpenAI api retry...Error code: 401 - {'error': {'message': "You didn't provide an API key. You need to provide your API key in an Authorization header using Bearer auth (i.e. Authorization: Bearer YOUR_KEY), or as the password field (with blank username) if you're accessing the API from your browser and are prompted for a username and password. You can obtain an API key from https://platform.openai.com/account/api-keys.", 'type': 'invalid_request_error', 'param': None, 'code': None}} +2025-10-24 16:03:58,819 - Stocklogger - WARNING - OpenAI api retry...Error code: 401 - {'error': {'message': "You didn't provide an API key. You need to provide your API key in an Authorization header using Bearer auth (i.e. Authorization: Bearer YOUR_KEY), or as the password field (with blank username) if you're accessing the API from your browser and are prompted for a username and password. You can obtain an API key from https://platform.openai.com/account/api-keys.", 'type': 'invalid_request_error', 'param': None, 'code': None}} +2025-10-24 16:03:59,820 - Stocklogger - ERROR - ERROR: OPENAI API FAILED. SKIP THIS INTERACTION. +2025-10-24 16:03:59,926 - Stocklogger - WARNING - OpenAI api retry...Error code: 401 - {'error': {'message': "You didn't provide an API key. You need to provide your API key in an Authorization header using Bearer auth (i.e. Authorization: Bearer YOUR_KEY), or as the password field (with blank username) if you're accessing the API from your browser and are prompted for a username and password. You can obtain an API key from https://platform.openai.com/account/api-keys.", 'type': 'invalid_request_error', 'param': None, 'code': None}} +2025-10-24 16:04:01,000 - Stocklogger - WARNING - OpenAI api retry...Error code: 401 - {'error': {'message': "You didn't provide an API key. You need to provide your API key in an Authorization header using Bearer auth (i.e. Authorization: Bearer YOUR_KEY), or as the password field (with blank username) if you're accessing the API from your browser and are prompted for a username and password. You can obtain an API key from https://platform.openai.com/account/api-keys.", 'type': 'invalid_request_error', 'param': None, 'code': None}} +2025-10-24 16:04:02,000 - Stocklogger - ERROR - ERROR: OPENAI API FAILED. SKIP THIS INTERACTION. +2025-10-24 16:04:02,279 - Stocklogger - WARNING - OpenAI api retry...Error code: 401 - {'error': {'message': "You didn't provide an API key. You need to provide your API key in an Authorization header using Bearer auth (i.e. Authorization: Bearer YOUR_KEY), or as the password field (with blank username) if you're accessing the API from your browser and are prompted for a username and password. You can obtain an API key from https://platform.openai.com/account/api-keys.", 'type': 'invalid_request_error', 'param': None, 'code': None}} +2025-10-24 16:04:03,348 - Stocklogger - WARNING - OpenAI api retry...Error code: 401 - {'error': {'message': "You didn't provide an API key. You need to provide your API key in an Authorization header using Bearer auth (i.e. Authorization: Bearer YOUR_KEY), or as the password field (with blank username) if you're accessing the API from your browser and are prompted for a username and password. You can obtain an API key from https://platform.openai.com/account/api-keys.", 'type': 'invalid_request_error', 'param': None, 'code': None}} +2025-10-24 16:04:04,348 - Stocklogger - ERROR - ERROR: OPENAI API FAILED. SKIP THIS INTERACTION. +2025-10-24 16:04:04,451 - Stocklogger - WARNING - OpenAI api retry...Error code: 401 - {'error': {'message': "You didn't provide an API key. You need to provide your API key in an Authorization header using Bearer auth (i.e. Authorization: Bearer YOUR_KEY), or as the password field (with blank username) if you're accessing the API from your browser and are prompted for a username and password. You can obtain an API key from https://platform.openai.com/account/api-keys.", 'type': 'invalid_request_error', 'param': None, 'code': None}} +2025-10-24 16:04:05,512 - Stocklogger - WARNING - OpenAI api retry...Error code: 401 - {'error': {'message': "You didn't provide an API key. You need to provide your API key in an Authorization header using Bearer auth (i.e. Authorization: Bearer YOUR_KEY), or as the password field (with blank username) if you're accessing the API from your browser and are prompted for a username and password. You can obtain an API key from https://platform.openai.com/account/api-keys.", 'type': 'invalid_request_error', 'param': None, 'code': None}} +2025-10-24 16:04:06,513 - Stocklogger - ERROR - ERROR: OPENAI API FAILED. SKIP THIS INTERACTION. +2025-10-24 16:04:06,624 - Stocklogger - WARNING - OpenAI api retry...Error code: 401 - {'error': {'message': "You didn't provide an API key. You need to provide your API key in an Authorization header using Bearer auth (i.e. Authorization: Bearer YOUR_KEY), or as the password field (with blank username) if you're accessing the API from your browser and are prompted for a username and password. You can obtain an API key from https://platform.openai.com/account/api-keys.", 'type': 'invalid_request_error', 'param': None, 'code': None}} +2025-10-24 16:04:07,703 - Stocklogger - WARNING - OpenAI api retry...Error code: 401 - {'error': {'message': "You didn't provide an API key. You need to provide your API key in an Authorization header using Bearer auth (i.e. Authorization: Bearer YOUR_KEY), or as the password field (with blank username) if you're accessing the API from your browser and are prompted for a username and password. You can obtain an API key from https://platform.openai.com/account/api-keys.", 'type': 'invalid_request_error', 'param': None, 'code': None}} +2025-10-24 16:05:00,006 - Stocklogger - DEBUG - Agents initial... +2025-10-24 16:05:00,007 - Stocklogger - DEBUG - cash: 1988917.0480964796, stock a: 21136, stock b:34884, debt: [{'loan': 'yes', 'amount': 1673170.7586466437, 'loan_type': 1, 'repayment_date': 132}] +2025-10-24 16:05:00,007 - Stocklogger - DEBUG - cash: 1459803.581309611, stock a: 43750, stock b:2873, debt: [{'loan': 'yes', 'amount': 397574.43021949555, 'loan_type': 2, 'repayment_date': 242}] +2025-10-24 16:05:00,007 - Stocklogger - DEBUG - cash: 244183.56880907487, stock a: 80597, stock b:54300, debt: [{'loan': 'yes', 'amount': 1937637.351186417, 'loan_type': 1, 'repayment_date': 242}] +2025-10-24 16:05:00,008 - Stocklogger - DEBUG - cash: 1243171.9624311938, stock a: 50453, stock b:51488, debt: [{'loan': 'yes', 'amount': 52788.80875485448, 'loan_type': 1, 'repayment_date': 242}] +2025-10-24 16:05:00,008 - Stocklogger - DEBUG - cash: 3508409.4973358475, stock a: 17334, stock b:7166, debt: [{'loan': 'yes', 'amount': 2654024.0363341756, 'loan_type': 2, 'repayment_date': 154}] +2025-10-24 16:05:00,008 - Stocklogger - DEBUG - cash: 9095.79506037761, stock a: 17805, stock b:5035, debt: [{'loan': 'yes', 'amount': 259774.0337861626, 'loan_type': 0, 'repayment_date': 154}] +2025-10-24 16:05:00,008 - Stocklogger - DEBUG - cash: 1887657.1429756894, stock a: 52151, stock b:31940, debt: [{'loan': 'yes', 'amount': 1067710.9325647866, 'loan_type': 0, 'repayment_date': 220}] +2025-10-24 16:05:00,008 - Stocklogger - DEBUG - cash: 4030411.1982034235, stock a: 16213, stock b:3332, debt: [{'loan': 'yes', 'amount': 1584628.3357201512, 'loan_type': 1, 'repayment_date': 66}] +2025-10-24 16:05:00,008 - Stocklogger - DEBUG - cash: 945286.2543379775, stock a: 112662, stock b:15436, debt: [{'loan': 'yes', 'amount': 3628426.587963763, 'loan_type': 1, 'repayment_date': 22}] +2025-10-24 16:05:00,008 - Stocklogger - DEBUG - cash: 2117000.5995960594, stock a: 79127, stock b:6032, debt: [{'loan': 'yes', 'amount': 490084.26457937906, 'loan_type': 0, 'repayment_date': 264}] +2025-10-24 16:05:00,009 - Stocklogger - DEBUG - cash: 1044741.6674265697, stock a: 66825, stock b:23264, debt: [{'loan': 'yes', 'amount': 2684270.681079663, 'loan_type': 1, 'repayment_date': 220}] +2025-10-24 16:05:00,009 - Stocklogger - DEBUG - cash: 1932747.0919187772, stock a: 13435, stock b:28825, debt: [{'loan': 'yes', 'amount': 1557687.8489286539, 'loan_type': 1, 'repayment_date': 242}] +2025-10-24 16:05:00,009 - Stocklogger - DEBUG - cash: 499500.7445368155, stock a: 99270, stock b:9346, debt: [{'loan': 'yes', 'amount': 1357654.793854383, 'loan_type': 0, 'repayment_date': 198}] +2025-10-24 16:05:00,009 - Stocklogger - DEBUG - cash: 1147509.9976787146, stock a: 85552, stock b:31860, debt: [{'loan': 'yes', 'amount': 3358846.364683549, 'loan_type': 0, 'repayment_date': 132}] +2025-10-24 16:05:00,009 - Stocklogger - DEBUG - cash: 1250588.7330281434, stock a: 35831, stock b:58907, debt: [{'loan': 'yes', 'amount': 4169876.904271951, 'loan_type': 0, 'repayment_date': 22}] +2025-10-24 16:05:00,009 - Stocklogger - DEBUG - cash: 761008.0676584635, stock a: 96782, stock b:9674, debt: [{'loan': 'yes', 'amount': 3532968.3727121013, 'loan_type': 1, 'repayment_date': 88}] +2025-10-24 16:05:00,009 - Stocklogger - DEBUG - cash: 2051682.696111477, stock a: 31124, stock b:2173, debt: [{'loan': 'yes', 'amount': 1811965.6261296002, 'loan_type': 0, 'repayment_date': 220}] +2025-10-24 16:05:00,009 - Stocklogger - DEBUG - cash: 1527818.1659294765, stock a: 50032, stock b:2892, debt: [{'loan': 'yes', 'amount': 2517934.3182363426, 'loan_type': 2, 'repayment_date': 220}] +2025-10-24 16:05:00,009 - Stocklogger - DEBUG - cash: 467925.4315162273, stock a: 33741, stock b:52345, debt: [{'loan': 'yes', 'amount': 896867.1776059845, 'loan_type': 0, 'repayment_date': 154}] +2025-10-24 16:05:00,009 - Stocklogger - DEBUG - cash: 1472675.1682109507, stock a: 100245, stock b:9007, debt: [{'loan': 'yes', 'amount': 4026302.14034215, 'loan_type': 2, 'repayment_date': 88}] +2025-10-24 16:05:00,009 - Stocklogger - DEBUG - cash: 3094989.873821696, stock a: 24354, stock b:17037, debt: [{'loan': 'yes', 'amount': 3819236.1645509964, 'loan_type': 0, 'repayment_date': 242}] +2025-10-24 16:05:00,010 - Stocklogger - DEBUG - cash: 2478707.864142312, stock a: 15475, stock b:41593, debt: [{'loan': 'yes', 'amount': 3647910.5145067736, 'loan_type': 2, 'repayment_date': 242}] +2025-10-24 16:05:00,010 - Stocklogger - DEBUG - cash: 1355916.5715178112, stock a: 78532, stock b:27755, debt: [{'loan': 'yes', 'amount': 2391101.971838174, 'loan_type': 2, 'repayment_date': 242}] +2025-10-24 16:05:00,010 - Stocklogger - DEBUG - cash: 474870.5771679884, stock a: 49317, stock b:56507, debt: [{'loan': 'yes', 'amount': 1530191.7791637843, 'loan_type': 1, 'repayment_date': 22}] +2025-10-24 16:05:00,010 - Stocklogger - DEBUG - cash: 325090.5521630809, stock a: 94956, stock b:43316, debt: [{'loan': 'yes', 'amount': 2858907.004952354, 'loan_type': 2, 'repayment_date': 110}] +2025-10-24 16:05:00,010 - Stocklogger - DEBUG - cash: 591402.6464781852, stock a: 58753, stock b:21765, debt: [{'loan': 'yes', 'amount': 1279448.8088530176, 'loan_type': 2, 'repayment_date': 132}] +2025-10-24 16:05:00,010 - Stocklogger - DEBUG - cash: 1524487.8217075197, stock a: 30241, stock b:50620, debt: [{'loan': 'yes', 'amount': 3304088.930366407, 'loan_type': 0, 'repayment_date': 264}] +2025-10-24 16:05:00,010 - Stocklogger - DEBUG - cash: 2001887.966356653, stock a: 28496, stock b:19468, debt: [{'loan': 'yes', 'amount': 2558350.8277743096, 'loan_type': 0, 'repayment_date': 176}] +2025-10-24 16:05:00,010 - Stocklogger - DEBUG - cash: 205290.17176719956, stock a: 112057, stock b:5577, debt: [{'loan': 'yes', 'amount': 1253474.9652761235, 'loan_type': 1, 'repayment_date': 22}] +2025-10-24 16:05:00,010 - Stocklogger - DEBUG - cash: 791714.1603169236, stock a: 45010, stock b:53022, debt: [{'loan': 'yes', 'amount': 737252.9683280393, 'loan_type': 2, 'repayment_date': 264}] +2025-10-24 16:05:00,010 - Stocklogger - DEBUG - cash: 358888.4569619416, stock a: 30858, stock b:83258, debt: [{'loan': 'yes', 'amount': 454935.2383744415, 'loan_type': 1, 'repayment_date': 220}] +2025-10-24 16:05:00,010 - Stocklogger - DEBUG - cash: 1050136.9404241107, stock a: 24636, stock b:18264, debt: [{'loan': 'yes', 'amount': 36120.0813065976, 'loan_type': 1, 'repayment_date': 66}] +2025-10-24 16:05:00,011 - Stocklogger - DEBUG - cash: 981889.7327135134, stock a: 2388, stock b:82656, debt: [{'loan': 'yes', 'amount': 36124.22963217732, 'loan_type': 2, 'repayment_date': 88}] +2025-10-24 16:05:00,011 - Stocklogger - DEBUG - cash: 1922667.0487036535, stock a: 45442, stock b:8611, debt: [{'loan': 'yes', 'amount': 2649788.8885399904, 'loan_type': 2, 'repayment_date': 110}] +2025-10-24 16:05:00,011 - Stocklogger - DEBUG - cash: 1612553.1870141434, stock a: 34334, stock b:7508, debt: [{'loan': 'yes', 'amount': 1781377.1240577875, 'loan_type': 2, 'repayment_date': 176}] +2025-10-24 16:05:00,011 - Stocklogger - DEBUG - cash: 176.1241116471357, stock a: 31204, stock b:95813, debt: [{'loan': 'yes', 'amount': 2460392.6112510855, 'loan_type': 1, 'repayment_date': 264}] +2025-10-24 16:05:00,011 - Stocklogger - DEBUG - cash: 397325.3071997634, stock a: 67332, stock b:50214, debt: [{'loan': 'yes', 'amount': 741128.3540101516, 'loan_type': 1, 'repayment_date': 176}] +2025-10-24 16:05:00,011 - Stocklogger - DEBUG - cash: 907931.8167764106, stock a: 86847, stock b:35062, debt: [{'loan': 'yes', 'amount': 462599.48744106217, 'loan_type': 2, 'repayment_date': 132}] +2025-10-24 16:05:00,011 - Stocklogger - DEBUG - cash: 1857178.5631877512, stock a: 55072, stock b:14250, debt: [{'loan': 'yes', 'amount': 1760467.2612432798, 'loan_type': 1, 'repayment_date': 154}] +2025-10-24 16:05:00,011 - Stocklogger - DEBUG - cash: 3398713.249019869, stock a: 38629, stock b:161, debt: [{'loan': 'yes', 'amount': 3891229.1946997982, 'loan_type': 2, 'repayment_date': 264}] +2025-10-24 16:05:00,011 - Stocklogger - DEBUG - cash: 2231675.535563858, stock a: 76578, stock b:6881, debt: [{'loan': 'yes', 'amount': 3129641.473939735, 'loan_type': 0, 'repayment_date': 154}] +2025-10-24 16:05:00,011 - Stocklogger - DEBUG - cash: 322248.68257340224, stock a: 63493, stock b:41877, debt: [{'loan': 'yes', 'amount': 917443.3261516895, 'loan_type': 0, 'repayment_date': 220}] +2025-10-24 16:05:00,012 - Stocklogger - DEBUG - cash: 469199.4238696262, stock a: 1641, stock b:31874, debt: [{'loan': 'yes', 'amount': 1076738.1819282968, 'loan_type': 2, 'repayment_date': 44}] +2025-10-24 16:05:00,012 - Stocklogger - DEBUG - cash: 1332944.8148218382, stock a: 37747, stock b:5011, debt: [{'loan': 'yes', 'amount': 1560644.22479062, 'loan_type': 0, 'repayment_date': 198}] +2025-10-24 16:05:00,012 - Stocklogger - DEBUG - cash: 1630872.3691560244, stock a: 50417, stock b:11093, debt: [{'loan': 'yes', 'amount': 1902464.9271136145, 'loan_type': 1, 'repayment_date': 22}] +2025-10-24 16:05:00,012 - Stocklogger - DEBUG - cash: 147781.4533594052, stock a: 33366, stock b:39431, debt: [{'loan': 'yes', 'amount': 1893381.9318071876, 'loan_type': 1, 'repayment_date': 264}] +2025-10-24 16:05:00,012 - Stocklogger - DEBUG - cash: 852782.9985713775, stock a: 50222, stock b:42021, debt: [{'loan': 'yes', 'amount': 2242479.687765445, 'loan_type': 1, 'repayment_date': 198}] +2025-10-24 16:05:00,012 - Stocklogger - DEBUG - cash: 566597.2020124671, stock a: 54917, stock b:16593, debt: [{'loan': 'yes', 'amount': 2557668.303849497, 'loan_type': 1, 'repayment_date': 242}] +2025-10-24 16:05:00,012 - Stocklogger - DEBUG - cash: 2013155.4959101833, stock a: 25063, stock b:34226, debt: [{'loan': 'yes', 'amount': 706087.9930631475, 'loan_type': 2, 'repayment_date': 132}] +2025-10-24 16:05:00,012 - Stocklogger - DEBUG - cash: 239320.2815185075, stock a: 8944, stock b:72148, debt: [{'loan': 'yes', 'amount': 1570958.9571100213, 'loan_type': 2, 'repayment_date': 110}] +2025-10-24 16:05:00,012 - Stocklogger - DEBUG - --------Simulation Start!-------- +2025-10-24 16:05:00,012 - Stocklogger - DEBUG - --------DAY 1--------- +2025-10-24 16:05:01,594 - Stocklogger - INFO - INFO: Agent 0 decide to loan: {'loan': 'yes', 'loan_type': 1, 'amount': 2000000, 'repayment_date': 45} +2025-10-24 16:05:03,427 - Stocklogger - INFO - INFO: Agent 1 decide not to loan +2025-10-24 16:05:03,891 - Stocklogger - INFO - INFO: Agent 2 decide not to loan +2025-10-24 16:05:05,620 - Stocklogger - INFO - INFO: Agent 3 decide not to loan +2025-10-24 16:05:07,855 - Stocklogger - INFO - INFO: Agent 4 decide not to loan +2025-10-24 16:05:09,141 - Stocklogger - INFO - INFO: Agent 5 decide not to loan +2025-10-24 16:05:10,481 - Stocklogger - INFO - INFO: Agent 6 decide to loan: {'loan': 'yes', 'loan_type': 1, 'amount': 1000000, 'repayment_date': 45} +2025-10-24 16:05:11,221 - Stocklogger - INFO - INFO: Agent 7 decide not to loan +2025-10-24 16:05:12,072 - Stocklogger - INFO - INFO: Agent 8 decide not to loan +2025-10-24 16:05:12,735 - Stocklogger - INFO - INFO: Agent 9 decide not to loan +2025-10-24 16:05:13,910 - Stocklogger - INFO - INFO: Agent 10 decide to loan: {'loan': 'yes', 'loan_type': 1, 'amount': 1295780.986346907, 'repayment_date': 45} +2025-10-24 16:05:15,399 - Stocklogger - INFO - INFO: Agent 11 decide not to loan +2025-10-24 16:05:16,795 - Stocklogger - INFO - INFO: Agent 12 decide not to loan +2025-10-24 16:05:17,293 - Stocklogger - INFO - INFO: Agent 13 decide not to loan +2025-10-24 16:05:18,060 - Stocklogger - INFO - INFO: Agent 14 decide not to loan +2025-10-24 16:05:18,796 - Stocklogger - INFO - INFO: Agent 15 decide not to loan +2025-10-24 16:05:19,477 - Stocklogger - INFO - INFO: Agent 16 decide not to loan +2025-10-24 16:05:20,412 - Stocklogger - INFO - INFO: Agent 17 decide not to loan +2025-10-24 16:05:21,573 - Stocklogger - INFO - INFO: Agent 18 decide to loan: {'loan': 'yes', 'loan_type': 1, 'amount': 500000, 'repayment_date': 45} +2025-10-24 16:05:22,899 - Stocklogger - INFO - INFO: Agent 19 decide not to loan +2025-10-24 16:05:32,103 - Stocklogger - INFO - INFO: Agent 20 decide not to loan +2025-10-24 16:05:32,764 - Stocklogger - INFO - INFO: Agent 21 decide not to loan +2025-10-24 16:05:33,634 - Stocklogger - INFO - INFO: Agent 22 decide not to loan +2025-10-24 16:05:34,421 - Stocklogger - INFO - INFO: Agent 23 decide not to loan +2025-10-24 16:05:36,089 - Stocklogger - INFO - INFO: Agent 24 decide to loan: {'loan': 'yes', 'loan_type': 1, 'amount': 2000000, 'repayment_date': 45} +2025-10-24 16:05:37,820 - Stocklogger - INFO - INFO: Agent 25 decide to loan: {'loan': 'yes', 'loan_type': 0, 'amount': 1945143.8376251678, 'repayment_date': 23} +2025-10-24 16:05:38,307 - Stocklogger - INFO - INFO: Agent 26 decide not to loan +2025-10-24 16:05:39,431 - Stocklogger - INFO - INFO: Agent 27 decide not to loan +2025-10-24 16:05:39,938 - Stocklogger - INFO - INFO: Agent 28 decide not to loan +2025-10-24 16:05:40,481 - Stocklogger - INFO - INFO: Agent 29 decide not to loan +2025-10-24 16:05:41,358 - Stocklogger - INFO - INFO: Agent 30 decide not to loan +2025-10-24 16:05:41,889 - Stocklogger - INFO - INFO: Agent 31 decide not to loan +2025-10-24 16:05:42,525 - Stocklogger - INFO - INFO: Agent 32 decide not to loan +2025-10-24 16:05:42,992 - Stocklogger - INFO - INFO: Agent 33 decide not to loan +2025-10-24 16:05:43,549 - Stocklogger - INFO - INFO: Agent 34 decide not to loan +2025-10-24 16:05:44,105 - Stocklogger - INFO - INFO: Agent 35 decide not to loan +2025-10-24 16:05:44,728 - Stocklogger - INFO - INFO: Agent 36 decide not to loan +2025-10-24 16:05:45,607 - Stocklogger - INFO - INFO: Agent 37 decide not to loan +2025-10-24 16:05:46,262 - Stocklogger - INFO - INFO: Agent 38 decide not to loan +2025-10-24 16:05:47,066 - Stocklogger - INFO - INFO: Agent 39 decide not to loan +2025-10-24 16:05:47,629 - Stocklogger - INFO - INFO: Agent 40 decide not to loan +2025-10-24 16:05:48,207 - Stocklogger - INFO - INFO: Agent 41 decide not to loan +2025-10-24 16:05:49,492 - Stocklogger - INFO - INFO: Agent 42 decide not to loan +2025-10-24 16:05:50,629 - Stocklogger - INFO - INFO: Agent 43 decide not to loan +2025-10-24 16:05:51,664 - Stocklogger - INFO - INFO: Agent 44 decide to loan: {'loan': 'yes', 'loan_type': 1, 'amount': 1684637.44204241, 'repayment_date': 45} +2025-10-24 16:05:52,415 - Stocklogger - INFO - INFO: Agent 45 decide not to loan +2025-10-24 16:05:54,131 - Stocklogger - INFO - INFO: Agent 46 decide to loan: {'loan': 'yes', 'loan_type': 2, 'amount': 1797803, 'repayment_date': 67} +2025-10-24 16:05:54,844 - Stocklogger - INFO - INFO: Agent 47 decide not to loan +2025-10-24 16:05:55,477 - Stocklogger - INFO - INFO: Agent 48 decide not to loan +2025-10-24 16:05:56,101 - Stocklogger - INFO - INFO: Agent 49 decide not to loan +2025-10-24 16:05:56,101 - Stocklogger - DEBUG - SESSION 1 +2025-10-24 16:05:58,271 - Stocklogger - INFO - INFO: Agent 27 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 50, 'price': 41} +2025-10-24 16:21:48,855 - Stocklogger - DEBUG - Agents initial... +2025-10-24 16:21:48,856 - Stocklogger - DEBUG - cash: 3648438.6702980977, stock a: 7927, stock b:25249, debt: [{'loan': 'yes', 'amount': 2339455.269618133, 'loan_type': 2, 'repayment_date': 220}] +2025-10-24 16:21:48,856 - Stocklogger - DEBUG - cash: 2491604.387976668, stock a: 12651, stock b:49049, debt: [{'loan': 'yes', 'amount': 336840.712750438, 'loan_type': 0, 'repayment_date': 220}] +2025-10-24 16:21:48,856 - Stocklogger - DEBUG - cash: 310336.0863603588, stock a: 42830, stock b:81270, debt: [{'loan': 'yes', 'amount': 2907246.815329219, 'loan_type': 1, 'repayment_date': 220}] +2025-10-24 16:21:48,857 - Stocklogger - DEBUG - cash: 2810141.658942213, stock a: 32324, stock b:24221, debt: [{'loan': 'yes', 'amount': 1502468.6286202671, 'loan_type': 2, 'repayment_date': 198}] +2025-10-24 16:21:48,857 - Stocklogger - DEBUG - cash: 872798.4911796644, stock a: 33156, stock b:44472, debt: [{'loan': 'yes', 'amount': 3228154.8742705947, 'loan_type': 1, 'repayment_date': 44}] +2025-10-24 16:21:48,857 - Stocklogger - DEBUG - cash: 1032023.3582376787, stock a: 16181, stock b:18663, debt: [{'loan': 'yes', 'amount': 920800.567386914, 'loan_type': 2, 'repayment_date': 154}] +2025-10-24 16:21:48,857 - Stocklogger - DEBUG - cash: 151140.25267346067, stock a: 15388, stock b:66563, debt: [{'loan': 'yes', 'amount': 1933131.2156763892, 'loan_type': 2, 'repayment_date': 66}] +2025-10-24 16:21:48,857 - Stocklogger - DEBUG - cash: 399858.2215775881, stock a: 139294, stock b:3141, debt: [{'loan': 'yes', 'amount': 2297340.0627793334, 'loan_type': 1, 'repayment_date': 22}] +2025-10-24 16:21:48,857 - Stocklogger - DEBUG - cash: 196312.139293377, stock a: 13479, stock b:8182, debt: [{'loan': 'yes', 'amount': 308825.16406163573, 'loan_type': 2, 'repayment_date': 44}] +2025-10-24 16:21:48,857 - Stocklogger - DEBUG - cash: 2590663.772754004, stock a: 31098, stock b:26287, debt: [{'loan': 'yes', 'amount': 4133118.6706156707, 'loan_type': 2, 'repayment_date': 176}] +2025-10-24 16:21:48,857 - Stocklogger - DEBUG - cash: 592962.2762614173, stock a: 19506, stock b:22087, debt: [{'loan': 'yes', 'amount': 500284.485902725, 'loan_type': 2, 'repayment_date': 220}] +2025-10-24 16:21:48,857 - Stocklogger - DEBUG - cash: 3441190.42068338, stock a: 14931, stock b:23878, debt: [{'loan': 'yes', 'amount': 1948510.3901920759, 'loan_type': 1, 'repayment_date': 198}] +2025-10-24 16:21:48,857 - Stocklogger - DEBUG - cash: 882913.092719762, stock a: 7655, stock b:23287, debt: [{'loan': 'yes', 'amount': 420784.572522187, 'loan_type': 2, 'repayment_date': 264}] +2025-10-24 16:21:48,858 - Stocklogger - DEBUG - cash: 1610502.4305203536, stock a: 29646, stock b:58545, debt: [{'loan': 'yes', 'amount': 148506.0594122556, 'loan_type': 2, 'repayment_date': 88}] +2025-10-24 16:21:48,858 - Stocklogger - DEBUG - cash: 187695.14877910732, stock a: 12080, stock b:11251, debt: [{'loan': 'yes', 'amount': 415368.67016414844, 'loan_type': 0, 'repayment_date': 242}] +2025-10-24 16:21:48,858 - Stocklogger - DEBUG - cash: 1640436.765145225, stock a: 54522, stock b:6853, debt: [{'loan': 'yes', 'amount': 526778.9636279219, 'loan_type': 2, 'repayment_date': 88}] +2025-10-24 16:21:48,858 - Stocklogger - DEBUG - cash: 673735.5743854617, stock a: 46589, stock b:53366, debt: [{'loan': 'yes', 'amount': 879317.8164614895, 'loan_type': 2, 'repayment_date': 220}] +2025-10-24 16:21:48,858 - Stocklogger - DEBUG - cash: 2066109.6224689519, stock a: 57061, stock b:18918, debt: [{'loan': 'yes', 'amount': 164712.93942827982, 'loan_type': 1, 'repayment_date': 264}] +2025-10-24 16:21:48,858 - Stocklogger - DEBUG - cash: 728901.4168795366, stock a: 132834, stock b:3539, debt: [{'loan': 'yes', 'amount': 4217735.492015767, 'loan_type': 1, 'repayment_date': 132}] +2025-10-24 16:21:48,858 - Stocklogger - DEBUG - cash: 840343.2391352967, stock a: 116495, stock b:12319, debt: [{'loan': 'yes', 'amount': 2199356.476399629, 'loan_type': 2, 'repayment_date': 132}] +2025-10-24 16:21:48,858 - Stocklogger - DEBUG - cash: 529308.9550861946, stock a: 19020, stock b:48264, debt: [{'loan': 'yes', 'amount': 1002078.2117200516, 'loan_type': 2, 'repayment_date': 242}] +2025-10-24 16:21:48,858 - Stocklogger - DEBUG - cash: 2441595.503087133, stock a: 6420, stock b:25731, debt: [{'loan': 'yes', 'amount': 1722243.1682001343, 'loan_type': 0, 'repayment_date': 176}] +2025-10-24 16:21:48,858 - Stocklogger - DEBUG - cash: 2157760.0177429523, stock a: 14848, stock b:59487, debt: [{'loan': 'yes', 'amount': 3284108.7737011993, 'loan_type': 2, 'repayment_date': 198}] +2025-10-24 16:21:48,858 - Stocklogger - DEBUG - cash: 4536272.703214253, stock a: 1076, stock b:5384, debt: [{'loan': 'yes', 'amount': 2661951.090610586, 'loan_type': 1, 'repayment_date': 220}] +2025-10-24 16:21:48,859 - Stocklogger - DEBUG - cash: 1225726.2237234246, stock a: 25481, stock b:65286, debt: [{'loan': 'yes', 'amount': 3403786.1554816053, 'loan_type': 0, 'repayment_date': 44}] +2025-10-24 16:21:48,859 - Stocklogger - DEBUG - cash: 2819613.9206951186, stock a: 65892, stock b:1415, debt: [{'loan': 'yes', 'amount': 3173515.1055531553, 'loan_type': 0, 'repayment_date': 44}] +2025-10-24 16:21:48,859 - Stocklogger - DEBUG - cash: 305736.3637274374, stock a: 30937, stock b:71436, debt: [{'loan': 'yes', 'amount': 1702035.8926571428, 'loan_type': 0, 'repayment_date': 110}] +2025-10-24 16:21:48,859 - Stocklogger - DEBUG - cash: 759427.1273447812, stock a: 41389, stock b:11624, debt: [{'loan': 'yes', 'amount': 406234.27801465994, 'loan_type': 2, 'repayment_date': 22}] +2025-10-24 16:21:48,859 - Stocklogger - DEBUG - cash: 240343.83993055075, stock a: 71024, stock b:64700, debt: [{'loan': 'yes', 'amount': 1004790.8978687653, 'loan_type': 0, 'repayment_date': 44}] +2025-10-24 16:21:48,859 - Stocklogger - DEBUG - cash: 2845462.8827739493, stock a: 13848, stock b:33746, debt: [{'loan': 'yes', 'amount': 3548833.005783274, 'loan_type': 2, 'repayment_date': 44}] +2025-10-24 16:21:48,859 - Stocklogger - DEBUG - cash: 3513633.5715164584, stock a: 25649, stock b:11913, debt: [{'loan': 'yes', 'amount': 3680140.9141911534, 'loan_type': 1, 'repayment_date': 132}] +2025-10-24 16:21:48,859 - Stocklogger - DEBUG - cash: 1745790.165051156, stock a: 40672, stock b:37413, debt: [{'loan': 'yes', 'amount': 2754064.023816813, 'loan_type': 2, 'repayment_date': 242}] +2025-10-24 16:21:48,859 - Stocklogger - DEBUG - cash: 1626813.8696657612, stock a: 59173, stock b:19281, debt: [{'loan': 'yes', 'amount': 1932668.3015953817, 'loan_type': 1, 'repayment_date': 264}] +2025-10-24 16:21:48,859 - Stocklogger - DEBUG - cash: 1141810.6815418373, stock a: 22125, stock b:45915, debt: [{'loan': 'yes', 'amount': 3256720.3671193514, 'loan_type': 0, 'repayment_date': 22}] +2025-10-24 16:21:48,860 - Stocklogger - DEBUG - cash: 2973363.344873069, stock a: 2619, stock b:40796, debt: [{'loan': 'yes', 'amount': 3978444.099382567, 'loan_type': 0, 'repayment_date': 110}] +2025-10-24 16:21:48,860 - Stocklogger - DEBUG - cash: 1033992.5728010169, stock a: 62088, stock b:20878, debt: [{'loan': 'yes', 'amount': 531182.234272638, 'loan_type': 1, 'repayment_date': 66}] +2025-10-24 16:21:48,860 - Stocklogger - DEBUG - cash: 203070.74383971223, stock a: 32349, stock b:87539, debt: [{'loan': 'yes', 'amount': 743899.4202183868, 'loan_type': 1, 'repayment_date': 66}] +2025-10-24 16:21:48,860 - Stocklogger - DEBUG - cash: 2384071.724025017, stock a: 70640, stock b:9993, debt: [{'loan': 'yes', 'amount': 2044007.1116374375, 'loan_type': 0, 'repayment_date': 264}] +2025-10-24 16:21:48,860 - Stocklogger - DEBUG - cash: 1806695.7211890954, stock a: 50152, stock b:14318, debt: [{'loan': 'yes', 'amount': 695129.2208495408, 'loan_type': 2, 'repayment_date': 88}] +2025-10-24 16:21:48,860 - Stocklogger - DEBUG - cash: 3218385.898352533, stock a: 6676, stock b:14691, debt: [{'loan': 'yes', 'amount': 133454.30145386027, 'loan_type': 0, 'repayment_date': 198}] +2025-10-24 16:21:48,860 - Stocklogger - DEBUG - cash: 3085373.3218233055, stock a: 12185, stock b:19185, debt: [{'loan': 'yes', 'amount': 1779069.6325731308, 'loan_type': 2, 'repayment_date': 22}] +2025-10-24 16:21:48,860 - Stocklogger - DEBUG - cash: 128903.3746670426, stock a: 7422, stock b:29983, debt: [{'loan': 'yes', 'amount': 856570.4716525812, 'loan_type': 1, 'repayment_date': 132}] +2025-10-24 16:21:48,860 - Stocklogger - DEBUG - cash: 660021.2744014355, stock a: 52493, stock b:42157, debt: [{'loan': 'yes', 'amount': 2899066.090108625, 'loan_type': 0, 'repayment_date': 44}] +2025-10-24 16:21:48,860 - Stocklogger - DEBUG - cash: 45201.601457441895, stock a: 45114, stock b:72575, debt: [{'loan': 'yes', 'amount': 2780512.387305886, 'loan_type': 1, 'repayment_date': 22}] +2025-10-24 16:21:48,860 - Stocklogger - DEBUG - cash: 609648.1887012129, stock a: 21794, stock b:82294, debt: [{'loan': 'yes', 'amount': 2401315.734495465, 'loan_type': 1, 'repayment_date': 88}] +2025-10-24 16:21:48,860 - Stocklogger - DEBUG - cash: 2543137.676586133, stock a: 12078, stock b:2896, debt: [{'loan': 'yes', 'amount': 1436593.8818196545, 'loan_type': 2, 'repayment_date': 66}] +2025-10-24 16:21:48,861 - Stocklogger - DEBUG - cash: 697663.8075471892, stock a: 6031, stock b:98870, debt: [{'loan': 'yes', 'amount': 347244.6874638879, 'loan_type': 0, 'repayment_date': 242}] +2025-10-24 16:21:48,861 - Stocklogger - DEBUG - cash: 5503.703516432923, stock a: 53758, stock b:52145, debt: [{'loan': 'yes', 'amount': 3047837.8812143365, 'loan_type': 2, 'repayment_date': 242}] +2025-10-24 16:21:48,861 - Stocklogger - DEBUG - cash: 451685.2770596008, stock a: 37200, stock b:48469, debt: [{'loan': 'yes', 'amount': 2909867.667041817, 'loan_type': 0, 'repayment_date': 154}] +2025-10-24 16:21:48,861 - Stocklogger - DEBUG - cash: 1476804.8793899608, stock a: 90921, stock b:17837, debt: [{'loan': 'yes', 'amount': 4758538.943431102, 'loan_type': 0, 'repayment_date': 264}] +2025-10-24 16:21:48,861 - Stocklogger - DEBUG - --------Simulation Start!-------- +2025-10-24 16:21:48,861 - Stocklogger - DEBUG - --------DAY 1--------- +2025-10-24 16:21:49,805 - Stocklogger - INFO - INFO: Agent 0 decide not to loan +2025-10-24 16:21:50,994 - Stocklogger - INFO - INFO: Agent 1 decide to loan: {'loan': 'yes', 'loan_type': 2, 'amount': 1000000, 'repayment_date': 67} +2025-10-24 16:21:51,917 - Stocklogger - INFO - INFO: Agent 2 decide to loan: {'loan': 'yes', 'loan_type': 2, 'amount': 1938789.2710311394, 'repayment_date': 67} +2025-10-24 16:21:53,803 - Stocklogger - INFO - INFO: Agent 3 decide not to loan +2025-10-24 16:21:54,362 - Stocklogger - INFO - INFO: Agent 4 decide not to loan +2025-10-24 16:21:55,105 - Stocklogger - INFO - INFO: Agent 5 decide not to loan +2025-10-24 16:21:55,762 - Stocklogger - INFO - INFO: Agent 6 decide not to loan +2025-10-24 16:21:58,388 - Stocklogger - INFO - INFO: Agent 7 decide to loan: {'loan': 'yes', 'loan_type': 1, 'amount': 2400000, 'repayment_date': 45} +2025-10-24 16:21:59,386 - Stocklogger - INFO - INFO: Agent 8 decide not to loan +2025-10-24 16:21:59,967 - Stocklogger - INFO - INFO: Agent 9 decide not to loan +2025-10-24 16:22:01,343 - Stocklogger - INFO - INFO: Agent 10 decide to loan: {'loan': 'yes', 'loan_type': 0, 'amount': 1561337.7903586922, 'repayment_date': 23} +2025-10-24 16:22:01,898 - Stocklogger - INFO - INFO: Agent 11 decide not to loan +2025-10-24 16:22:02,665 - Stocklogger - INFO - INFO: Agent 12 decide not to loan +2025-10-24 16:22:03,685 - Stocklogger - INFO - INFO: Agent 13 decide to loan: {'loan': 'yes', 'loan_type': 1, 'amount': 100000, 'repayment_date': 45} +2025-10-24 16:22:04,251 - Stocklogger - INFO - INFO: Agent 14 decide not to loan +2025-10-24 16:22:04,925 - Stocklogger - INFO - INFO: Agent 15 decide not to loan +2025-10-24 16:22:05,385 - Stocklogger - INFO - INFO: Agent 16 decide not to loan +2025-10-24 16:22:06,257 - Stocklogger - INFO - INFO: Agent 17 decide not to loan +2025-10-24 16:22:06,796 - Stocklogger - INFO - INFO: Agent 18 decide not to loan +2025-10-24 16:22:07,969 - Stocklogger - INFO - INFO: Agent 19 decide to loan: {'loan': 'yes', 'loan_type': 2, 'amount': 2628596.762735668, 'repayment_date': 67} +2025-10-24 16:22:08,919 - Stocklogger - INFO - INFO: Agent 20 decide not to loan +2025-10-24 16:22:09,484 - Stocklogger - INFO - INFO: Agent 21 decide not to loan +2025-10-24 16:22:10,098 - Stocklogger - INFO - INFO: Agent 22 decide not to loan +2025-10-24 16:22:10,819 - Stocklogger - INFO - INFO: Agent 23 decide not to loan +2025-10-24 16:22:11,341 - Stocklogger - INFO - INFO: Agent 24 decide not to loan +2025-10-24 16:22:11,921 - Stocklogger - INFO - INFO: Agent 25 decide not to loan +2025-10-24 16:22:12,561 - Stocklogger - INFO - INFO: Agent 26 decide not to loan +2025-10-24 16:22:13,265 - Stocklogger - INFO - INFO: Agent 27 decide to loan: {'loan': 'yes', 'loan_type': 1, 'amount': 200000, 'repayment_date': 45} +2025-10-24 16:22:13,871 - Stocklogger - INFO - INFO: Agent 28 decide not to loan +2025-10-24 16:22:14,410 - Stocklogger - INFO - INFO: Agent 29 decide not to loan +2025-10-24 16:22:15,607 - Stocklogger - INFO - INFO: Agent 30 decide to loan: {'loan': 'yes', 'loan_type': 1, 'amount': 1079482.6573253046, 'repayment_date': 45} +2025-10-24 16:22:16,198 - Stocklogger - INFO - INFO: Agent 31 decide not to loan +2025-10-24 16:22:16,979 - Stocklogger - INFO - INFO: Agent 32 decide not to loan +2025-10-24 16:22:17,488 - Stocklogger - INFO - INFO: Agent 33 decide not to loan +2025-10-24 16:22:18,416 - Stocklogger - INFO - INFO: Agent 34 decide to loan: {'loan': 'yes', 'loan_type': 0, 'amount': 705329.2454905016, 'repayment_date': 23} +2025-10-24 16:22:19,223 - Stocklogger - INFO - INFO: Agent 35 decide to loan: {'loan': 'yes', 'loan_type': 0, 'amount': 3200570.338528379, 'repayment_date': 23} +2025-10-24 16:22:20,010 - Stocklogger - INFO - INFO: Agent 36 decide not to loan +2025-10-24 16:22:21,157 - Stocklogger - INFO - INFO: Agent 37 decide not to loan +2025-10-24 16:22:22,242 - Stocklogger - INFO - INFO: Agent 38 decide not to loan +2025-10-24 16:22:22,667 - Stocklogger - INFO - INFO: Agent 39 decide not to loan +2025-10-24 16:22:23,672 - Stocklogger - INFO - INFO: Agent 40 decide not to loan +2025-10-24 16:22:24,267 - Stocklogger - INFO - INFO: Agent 41 decide not to loan +2025-10-24 16:22:24,816 - Stocklogger - INFO - INFO: Agent 42 decide not to loan +2025-10-24 16:22:25,323 - Stocklogger - INFO - INFO: Agent 43 decide not to loan +2025-10-24 16:22:25,848 - Stocklogger - INFO - INFO: Agent 44 decide not to loan +2025-10-24 16:22:26,782 - Stocklogger - INFO - INFO: Agent 45 decide to loan: {'loan': 'yes', 'loan_type': 1, 'amount': 1500000, 'repayment_date': 45} +2025-10-24 16:22:27,272 - Stocklogger - INFO - INFO: Agent 46 decide not to loan +2025-10-24 16:22:27,817 - Stocklogger - INFO - INFO: Agent 47 decide not to loan +2025-10-24 16:22:28,456 - Stocklogger - INFO - INFO: Agent 48 decide not to loan +2025-10-24 16:22:29,165 - Stocklogger - INFO - INFO: Agent 49 decide not to loan +2025-10-24 16:22:29,165 - Stocklogger - DEBUG - SESSION 1 +2025-10-24 16:22:30,025 - Stocklogger - INFO - INFO: Agent 35 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 1000, 'price': 41} +2025-10-24 16:22:31,200 - Stocklogger - INFO - INFO: Agent 4 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 2000, 'price': 31} +2025-10-24 16:22:32,308 - Stocklogger - INFO - INFO: Agent 44 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 2000, 'price': 31} +2025-10-24 16:22:33,883 - Stocklogger - INFO - INFO: Agent 33 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 4000, 'price': 31.5} +2025-10-24 16:22:34,823 - Stocklogger - INFO - INFO: Agent 5 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 4000, 'price': 31.5} +2025-10-24 16:22:35,747 - Stocklogger - INFO - INFO: Agent 42 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 6000, 'price': 31.5} +2025-10-24 16:22:36,569 - Stocklogger - INFO - INFO: Agent 20 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 6000, 'price': 32} +2025-10-24 16:22:38,378 - Stocklogger - INFO - INFO: Agent 25 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 6000, 'price': 32} +2025-10-24 16:22:39,002 - Stocklogger - INFO - INFO: Agent 8 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 4000, 'price': 32} +2025-10-24 16:22:40,206 - Stocklogger - DEBUG - Sell more than hold: ```json +{"action_type":"sell", "stock":"A", "amount":4000, "price":32} +``` +2025-10-24 16:22:40,888 - Stocklogger - INFO - INFO: Agent 23 decide not to action +2025-10-24 16:22:41,717 - Stocklogger - INFO - INFO: Agent 16 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 6000, 'price': 32} +2025-10-24 16:22:42,862 - Stocklogger - INFO - INFO: Agent 38 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 6000, 'price': 32} +2025-10-24 16:22:43,966 - Stocklogger - INFO - INFO: Agent 0 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 6000, 'price': 32} +2025-10-24 16:22:45,698 - Stocklogger - INFO - INFO: Agent 31 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 6000, 'price': 32} +2025-10-24 16:22:46,740 - Stocklogger - INFO - INFO: Agent 45 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 6000, 'price': 32.5} +2025-10-24 16:22:47,584 - Stocklogger - INFO - INFO: Agent 39 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 6000, 'price': 32.5} +2025-10-24 16:22:48,514 - Stocklogger - INFO - INFO: Agent 11 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 6000, 'price': 32.5} +2025-10-24 16:22:49,525 - Stocklogger - INFO - INFO: Agent 7 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 6000, 'price': 32.5} +2025-10-24 16:22:52,176 - Stocklogger - INFO - INFO: Agent 48 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 6000, 'price': 33} +2025-10-24 16:22:53,039 - Stocklogger - INFO - INFO: Agent 30 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 6000, 'price': 33} +2025-10-24 16:22:54,543 - Stocklogger - INFO - INFO: Agent 18 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 6000, 'price': 33} +2025-10-24 16:22:55,452 - Stocklogger - INFO - INFO: Agent 2 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 6000, 'price': 33} +2025-10-24 16:22:57,789 - Stocklogger - INFO - INFO: Agent 13 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 6000, 'price': 33} +2025-10-24 16:22:59,305 - Stocklogger - INFO - INFO: Agent 6 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 6000, 'price': 32.5} +2025-10-24 16:23:00,428 - Stocklogger - INFO - INFO: Agent 49 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 6000, 'price': 33} +2025-10-24 16:23:01,805 - Stocklogger - INFO - INFO: Agent 40 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 6000, 'price': 33} +2025-10-24 16:23:03,771 - Stocklogger - INFO - INFO: Agent 32 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 6000, 'price': 32.5} +2025-10-24 16:23:04,880 - Stocklogger - INFO - INFO: Agent 9 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 6000, 'price': 33} +2025-10-24 16:23:06,001 - Stocklogger - INFO - INFO: Agent 19 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 12000, 'price': 33} +2025-10-24 16:23:07,064 - Stocklogger - INFO - INFO: Agent 43 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 6000, 'price': 33} +2025-10-24 16:23:08,048 - Stocklogger - INFO - INFO: Agent 3 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 6000, 'price': 33.5} +2025-10-24 16:23:09,197 - Stocklogger - INFO - INFO: Agent 15 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 12000, 'price': 33} +2025-10-24 16:23:10,226 - Stocklogger - INFO - INFO: Agent 10 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 12000, 'price': 33.5} +2025-10-24 16:23:11,289 - Stocklogger - INFO - INFO: Agent 37 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 12000, 'price': 33.5} +2025-10-24 16:23:12,261 - Stocklogger - INFO - INFO: Agent 21 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 6000, 'price': 33} +2025-10-24 16:23:13,838 - Stocklogger - INFO - INFO: Agent 46 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 6000, 'price': 33.5} +2025-10-24 16:23:14,730 - Stocklogger - INFO - INFO: Agent 17 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 6000, 'price': 33} +2025-10-24 16:23:15,988 - Stocklogger - INFO - INFO: Agent 36 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 6000, 'price': 33.5} +2025-10-24 16:23:17,153 - Stocklogger - INFO - INFO: Agent 12 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 6000, 'price': 33.5} +2025-10-24 16:23:17,980 - Stocklogger - INFO - INFO: Agent 29 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 6000, 'price': 33.5} +2025-10-24 16:23:19,130 - Stocklogger - INFO - INFO: Agent 27 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 6000, 'price': 33.5} +2025-10-24 16:23:20,800 - Stocklogger - INFO - INFO: Agent 41 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 6000, 'price': 33} +2025-10-24 16:23:22,718 - Stocklogger - INFO - INFO: Agent 24 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 6000, 'price': 33.5} +2025-10-24 16:23:24,064 - Stocklogger - INFO - INFO: Agent 22 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 6000, 'price': 33.5} +2025-10-24 16:23:25,463 - Stocklogger - INFO - INFO: Agent 28 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 12000, 'price': 33.5} +2025-10-24 16:23:26,669 - Stocklogger - DEBUG - Sell more than hold: ```json +{"action_type": "sell", "stock": "A", "amount": 12000, "price": 34} +``` +2025-10-24 16:23:27,497 - Stocklogger - INFO - INFO: Agent 34 decide not to action +2025-10-24 16:23:28,597 - Stocklogger - INFO - INFO: Agent 26 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 12000, 'price': 33.5} +2025-10-24 16:23:30,053 - Stocklogger - INFO - INFO: Agent 47 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 6000, 'price': 33.5} +2025-10-24 16:23:30,945 - Stocklogger - INFO - INFO: Agent 1 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 6000, 'price': 33.5} +2025-10-24 16:23:31,973 - Stocklogger - INFO - INFO: Agent 14 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 6000, 'price': 33.5} +2025-10-24 16:23:31,996 - Stocklogger - DEBUG - SESSION 2 +2025-10-24 16:23:35,763 - Stocklogger - INFO - INFO: Agent 30 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 2000, 'price': 42} +2025-10-24 16:23:38,067 - Stocklogger - INFO - INFO: Agent 7 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 3000, 'price': 42} +2025-10-24 16:23:40,107 - Stocklogger - INFO - INFO: Agent 26 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 3000, 'price': 41} +2025-10-24 16:23:42,160 - Stocklogger - INFO - INFO: Agent 25 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 8000, 'price': 33.5} +2025-10-24 16:23:43,653 - Stocklogger - INFO - INFO: Agent 20 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 3000, 'price': 41} +2025-10-24 16:23:46,461 - Stocklogger - INFO - INFO: Agent 4 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 3000, 'price': 43} +2025-10-24 16:23:48,228 - Stocklogger - INFO - INFO: Agent 15 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 2000, 'price': 43} +2025-10-24 16:23:50,694 - Stocklogger - INFO - INFO: Agent 41 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 2000, 'price': 42} +2025-10-24 16:23:55,099 - Stocklogger - DEBUG - Buy more than cash: ```json +{"action_type": "buy", "stock": "B", "amount": 1000, "price": 42} +``` +2025-10-24 16:23:58,204 - Stocklogger - INFO - INFO: Agent 47 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 100, 'price': 41} +2025-10-24 16:23:59,231 - Stocklogger - INFO - INFO: Agent 19 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 3000, 'price': 42} +2025-10-24 16:24:00,347 - Stocklogger - INFO - INFO: Agent 37 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 3000, 'price': 42} +2025-10-24 16:24:01,779 - Stocklogger - INFO - INFO: Agent 27 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 3000, 'price': 42} +2025-10-24 16:24:03,088 - Stocklogger - INFO - INFO: Agent 28 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 2000, 'price': 43} +2025-10-24 16:24:04,892 - Stocklogger - INFO - INFO: Agent 40 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 2000, 'price': 42} +2025-10-24 16:24:06,074 - Stocklogger - INFO - INFO: Agent 14 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 3000, 'price': 42} +2025-10-24 16:24:07,197 - Stocklogger - INFO - INFO: Agent 10 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 3000, 'price': 42} +2025-10-24 16:24:09,474 - Stocklogger - INFO - INFO: Agent 5 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 3000, 'price': 42} +2025-10-24 16:24:10,754 - Stocklogger - INFO - INFO: Agent 12 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 3000, 'price': 42} +2025-10-24 16:24:12,993 - Stocklogger - INFO - INFO: Agent 21 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 3000, 'price': 42} +2025-10-24 16:24:13,835 - Stocklogger - INFO - INFO: Agent 23 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 3000, 'price': 42} +2025-10-24 16:24:14,822 - Stocklogger - INFO - INFO: Agent 16 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 3000, 'price': 42} +2025-10-24 16:24:15,838 - Stocklogger - INFO - INFO: Agent 29 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 3000, 'price': 42} +2025-10-24 16:24:17,476 - Stocklogger - INFO - INFO: Agent 36 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 3000, 'price': 42} +2025-10-24 16:24:19,003 - Stocklogger - INFO - INFO: Agent 32 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 3000, 'price': 42} +2025-10-24 16:24:19,953 - Stocklogger - INFO - INFO: Agent 17 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 3000, 'price': 42} +2025-10-24 16:24:20,952 - Stocklogger - INFO - INFO: Agent 42 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 3000, 'price': 42} +2025-10-24 16:24:21,831 - Stocklogger - INFO - INFO: Agent 39 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 3000, 'price': 42} +2025-10-24 16:24:22,797 - Stocklogger - INFO - INFO: Agent 18 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 3000, 'price': 42} +2025-10-24 16:24:23,951 - Stocklogger - DEBUG - Buy more than cash: ```json +{"action_type": "buy", "stock": "B", "amount": 3000, "price": 42} +``` +2025-10-24 16:24:26,561 - Stocklogger - INFO - INFO: Agent 43 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 1000, 'price': 41} +2025-10-24 16:24:28,716 - Stocklogger - INFO - INFO: Agent 2 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 3000, 'price': 42} +2025-10-24 16:24:30,416 - Stocklogger - INFO - INFO: Agent 48 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 3000, 'price': 42} +2025-10-24 16:24:33,600 - Stocklogger - INFO - INFO: Agent 22 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 3000, 'price': 42} +2025-10-24 16:24:36,192 - Stocklogger - INFO - INFO: Agent 13 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 3000, 'price': 42} +2025-10-24 16:24:37,742 - Stocklogger - INFO - INFO: Agent 34 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 500, 'price': 33} +2025-10-24 16:24:39,116 - Stocklogger - INFO - INFO: Agent 46 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 3000, 'price': 42} +2025-10-24 16:24:40,153 - Stocklogger - INFO - INFO: Agent 38 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 2000, 'price': 42} +2025-10-24 16:24:42,585 - Stocklogger - INFO - INFO: Agent 45 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 3000, 'price': 42} +2025-10-24 16:24:44,127 - Stocklogger - INFO - INFO: Agent 1 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 3000, 'price': 42} +2025-10-24 16:24:45,749 - Stocklogger - INFO - INFO: Agent 0 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 3000, 'price': 42} +2025-10-24 16:24:46,974 - Stocklogger - INFO - INFO: Agent 8 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 3000, 'price': 42} +2025-10-24 16:24:48,049 - Stocklogger - INFO - INFO: Agent 11 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 3000, 'price': 42} +2025-10-24 16:24:49,315 - Stocklogger - INFO - INFO: Agent 33 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 3000, 'price': 43} +2025-10-24 16:24:52,261 - Stocklogger - INFO - INFO: Agent 49 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 3000, 'price': 42} +2025-10-24 16:24:53,634 - Stocklogger - INFO - INFO: Agent 31 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 3000, 'price': 42} +2025-10-24 16:24:54,788 - Stocklogger - INFO - INFO: Agent 9 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 3000, 'price': 42} +2025-10-24 16:24:55,873 - Stocklogger - INFO - INFO: Agent 44 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 3000, 'price': 42} +2025-10-24 16:24:57,117 - Stocklogger - INFO - INFO: Agent 24 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 3000, 'price': 42} +2025-10-24 16:25:00,017 - Stocklogger - INFO - INFO: Agent 3 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 3000, 'price': 42} +2025-10-24 16:25:01,152 - Stocklogger - INFO - INFO: Agent 35 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 6000, 'price': 33.5} +2025-10-24 16:25:03,717 - Stocklogger - INFO - INFO: Agent 6 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 3000, 'price': 42} +2025-10-24 16:25:03,751 - Stocklogger - DEBUG - SESSION 3 +2025-10-24 16:25:07,055 - Stocklogger - INFO - INFO: Agent 42 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 8000, 'price': 33.5} +2025-10-24 16:25:08,350 - Stocklogger - INFO - INFO: Agent 0 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 4000, 'price': 33.5} +2025-10-24 16:25:09,632 - Stocklogger - INFO - INFO: Agent 27 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 3000, 'price': 42} +2025-10-24 16:25:11,015 - Stocklogger - INFO - INFO: Agent 39 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 4000, 'price': 33.5} +2025-10-24 16:25:12,124 - Stocklogger - INFO - INFO: Agent 26 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 6000, 'price': 33.5} +2025-10-24 16:25:21,136 - Stocklogger - INFO - INFO: Agent 14 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 3000, 'price': 42} +2025-10-24 16:25:27,642 - Stocklogger - INFO - INFO: Agent 16 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 6000, 'price': 33.5} +2025-10-24 16:25:29,081 - Stocklogger - INFO - INFO: Agent 49 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 6000, 'price': 33.5} +2025-10-24 16:25:31,648 - Stocklogger - INFO - INFO: Agent 34 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 3000, 'price': 42} +2025-10-24 16:25:33,781 - Stocklogger - INFO - INFO: Agent 25 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 8000, 'price': 33.5} +2025-10-24 16:25:35,106 - Stocklogger - INFO - INFO: Agent 30 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 4000, 'price': 42} +2025-10-24 16:25:36,976 - Stocklogger - INFO - INFO: Agent 17 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 6000, 'price': 33.5} +2025-10-24 16:25:42,095 - Stocklogger - INFO - INFO: Agent 47 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 100, 'price': 41} +2025-10-24 16:25:44,089 - Stocklogger - INFO - INFO: Agent 8 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 6000, 'price': 33.5} +2025-10-24 16:25:45,882 - Stocklogger - INFO - INFO: Agent 36 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 7000, 'price': 33.5} +2025-10-24 16:25:47,779 - Stocklogger - INFO - INFO: Agent 45 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 6000, 'price': 33} +2025-10-24 16:25:49,112 - Stocklogger - INFO - INFO: Agent 46 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 3000, 'price': 42} +2025-10-24 16:25:50,385 - Stocklogger - INFO - INFO: Agent 22 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 3000, 'price': 42} +2025-10-24 16:25:51,624 - Stocklogger - INFO - INFO: Agent 3 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 3000, 'price': 42} +2025-10-24 16:25:55,413 - Stocklogger - INFO - INFO: Agent 20 decide to action: {'action_type': 'sell', 'stock': 'B', 'amount': 2000, 'price': 43} +2025-10-24 16:25:55,446 - Stocklogger - INFO - ACTION - BUY:4, SELL:20, STOCK:B, PRICE:43, AMOUNT:2000 +2025-10-24 16:25:56,939 - Stocklogger - INFO - INFO: Agent 41 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 3000, 'price': 42} +2025-10-24 16:26:00,313 - Stocklogger - INFO - INFO: Agent 9 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 8000, 'price': 33.5} +2025-10-24 16:26:01,696 - Stocklogger - INFO - INFO: Agent 7 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 6000, 'price': 33.5} +2025-10-24 16:26:03,894 - Stocklogger - INFO - INFO: Agent 10 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 3000, 'price': 42} +2025-10-24 16:26:04,985 - Stocklogger - INFO - INFO: Agent 21 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 6000, 'price': 33.5} +2025-10-24 16:26:06,868 - Stocklogger - INFO - INFO: Agent 19 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 12000, 'price': 33.5} +2025-10-24 16:26:08,842 - Stocklogger - INFO - INFO: Agent 13 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 6000, 'price': 33.5} +2025-10-24 16:26:13,125 - Stocklogger - DEBUG - Sell more than hold: {"action_type": "sell", "stock": "A", "amount": 8000, "price": 33.5} +2025-10-24 16:26:14,174 - Stocklogger - INFO - INFO: Agent 12 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 6000, 'price': 33.5} +2025-10-24 16:26:22,286 - Stocklogger - INFO - INFO: Agent 1 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 6000, 'price': 33.5} +2025-10-24 16:26:32,321 - Stocklogger - INFO - INFO: Agent 48 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 8000, 'price': 34} +2025-10-24 16:26:41,473 - Stocklogger - INFO - INFO: Agent 35 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 3000, 'price': 42} +2025-10-24 16:26:43,386 - Stocklogger - INFO - INFO: Agent 33 decide to action: {'action_type': 'sell', 'stock': 'B', 'amount': 3000, 'price': 43} +2025-10-24 16:26:43,425 - Stocklogger - INFO - ACTION - BUY:4, SELL:33, STOCK:B, PRICE:43, AMOUNT:1000 +2025-10-24 16:26:43,432 - Stocklogger - INFO - ACTION - BUY:15, SELL:33, STOCK:B, PRICE:43, AMOUNT:2000 +2025-10-24 16:26:48,942 - Stocklogger - INFO - INFO: Agent 18 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 6000, 'price': 34} +2025-10-24 16:26:53,267 - Stocklogger - INFO - INFO: Agent 11 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 6000, 'price': 34} +2025-10-24 16:26:54,368 - Stocklogger - INFO - INFO: Agent 23 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 500, 'price': 33} +2025-10-24 16:26:55,746 - Stocklogger - INFO - INFO: Agent 40 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 3000, 'price': 42} +2025-10-24 16:27:03,931 - Stocklogger - INFO - INFO: Agent 38 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 6000, 'price': 34} +2025-10-24 16:27:05,403 - Stocklogger - INFO - INFO: Agent 43 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 1000, 'price': 41} +2025-10-24 16:27:16,222 - Stocklogger - INFO - INFO: Agent 37 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 3000, 'price': 42} +2025-10-24 16:27:18,268 - Stocklogger - INFO - INFO: Agent 28 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 6000, 'price': 34} +2025-10-24 16:27:19,451 - Stocklogger - INFO - INFO: Agent 6 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 6000, 'price': 34} +2025-10-24 16:27:23,062 - Stocklogger - INFO - INFO: Agent 32 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 3000, 'price': 42} +2025-10-24 16:27:24,143 - Stocklogger - INFO - INFO: Agent 29 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 6000, 'price': 34} +2025-10-24 16:27:29,323 - Stocklogger - INFO - INFO: Agent 24 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 8000, 'price': 34} +2025-10-24 16:27:30,496 - Stocklogger - INFO - INFO: Agent 44 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 4000, 'price': 34} +2025-10-24 16:27:31,968 - Stocklogger - INFO - INFO: Agent 15 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 3000, 'price': 42} +2025-10-24 16:27:33,429 - Stocklogger - INFO - INFO: Agent 2 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 3000, 'price': 42} +2025-10-24 16:27:40,882 - Stocklogger - INFO - INFO: Agent 4 decide to action: {'action_type': 'sell', 'stock': 'B', 'amount': 3000, 'price': 44} +2025-10-24 16:27:50,746 - Stocklogger - INFO - INFO: Agent 5 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 4000, 'price': 34} +2025-10-24 16:27:52,147 - Stocklogger - INFO - INFO: Agent 31 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 3000, 'price': 42} +2025-10-24 16:27:53,638 - Stocklogger - INFO - Agent 0 tomorrow estimation: {'buy_A': 'no', 'buy_B': 'yes', 'sell_A': 'yes', 'sell_B': 'no', 'loan': 'no'} +2025-10-24 16:27:54,702 - Stocklogger - INFO - Agent 1 tomorrow estimation: {'buy_A': 'no', 'buy_B': 'yes', 'sell_A': 'yes', 'sell_B': 'no', 'loan': 'no'} +2025-10-24 16:28:00,076 - Stocklogger - INFO - Agent 2 tomorrow estimation: {'buy_A': 'no', 'buy_B': 'yes', 'sell_A': 'yes', 'sell_B': 'no', 'loan': 'no'} +2025-10-24 16:28:01,497 - Stocklogger - INFO - Agent 3 tomorrow estimation: {'buy_A': 'no', 'buy_B': 'yes', 'sell_A': 'yes', 'sell_B': 'no', 'loan': 'no'} +2025-10-24 16:28:02,867 - Stocklogger - INFO - Agent 4 tomorrow estimation: {'buy_A': 'no', 'buy_B': 'yes', 'sell_A': 'yes', 'sell_B': 'yes', 'loan': 'no'} +2025-10-24 16:28:09,291 - Stocklogger - INFO - Agent 5 tomorrow estimation: {'buy_A': 'yes', 'buy_B': 'yes', 'sell_A': 'yes', 'sell_B': 'yes', 'loan': 'no'} +2025-10-24 16:28:10,961 - Stocklogger - INFO - Agent 6 tomorrow estimation: {'buy_A': 'no', 'buy_B': 'yes', 'sell_A': 'yes', 'sell_B': 'no', 'loan': 'no'} +2025-10-24 16:28:17,505 - Stocklogger - INFO - Agent 7 tomorrow estimation: {'buy_A': 'no', 'buy_B': 'yes', 'sell_A': 'yes', 'sell_B': 'no', 'loan': 'no'} +2025-10-24 16:28:19,001 - Stocklogger - INFO - Agent 8 tomorrow estimation: {'buy_A': 'no', 'buy_B': 'yes', 'sell_A': 'yes', 'sell_B': 'no', 'loan': 'no'} +2025-10-24 16:28:20,343 - Stocklogger - INFO - Agent 9 tomorrow estimation: {'buy_A': 'no', 'buy_B': 'yes', 'sell_A': 'yes', 'sell_B': 'no', 'loan': 'no'} +2025-10-24 16:28:21,754 - Stocklogger - INFO - Agent 10 tomorrow estimation: {'buy_A': 'yes', 'buy_B': 'yes', 'sell_A': 'yes', 'sell_B': 'no', 'loan': 'yes'} +2025-10-24 16:28:31,751 - Stocklogger - INFO - Agent 11 tomorrow estimation: {'buy_A': 'no', 'buy_B': 'yes', 'sell_A': 'no', 'sell_B': 'yes', 'loan': 'no'} +2025-10-24 16:28:32,930 - Stocklogger - INFO - Agent 12 tomorrow estimation: {'buy_A': 'no', 'buy_B': 'yes', 'sell_A': 'yes', 'sell_B': 'no', 'loan': 'no'} +2025-10-24 16:28:34,799 - Stocklogger - INFO - Agent 13 tomorrow estimation: {'buy_A': 'no', 'buy_B': 'yes', 'sell_A': 'yes', 'sell_B': 'no', 'loan': 'no'} +2025-10-24 16:28:38,967 - Stocklogger - INFO - Agent 14 tomorrow estimation: {'buy_A': 'no', 'buy_B': 'yes', 'sell_A': 'yes', 'sell_B': 'no', 'loan': 'no'} +2025-10-24 16:28:44,126 - Stocklogger - INFO - Agent 15 tomorrow estimation: {'buy_A': 'no', 'buy_B': 'yes', 'sell_A': 'yes', 'sell_B': 'no', 'loan': 'no'} +2025-10-24 16:28:47,934 - Stocklogger - INFO - Agent 16 tomorrow estimation: {'buy_A': 'no', 'buy_B': 'yes', 'sell_A': 'yes', 'sell_B': 'no', 'loan': 'no'} +2025-10-24 16:28:51,738 - Stocklogger - INFO - Agent 17 tomorrow estimation: {'buy_A': 'no', 'buy_B': 'yes', 'sell_A': 'yes', 'sell_B': 'no', 'loan': 'no'} +2025-10-24 16:28:52,765 - Stocklogger - INFO - Agent 18 tomorrow estimation: {'buy_A': 'no', 'buy_B': 'yes', 'sell_A': 'yes', 'sell_B': 'no', 'loan': 'no'} +2025-10-24 16:28:53,803 - Stocklogger - INFO - Agent 19 tomorrow estimation: {'buy_A': 'no', 'buy_B': 'yes', 'sell_A': 'yes', 'sell_B': 'no', 'loan': 'no'} +2025-10-24 16:29:04,092 - Stocklogger - INFO - Agent 20 tomorrow estimation: {'buy_A': 'no', 'buy_B': 'yes', 'sell_A': 'yes', 'sell_B': 'no', 'loan': 'no'} +2025-10-24 16:29:05,086 - Stocklogger - INFO - Agent 21 tomorrow estimation: {'buy_A': 'no', 'buy_B': 'yes', 'sell_A': 'yes', 'sell_B': 'no', 'loan': 'no'} +2025-10-24 16:29:07,052 - Stocklogger - INFO - Agent 22 tomorrow estimation: {'buy_A': 'no', 'buy_B': 'yes', 'sell_A': 'yes', 'sell_B': 'no', 'loan': 'no'} +2025-10-24 16:29:08,532 - Stocklogger - INFO - Agent 23 tomorrow estimation: {'buy_A': 'no', 'buy_B': 'yes', 'sell_A': 'yes', 'sell_B': 'no', 'loan': 'no'} +2025-10-24 16:29:15,702 - Stocklogger - INFO - Agent 24 tomorrow estimation: {'buy_A': 'no', 'buy_B': 'yes', 'sell_A': 'yes', 'sell_B': 'no', 'loan': 'no'} +2025-10-24 16:29:17,658 - Stocklogger - INFO - Agent 25 tomorrow estimation: {'buy_A': 'no', 'buy_B': 'yes', 'sell_A': 'yes', 'sell_B': 'no', 'loan': 'no'} +2025-10-24 16:29:19,432 - Stocklogger - INFO - Agent 26 tomorrow estimation: {'buy_A': 'no', 'buy_B': 'yes', 'sell_A': 'yes', 'sell_B': 'no', 'loan': 'no'} +2025-10-24 16:29:20,654 - Stocklogger - INFO - Agent 27 tomorrow estimation: {'buy_A': 'no', 'buy_B': 'yes', 'sell_A': 'yes', 'sell_B': 'no', 'loan': 'no'} +2025-10-24 16:29:22,618 - Stocklogger - INFO - Agent 28 tomorrow estimation: {'buy_A': 'no', 'buy_B': 'yes', 'sell_A': 'yes', 'sell_B': 'no', 'loan': 'no'} +2025-10-24 16:29:32,491 - Stocklogger - INFO - Agent 29 tomorrow estimation: {'buy_A': 'no', 'buy_B': 'yes', 'sell_A': 'yes', 'sell_B': 'no', 'loan': 'no'} +2025-10-24 16:29:33,521 - Stocklogger - INFO - Agent 30 tomorrow estimation: {'buy_A': 'no', 'buy_B': 'yes', 'sell_A': 'yes', 'sell_B': 'no', 'loan': 'no'} +2025-10-24 16:29:34,486 - Stocklogger - INFO - Agent 31 tomorrow estimation: {'buy_A': 'no', 'buy_B': 'yes', 'sell_A': 'yes', 'sell_B': 'no', 'loan': 'no'} +2025-10-24 16:29:40,026 - Stocklogger - INFO - Agent 32 tomorrow estimation: {'buy_A': 'no', 'buy_B': 'yes', 'sell_A': 'yes', 'sell_B': 'no', 'loan': 'no'} +2025-10-24 16:29:41,181 - Stocklogger - INFO - Agent 33 tomorrow estimation: {'buy_A': 'no', 'buy_B': 'yes', 'sell_A': 'yes', 'sell_B': 'no', 'loan': 'no'} +2025-10-24 16:29:48,900 - Stocklogger - INFO - Agent 34 tomorrow estimation: {'buy_A': 'no', 'buy_B': 'yes', 'sell_A': 'yes', 'sell_B': 'no', 'loan': 'no'} +2025-10-24 16:29:50,330 - Stocklogger - INFO - Agent 35 tomorrow estimation: {'buy_A': 'yes', 'buy_B': 'yes', 'sell_A': 'yes', 'sell_B': 'no', 'loan': 'no'} +2025-10-24 16:29:51,665 - Stocklogger - INFO - Agent 36 tomorrow estimation: {'buy_A': 'no', 'buy_B': 'yes', 'sell_A': 'yes', 'sell_B': 'no', 'loan': 'no'} +2025-10-24 16:29:58,222 - Stocklogger - INFO - Agent 37 tomorrow estimation: {'buy_A': 'no', 'buy_B': 'yes', 'sell_A': 'yes', 'sell_B': 'no', 'loan': 'no'} +2025-10-24 16:30:06,064 - Stocklogger - INFO - Agent 38 tomorrow estimation: {'buy_A': 'no', 'buy_B': 'yes', 'sell_A': 'yes', 'sell_B': 'no', 'loan': 'no'} +2025-10-24 16:30:07,358 - Stocklogger - INFO - Agent 39 tomorrow estimation: {'buy_A': 'no', 'buy_B': 'yes', 'sell_A': 'yes', 'sell_B': 'no', 'loan': 'no'} +2025-10-24 16:30:08,722 - Stocklogger - INFO - Agent 40 tomorrow estimation: {'buy_A': 'no', 'buy_B': 'yes', 'sell_A': 'yes', 'sell_B': 'no', 'loan': 'no'} +2025-10-24 16:30:09,598 - Stocklogger - INFO - Agent 41 tomorrow estimation: {'buy_A': 'no', 'buy_B': 'yes', 'sell_A': 'yes', 'sell_B': 'no', 'loan': 'no'} +2025-10-24 16:30:17,318 - Stocklogger - INFO - Agent 42 tomorrow estimation: {'buy_A': 'no', 'buy_B': 'yes', 'sell_A': 'yes', 'sell_B': 'no', 'loan': 'no'} +2025-10-24 16:30:18,623 - Stocklogger - INFO - Agent 43 tomorrow estimation: {'buy_A': 'no', 'buy_B': 'yes', 'sell_A': 'yes', 'sell_B': 'no', 'loan': 'no'} +2025-10-24 16:30:20,286 - Stocklogger - INFO - Agent 44 tomorrow estimation: {'buy_A': 'no', 'buy_B': 'yes', 'sell_A': 'yes', 'sell_B': 'no', 'loan': 'no'} +2025-10-24 16:30:21,844 - Stocklogger - INFO - Agent 45 tomorrow estimation: {'buy_A': 'no', 'buy_B': 'yes', 'sell_A': 'yes', 'sell_B': 'no', 'loan': 'no'} +2025-10-24 16:30:23,701 - Stocklogger - INFO - Agent 46 tomorrow estimation: {'buy_A': 'no', 'buy_B': 'yes', 'sell_A': 'yes', 'sell_B': 'no', 'loan': 'no'} +2025-10-24 16:30:25,495 - Stocklogger - INFO - Agent 47 tomorrow estimation: {'buy_A': 'no', 'buy_B': 'yes', 'sell_A': 'yes', 'sell_B': 'no', 'loan': 'no'} +2025-10-24 16:30:30,629 - Stocklogger - INFO - Agent 48 tomorrow estimation: {'buy_A': 'no', 'buy_B': 'yes', 'sell_A': 'yes', 'sell_B': 'no', 'loan': 'no'} +2025-10-24 16:30:34,790 - Stocklogger - INFO - Agent 49 tomorrow estimation: {'buy_A': 'no', 'buy_B': 'yes', 'sell_A': 'yes', 'sell_B': 'no', 'loan': 'no'} +2025-10-24 16:30:34,810 - Stocklogger - DEBUG - DAY 1 ends, display forum messages... +2025-10-24 16:30:38,076 - Stocklogger - INFO - Agent 0 says: **Trading Tips for Today:** + +1. **Market Overview:** The market has shown significant fluctuations today, particularly in technology stocks, which may affect future price movements. Keep an eye on Company B, as it is expected to rebound with strong buying interest. + +2. **Stock A:** Despite recent sell-offs, I believe Company A remains a stable long-term investment. However, short-term traders may want to take advantage of price increase opportunities. + +3. **Stock B:** With the projected revenue growth and the recent positive corporate news, it could be a good time to accumulate shares while prices are still reasonable. + +4. **Loan Decisions:** For conservative investors, avoiding loans may provide a safer strategy during uncertain times. Focus on trading based on available cash reserves. + +5. **Actionable Strategy:** Consider short-term trades in Stock B but maintain a balanced portfolio by holding onto a portion of Stock A for stability. + +Happy trading and stay informed! +2025-10-24 16:30:51,289 - Stocklogger - INFO - Agent 1 says: **Trading Tips for Today:** + +1. **Company A**: Despite the recent sell-offs, there remains potential for recovery due to new strategic directions under the new CEO. Monitor closely for upward trends and trading volume. Consider selling selective portions to capitalize on positive movements. + +2. **Company B**: With robust growth expected and positive market sentiment, now is a good time to consider accumulating shares. The recent activities suggest increasing interest, indicating a potential bullish run. + +3. **General Strategy**: Keep an eye on market news and sentiments which may impact stock prices, especially any government policies affecting these sectors. Diversification remains key to managing risk. + +Happy Trading! 📈💼 +2025-10-24 16:30:54,664 - Stocklogger - INFO - Agent 2 says: **Trading Tips for Today:** + +1. **Company A** is showing promising price resistance at $33.5, and despite recent sell-offs, the overall trend remains stable. Consider executing sells in the upcoming sessions if it approaches resistance levels. + +2. **Company B** continues to exhibit growth potential, recommended for buying, especially with its recent price movements around $42. It is advisable to take advantage of any dips to accumulate more shares in anticipation of future gains. + +3. Keep an eye on market sentiments and news developments as they can greatly influence stock prices, particularly in volatile sectors. + +4. As for financing strategies, it might be prudent to avoid new loans unless absolutely necessary, given the current market conditions. + +Happy trading! +2025-10-24 16:30:57,629 - Stocklogger - INFO - Agent 3 says: **Trading Tips:** + +Today, we witnessed some interesting movements in both Company A and Company B's stocks. Company A has shown resilience despite a steady decline in revenue over the past few years, suggesting potential opportunities for profit on the selling side as we see upward pressure on prices. + +For Company B, the continuous demand in the tech sector despite recent fluctuations points to a robust growth potential, making it a good candidate for buying. Keep an eye on news regarding government inquiries and operational transparency, as this could affect stock performance. + +Overall, consider balancing your portfolio by selling some shares of A while investing in the growth potential of B. Happy trading! +2025-10-24 16:31:01,404 - Stocklogger - INFO - Agent 4 says: **Trading Tips for Today:** + +1. **Company A**: Despite experiencing some volatility, the overall sentiment remains cautiously optimistic. I recommend considering selling shares at higher price levels as the market is seeing increased selling activity. Monitor for any news or announcements that may influence the company's trajectory. + +2. **Company B**: With a strong buying interest and promising future growth potential, I suggest taking advantage of current opportunities to acquire more shares if the price dips below 42. Keep an eye on market reactions to ongoing government inquiries, but overall confidence in the stock seems strong. + +3. **General Advice**: Ensure to manage risk, especially in fluctuating markets, by diversifying your holdings and being aware of the latest market news. Happy trading! +2025-10-24 16:31:05,276 - Stocklogger - INFO - Agent 5 says: **Trading Tips for Today's Market:** + +1. **Stock A:** The recent sell-off pressure indicates that it might be a good time to accumulate shares at lower prices. With potential improvements in the company's outlook, consider buying if the price dips further. + +2. **Stock B:** Strong buying interest has been noted, coupled with anticipated revenue growth. The stock appears to be a strong buy for those looking to capitalize on future performance. + +3. **Loan Considerations:** While the current cash position is healthy, be mindful of market volatility. Utilizing leverage might amplify gains, but assess risks carefully. + +4. **General Advice:** Diversifying your portfolio with both established companies and promising growth stocks can mitigate risks. Keep an eye on market trends and always perform due diligence. + +Happy trading! +2025-10-24 16:31:15,949 - Stocklogger - INFO - Agent 6 says: **Trading Tips for Today:** + +1. **Company A:** While the recent sell-off presented opportunities, the overall performance trends suggest a cautious approach. It's advisable to monitor market sentiment before engaging further. + +2. **Company B:** With the expected revenue growth and strong demand, I recommend considering buying shares at the current price levels. The bullish outlook paired with government support bodes well for future gains. + +3. **Overall Market Insight:** Keep an eye on external market factors and upcoming financial reports. Staying informed will help guide your trading decisions. + +Happy trading, everyone! +2025-10-24 16:31:19,274 - Stocklogger - INFO - Agent 7 says: **Trading Tips:** + +As we conclude today's trading session, here are some key insights: + +1. **Company A**: While it has shown resilience, the upward momentum seems limited due to persistent operational challenges. Consider selling if you hold significant shares. + +2. **Company B**: A promising tech stock that is experiencing a strong demand. The recent buy orders indicate confidence in its growth potential. Now might be a good time to accumulate more shares. + +3. **Market Trends**: Keep an eye on fluctuations and trends over the next few days. Stay informed about company news and regulatory inquiries that may impact prices. + +4. **Loan Strategy**: With current market conditions, it may be prudent to avoid additional loans unless significant opportunities arise. + +Happy trading, and let’s make informed decisions together! +2025-10-24 16:31:22,338 - Stocklogger - INFO - Agent 8 says: **Trading Tips:** + +1. **Stock A**: Despite recent sell-offs, Company A appears to be stabilizing. Monitor closely for potential rebounds as new initiatives from management take shape. Consider selective selling if prices meet your targets. + +2. **Stock B**: With positive outlooks on forthcoming revenue growth and current buy interest, now may be a good time to accumulate more shares. However, due diligence is necessary due to past transparency issues. + +3. **General Market**: Keep an eye on market trends and sentiment. Economic factors may influence stock performance significantly in the upcoming sessions. + +4. **Loans**: Proceed cautiously with loans unless absolutely necessary for strategic investments. Focus on managing cash flow effectively for stability. + +Happy trading! Always assess risks before making significant moves. +2025-10-24 16:31:25,006 - Stocklogger - INFO - Agent 9 says: **Trading Tips for Today:** + +1. **Stock A**: Despite some recent selling pressure, I see potential for a rebound given the new CEO's proactive approach and efforts to explore new business avenues. Consider monitoring entry points for when the stock stabilizes. + +2. **Stock B**: With a strong growth trajectory ahead and increasing buy interest, I recommend accumulating shares. The company's commitment to transparency and societal contributions may foster positive sentiment. + +3. **Market Outlook**: With government support and operational improvements expected, a cautious but optimistic approach is warranted. Keep an eye on macroeconomic factors that could impact the tech sector. + +Happy Trading! +2025-10-24 16:31:28,829 - Stocklogger - INFO - Agent 10 says: **Trading Tips for Today:** + +1. **Stock A**: Despite recent sell-offs, there are signs of potential recovery with the new management in place. Watch for buying opportunities, especially if the price dips below 32.5. + +2. **Stock B**: Strong growth potential remains, particularly given the promising tech outlook. Consider increasing positions, especially if prices stabilize around 42. + +3. **Market Strategy**: Keeping an eye on government support initiatives can provide lucrative trading windows. Don’t hesitate to utilize loans to amplify your trading capabilities during this favorable period. + +4. **General Advice**: Monitor market reactions closely and adjust your strategies accordingly. Diversifying between A and B may help hedge against risks. + +Happy trading! +2025-10-24 16:31:31,375 - Stocklogger - INFO - Agent 11 says: **Trading Tips for Today:** + +1. **Stock A:** Despite a stable revenue trend in the past, recent sell-offs indicate that caution is warranted. Expect volatility; focus on holding rather than selling unless the price surpasses recent highs. + +2. **Stock B:** The growth potential looks promising, especially considering the expected revenue growth rate resurgence. Buyers may want to consider accumulating shares as the tech market is showing signs of recovery. + +3. **General Advice:** Before making any transactions, assess market sentiment and be wary of overleveraging. It's crucial to stay informed about the company's operational outlook and any potential regulatory impacts. + +**Happy Trading!** +2025-10-24 16:31:39,531 - Stocklogger - INFO - Agent 12 says: **Trading Tips for Today:** + +1. **Stock A Performance:** Despite some volatility, Stock A showed resilience with potential for profit-taking at higher price levels. Keep an eye on market trends and consider selling if prices rise further. + +2. **Stock B Opportunities:** Stock B remains a solid buy opportunity due to expected growth in the tech sector. With plans for expansion and increasing demand, it's wise to accumulate shares while prices are still favorable. + +3. **Market Sentiment:** Traders should stay informed about developments related to both companies, particularly on government policies and subsidies that could impact stock performance. + +4. **Loan Considerations:** Given current market conditions, maintaining cash reserves without new loans is advisable. Focus on strategic buying and selling based on market movements. + +Stay vigilant and happy trading! +2025-10-24 16:31:42,105 - Stocklogger - INFO - Agent 13 says: **Trading Tips:** + +"As we close today's trading session, keep an eye on the growth potential of Company B. Despite some recent scrutiny, its revenue growth trajectory remains promising, especially with expected growth rates surpassing 20%. Meanwhile, Company A, with a stable but uncertain outlook due to past performance, offers selling opportunities if the price continues to rise. Always consider your risk tolerance when deciding to buy or sell. Stay informed and happy trading!" +2025-10-24 16:31:46,208 - Stocklogger - INFO - Agent 14 says: **Trading Tips:** + +1. **Focus on Company B:** Given the current growth trajectory and positive outlook, consider buying shares of Company B. The stock has shown resilience and the potential for significant gains as market conditions improve. + +2. **Watch Company A:** While Company A has a stable operational outlook due to recent management changes, the overall revenue trend is declining. Short-term selling opportunities may present themselves, especially if prices rise from the current levels. + +3. **No Loans Needed:** The current cash reserves are sufficient for trading activities. Avoid taking on additional debt unless a compelling investment opportunity arises. + +4. **Stay Informed:** Keep an eye on market trends and news related to both companies, especially any governmental actions or financial reports that may influence stock prices. + +Let’s stay proactive and informed in our trading strategies! +2025-10-24 17:28:00,273 - Stocklogger - DEBUG - Agents initial... +2025-10-24 17:28:00,273 - Stocklogger - DEBUG - cash: 2971416.381922153, stock a: 27269, stock b:13284, debt: [{'loan': 'yes', 'amount': 418350.6010166338, 'loan_type': 2, 'repayment_date': 66}] +2025-10-24 17:28:00,273 - Stocklogger - DEBUG - cash: 257791.36671634228, stock a: 43389, stock b:83984, debt: [{'loan': 'yes', 'amount': 4461073.2824422745, 'loan_type': 1, 'repayment_date': 198}] +2025-10-24 17:28:00,273 - Stocklogger - DEBUG - cash: 2119670.217033741, stock a: 54871, stock b:17962, debt: [{'loan': 'yes', 'amount': 1259273.963112424, 'loan_type': 0, 'repayment_date': 264}] +2025-10-24 17:28:00,273 - Stocklogger - DEBUG - cash: 1559981.0359118015, stock a: 56302, stock b:31923, debt: [{'loan': 'yes', 'amount': 35730.35012764625, 'loan_type': 1, 'repayment_date': 22}] +2025-10-24 17:28:00,274 - Stocklogger - DEBUG - cash: 243348.3725143637, stock a: 23236, stock b:50955, debt: [{'loan': 'yes', 'amount': 1323771.0441515504, 'loan_type': 2, 'repayment_date': 198}] +2025-10-24 17:28:00,274 - Stocklogger - DEBUG - cash: 2352484.226942778, stock a: 830, stock b:56428, debt: [{'loan': 'yes', 'amount': 2710966.1366604636, 'loan_type': 0, 'repayment_date': 154}] +2025-10-24 17:28:00,274 - Stocklogger - DEBUG - cash: 1255679.385476886, stock a: 2485, stock b:88355, debt: [{'loan': 'yes', 'amount': 1038237.1430997817, 'loan_type': 0, 'repayment_date': 242}] +2025-10-24 17:28:00,274 - Stocklogger - DEBUG - cash: 241133.65770470686, stock a: 29604, stock b:58027, debt: [{'loan': 'yes', 'amount': 112831.95242583477, 'loan_type': 2, 'repayment_date': 110}] +2025-10-24 17:28:00,274 - Stocklogger - DEBUG - cash: 462197.41808303236, stock a: 44410, stock b:34709, debt: [{'loan': 'yes', 'amount': 1750828.648905275, 'loan_type': 2, 'repayment_date': 132}] +2025-10-24 17:28:00,274 - Stocklogger - DEBUG - cash: 152939.41868231297, stock a: 64888, stock b:57260, debt: [{'loan': 'yes', 'amount': 3939112.824741554, 'loan_type': 1, 'repayment_date': 132}] +2025-10-24 17:28:00,274 - Stocklogger - DEBUG - cash: 1466126.0716949708, stock a: 32892, stock b:16179, debt: [{'loan': 'yes', 'amount': 1456093.6871134073, 'loan_type': 0, 'repayment_date': 44}] +2025-10-24 17:28:00,274 - Stocklogger - DEBUG - cash: 2229968.4760598154, stock a: 15589, stock b:8350, debt: [{'loan': 'yes', 'amount': 1808389.4557595032, 'loan_type': 0, 'repayment_date': 264}] +2025-10-24 17:28:00,274 - Stocklogger - DEBUG - cash: 3938525.8176220306, stock a: 11966, stock b:4887, debt: [{'loan': 'yes', 'amount': 248891.0131130345, 'loan_type': 2, 'repayment_date': 88}] +2025-10-24 17:28:00,274 - Stocklogger - DEBUG - cash: 1188859.1052084928, stock a: 97937, stock b:16777, debt: [{'loan': 'yes', 'amount': 1098144.035233966, 'loan_type': 2, 'repayment_date': 242}] +2025-10-24 17:28:00,275 - Stocklogger - DEBUG - cash: 652031.2734327477, stock a: 96405, stock b:15740, debt: [{'loan': 'yes', 'amount': 3414646.0151750525, 'loan_type': 0, 'repayment_date': 44}] +2025-10-24 17:28:00,275 - Stocklogger - DEBUG - cash: 1738823.5547042452, stock a: 10437, stock b:61906, debt: [{'loan': 'yes', 'amount': 1289523.6492951545, 'loan_type': 2, 'repayment_date': 44}] +2025-10-24 17:28:00,275 - Stocklogger - DEBUG - cash: 919811.0479914246, stock a: 30585, stock b:44461, debt: [{'loan': 'yes', 'amount': 754132.6128214742, 'loan_type': 2, 'repayment_date': 264}] +2025-10-24 17:28:00,275 - Stocklogger - DEBUG - cash: 2229080.606079838, stock a: 23452, stock b:42523, debt: [{'loan': 'yes', 'amount': 1313419.3016837297, 'loan_type': 1, 'repayment_date': 198}] +2025-10-24 17:28:00,275 - Stocklogger - DEBUG - cash: 2678723.268656016, stock a: 5574, stock b:22278, debt: [{'loan': 'yes', 'amount': 2433082.229863293, 'loan_type': 1, 'repayment_date': 154}] +2025-10-24 17:28:00,275 - Stocklogger - DEBUG - cash: 2482121.711450139, stock a: 65751, stock b:3816, debt: [{'loan': 'yes', 'amount': 1795420.3488654646, 'loan_type': 0, 'repayment_date': 198}] +2025-10-24 17:28:00,275 - Stocklogger - DEBUG - cash: 585833.5573255308, stock a: 14976, stock b:73493, debt: [{'loan': 'yes', 'amount': 1910465.7881913423, 'loan_type': 1, 'repayment_date': 220}] +2025-10-24 17:28:00,275 - Stocklogger - DEBUG - cash: 1146648.3040369856, stock a: 32309, stock b:49532, debt: [{'loan': 'yes', 'amount': 1190141.8156267186, 'loan_type': 1, 'repayment_date': 154}] +2025-10-24 17:28:00,275 - Stocklogger - DEBUG - cash: 1255043.7261302595, stock a: 89769, stock b:8644, debt: [{'loan': 'yes', 'amount': 1354954.289722818, 'loan_type': 2, 'repayment_date': 264}] +2025-10-24 17:28:00,275 - Stocklogger - DEBUG - cash: 1551323.009407985, stock a: 104396, stock b:4381, debt: [{'loan': 'yes', 'amount': 3598203.6810443457, 'loan_type': 2, 'repayment_date': 44}] +2025-10-24 17:28:00,275 - Stocklogger - DEBUG - cash: 595864.8431036089, stock a: 32839, stock b:22713, debt: [{'loan': 'yes', 'amount': 1112805.5407413524, 'loan_type': 1, 'repayment_date': 22}] +2025-10-24 17:28:00,276 - Stocklogger - DEBUG - cash: 312986.32885195245, stock a: 19451, stock b:12877, debt: [{'loan': 'yes', 'amount': 1040843.3329357913, 'loan_type': 0, 'repayment_date': 22}] +2025-10-24 17:28:00,276 - Stocklogger - DEBUG - cash: 1936757.3630143087, stock a: 32639, stock b:9012, debt: [{'loan': 'yes', 'amount': 1103925.6194409796, 'loan_type': 2, 'repayment_date': 110}] +2025-10-24 17:28:00,276 - Stocklogger - DEBUG - cash: 735428.4418644647, stock a: 3520, stock b:95952, debt: [{'loan': 'yes', 'amount': 479112.3071269027, 'loan_type': 2, 'repayment_date': 198}] +2025-10-24 17:28:00,276 - Stocklogger - DEBUG - cash: 786887.2218549106, stock a: 94454, stock b:31938, debt: [{'loan': 'yes', 'amount': 3796521.4959727176, 'loan_type': 0, 'repayment_date': 22}] +2025-10-24 17:28:00,276 - Stocklogger - DEBUG - cash: 123726.71322173289, stock a: 26756, stock b:74581, debt: [{'loan': 'yes', 'amount': 692623.4970890455, 'loan_type': 1, 'repayment_date': 264}] +2025-10-24 17:28:00,276 - Stocklogger - DEBUG - cash: 276327.84603076, stock a: 29382, stock b:32501, debt: [{'loan': 'yes', 'amount': 1845114.9156081525, 'loan_type': 1, 'repayment_date': 198}] +2025-10-24 17:28:00,276 - Stocklogger - DEBUG - cash: 1328996.5592399922, stock a: 10218, stock b:27398, debt: [{'loan': 'yes', 'amount': 1422009.104208719, 'loan_type': 2, 'repayment_date': 242}] +2025-10-24 17:28:00,276 - Stocklogger - DEBUG - cash: 43683.85666781549, stock a: 9675, stock b:37786, debt: [{'loan': 'yes', 'amount': 1838166.416922763, 'loan_type': 2, 'repayment_date': 88}] +2025-10-24 17:28:00,328 - Stocklogger - DEBUG - cash: 435227.25621618517, stock a: 94854, stock b:2499, debt: [{'loan': 'yes', 'amount': 661509.6339606197, 'loan_type': 0, 'repayment_date': 198}] +2025-10-24 17:28:00,328 - Stocklogger - DEBUG - cash: 941400.0276917383, stock a: 69330, stock b:13601, debt: [{'loan': 'yes', 'amount': 2434419.9684983892, 'loan_type': 2, 'repayment_date': 22}] +2025-10-24 17:28:00,328 - Stocklogger - DEBUG - cash: 339381.250134978, stock a: 141935, stock b:8753, debt: [{'loan': 'yes', 'amount': 1885068.1177880906, 'loan_type': 1, 'repayment_date': 242}] +2025-10-24 17:28:00,328 - Stocklogger - DEBUG - cash: 1192354.6996584504, stock a: 79224, stock b:22062, debt: [{'loan': 'yes', 'amount': 819621.8814971035, 'loan_type': 2, 'repayment_date': 220}] +2025-10-24 17:28:00,328 - Stocklogger - DEBUG - cash: 4173173.2436399255, stock a: 9886, stock b:9107, debt: [{'loan': 'yes', 'amount': 2989290.5441342187, 'loan_type': 2, 'repayment_date': 44}] +2025-10-24 17:28:00,329 - Stocklogger - DEBUG - cash: 2161447.813836956, stock a: 18426, stock b:28131, debt: [{'loan': 'yes', 'amount': 307632.49958768446, 'loan_type': 0, 'repayment_date': 132}] +2025-10-24 17:28:00,329 - Stocklogger - DEBUG - cash: 3953149.586093615, stock a: 4441, stock b:4609, debt: [{'loan': 'yes', 'amount': 717925.6941291612, 'loan_type': 2, 'repayment_date': 88}] +2025-10-24 17:28:00,329 - Stocklogger - DEBUG - cash: 139035.07363418277, stock a: 69949, stock b:63491, debt: [{'loan': 'yes', 'amount': 3081864.817790975, 'loan_type': 0, 'repayment_date': 264}] +2025-10-24 17:28:00,329 - Stocklogger - DEBUG - cash: 259575.74896684045, stock a: 76317, stock b:38715, debt: [{'loan': 'yes', 'amount': 400415.5312823088, 'loan_type': 1, 'repayment_date': 154}] +2025-10-24 17:28:00,329 - Stocklogger - DEBUG - cash: 459759.7375704143, stock a: 45124, stock b:65966, debt: [{'loan': 'yes', 'amount': 2554198.062232491, 'loan_type': 2, 'repayment_date': 66}] +2025-10-24 17:28:00,329 - Stocklogger - DEBUG - cash: 1950088.5582819432, stock a: 43213, stock b:11459, debt: [{'loan': 'yes', 'amount': 692917.3167105517, 'loan_type': 1, 'repayment_date': 176}] +2025-10-24 17:28:00,329 - Stocklogger - DEBUG - cash: 1917454.3565207392, stock a: 24237, stock b:45100, debt: [{'loan': 'yes', 'amount': 1570910.133182448, 'loan_type': 0, 'repayment_date': 132}] +2025-10-24 17:28:00,329 - Stocklogger - DEBUG - cash: 1760846.4293656957, stock a: 34717, stock b:35982, debt: [{'loan': 'yes', 'amount': 1519077.0219426875, 'loan_type': 0, 'repayment_date': 88}] +2025-10-24 17:28:00,329 - Stocklogger - DEBUG - cash: 592583.5485640713, stock a: 49186, stock b:27368, debt: [{'loan': 'yes', 'amount': 2804796.8000175115, 'loan_type': 2, 'repayment_date': 44}] +2025-10-24 17:28:00,329 - Stocklogger - DEBUG - cash: 467124.7001672474, stock a: 105501, stock b:858, debt: [{'loan': 'yes', 'amount': 1481456.1450906594, 'loan_type': 0, 'repayment_date': 22}] +2025-10-24 17:28:00,329 - Stocklogger - DEBUG - cash: 4126498.0328997974, stock a: 9749, stock b:11970, debt: [{'loan': 'yes', 'amount': 1553133.3923160657, 'loan_type': 1, 'repayment_date': 176}] +2025-10-24 17:28:00,330 - Stocklogger - DEBUG - cash: 620679.8254100487, stock a: 40465, stock b:14890, debt: [{'loan': 'yes', 'amount': 1536372.450772282, 'loan_type': 2, 'repayment_date': 132}] +2025-10-24 17:28:00,330 - Stocklogger - DEBUG - --------Simulation Start!-------- +2025-10-24 17:28:00,330 - Stocklogger - DEBUG - --------DAY 1--------- +2025-10-24 17:28:01,437 - Stocklogger - INFO - INFO: Agent 0 decide not to loan +2025-10-24 17:28:01,933 - Stocklogger - INFO - INFO: Agent 1 decide not to loan +2025-10-24 17:28:03,597 - Stocklogger - INFO - INFO: Agent 2 decide not to loan +2025-10-24 17:28:04,751 - Stocklogger - INFO - INFO: Agent 3 decide to loan: {'loan': 'yes', 'loan_type': 1, 'amount': 200000, 'repayment_date': 45} +2025-10-24 17:28:05,292 - Stocklogger - INFO - INFO: Agent 4 decide not to loan +2025-10-24 17:28:06,040 - Stocklogger - INFO - INFO: Agent 5 decide not to loan +2025-10-24 17:28:07,592 - Stocklogger - INFO - INFO: Agent 6 decide not to loan +2025-10-24 17:28:08,046 - Stocklogger - INFO - INFO: Agent 7 decide not to loan +2025-10-24 17:32:37,706 - Stocklogger - INFO - 🤖 Initializing LLM agents... +2025-10-24 17:32:37,706 - Stocklogger - INFO - 🤖 Initializing LLM agents... +2025-10-24 17:32:38,436 - Stocklogger - INFO - INFO: Agent 0 decide to loan: {'loan': 'yes', 'loan_type': 1, 'amount': 200000, 'repayment_date': 45} +2025-10-24 17:32:38,436 - Stocklogger - INFO - INFO: Agent 0 decide to loan: {'loan': 'yes', 'loan_type': 1, 'amount': 200000, 'repayment_date': 45} +2025-10-24 17:32:38,962 - Stocklogger - INFO - INFO: Agent 1 decide not to loan +2025-10-24 17:32:38,962 - Stocklogger - INFO - INFO: Agent 1 decide not to loan +2025-10-24 17:32:39,505 - Stocklogger - INFO - INFO: Agent 2 decide not to loan +2025-10-24 17:32:39,505 - Stocklogger - INFO - INFO: Agent 2 decide not to loan +2025-10-24 17:32:41,961 - Stocklogger - INFO - INFO: Agent 3 decide to loan: {'loan': 'yes', 'loan_type': 2, 'amount': 3000000, 'repayment_date': 67} +2025-10-24 17:32:41,961 - Stocklogger - INFO - INFO: Agent 3 decide to loan: {'loan': 'yes', 'loan_type': 2, 'amount': 3000000, 'repayment_date': 67} +2025-10-24 17:32:44,465 - Stocklogger - INFO - INFO: Agent 4 decide not to loan +2025-10-24 17:32:44,465 - Stocklogger - INFO - INFO: Agent 4 decide not to loan +2025-10-24 17:32:45,746 - Stocklogger - INFO - INFO: Agent 5 decide not to loan +2025-10-24 17:32:45,746 - Stocklogger - INFO - INFO: Agent 5 decide not to loan +2025-10-24 17:32:46,550 - Stocklogger - INFO - INFO: Agent 6 decide not to loan +2025-10-24 17:32:46,550 - Stocklogger - INFO - INFO: Agent 6 decide not to loan +2025-10-24 17:32:47,534 - Stocklogger - INFO - INFO: Agent 7 decide to loan: {'loan': 'yes', 'loan_type': 1, 'amount': 1176535.821773331, 'repayment_date': 45} +2025-10-24 17:32:47,534 - Stocklogger - INFO - INFO: Agent 7 decide to loan: {'loan': 'yes', 'loan_type': 1, 'amount': 1176535.821773331, 'repayment_date': 45} +2025-10-24 17:32:48,346 - Stocklogger - INFO - INFO: Agent 8 decide not to loan +2025-10-24 17:32:48,346 - Stocklogger - INFO - INFO: Agent 8 decide not to loan +2025-10-24 17:32:49,017 - Stocklogger - INFO - INFO: Agent 9 decide not to loan +2025-10-24 17:32:49,017 - Stocklogger - INFO - INFO: Agent 9 decide not to loan +2025-10-24 17:32:49,896 - Stocklogger - INFO - INFO: Agent 10 decide to loan: {'loan': 'yes', 'loan_type': 1, 'amount': 1292358.8295984368, 'repayment_date': 45} +2025-10-24 17:32:49,896 - Stocklogger - INFO - INFO: Agent 10 decide to loan: {'loan': 'yes', 'loan_type': 1, 'amount': 1292358.8295984368, 'repayment_date': 45} +2025-10-24 17:32:50,665 - Stocklogger - INFO - INFO: Agent 11 decide not to loan +2025-10-24 17:32:50,665 - Stocklogger - INFO - INFO: Agent 11 decide not to loan +2025-10-24 17:32:51,134 - Stocklogger - INFO - INFO: Agent 12 decide not to loan +2025-10-24 17:32:51,134 - Stocklogger - INFO - INFO: Agent 12 decide not to loan +2025-10-24 17:32:51,654 - Stocklogger - INFO - INFO: Agent 13 decide not to loan +2025-10-24 17:32:51,654 - Stocklogger - INFO - INFO: Agent 13 decide not to loan +2025-10-24 17:32:52,433 - Stocklogger - INFO - INFO: Agent 14 decide not to loan +2025-10-24 17:32:52,433 - Stocklogger - INFO - INFO: Agent 14 decide not to loan +2025-10-24 17:32:53,114 - Stocklogger - INFO - INFO: Agent 15 decide not to loan +2025-10-24 17:32:53,114 - Stocklogger - INFO - INFO: Agent 15 decide not to loan +2025-10-24 17:32:53,787 - Stocklogger - INFO - INFO: Agent 16 decide not to loan +2025-10-24 17:32:53,787 - Stocklogger - INFO - INFO: Agent 16 decide not to loan +2025-10-24 17:32:54,436 - Stocklogger - INFO - INFO: Agent 17 decide not to loan +2025-10-24 17:32:54,436 - Stocklogger - INFO - INFO: Agent 17 decide not to loan +2025-10-24 17:32:54,983 - Stocklogger - INFO - INFO: Agent 18 decide not to loan +2025-10-24 17:32:54,983 - Stocklogger - INFO - INFO: Agent 18 decide not to loan +2025-10-24 17:32:56,677 - Stocklogger - INFO - INFO: Agent 19 decide not to loan +2025-10-24 17:32:56,677 - Stocklogger - INFO - INFO: Agent 19 decide not to loan +2025-10-24 17:32:57,687 - Stocklogger - INFO - INFO: Agent 20 decide to loan: {'loan': 'yes', 'loan_type': 2, 'amount': 464514.6622505295, 'repayment_date': 67} +2025-10-24 17:32:57,687 - Stocklogger - INFO - INFO: Agent 20 decide to loan: {'loan': 'yes', 'loan_type': 2, 'amount': 464514.6622505295, 'repayment_date': 67} +2025-10-24 17:32:58,344 - Stocklogger - INFO - INFO: Agent 21 decide not to loan +2025-10-24 17:32:58,344 - Stocklogger - INFO - INFO: Agent 21 decide not to loan +2025-10-24 17:32:59,500 - Stocklogger - INFO - INFO: Agent 22 decide not to loan +2025-10-24 17:32:59,500 - Stocklogger - INFO - INFO: Agent 22 decide not to loan +2025-10-24 17:33:00,447 - Stocklogger - INFO - INFO: Agent 23 decide to loan: {'loan': 'yes', 'loan_type': 1, 'amount': 2333821, 'repayment_date': 45} +2025-10-24 17:33:00,447 - Stocklogger - INFO - INFO: Agent 23 decide to loan: {'loan': 'yes', 'loan_type': 1, 'amount': 2333821, 'repayment_date': 45} +2025-10-24 17:33:01,279 - Stocklogger - INFO - INFO: Agent 24 decide not to loan +2025-10-24 17:33:01,279 - Stocklogger - INFO - INFO: Agent 24 decide not to loan +2025-10-24 17:33:01,933 - Stocklogger - INFO - INFO: Agent 25 decide not to loan +2025-10-24 17:33:01,933 - Stocklogger - INFO - INFO: Agent 25 decide not to loan +2025-10-24 17:33:02,595 - Stocklogger - INFO - INFO: Agent 26 decide not to loan +2025-10-24 17:33:02,595 - Stocklogger - INFO - INFO: Agent 26 decide not to loan +2025-10-24 17:33:03,069 - Stocklogger - INFO - INFO: Agent 27 decide not to loan +2025-10-24 17:33:03,069 - Stocklogger - INFO - INFO: Agent 27 decide not to loan +2025-10-24 17:33:03,815 - Stocklogger - INFO - INFO: Agent 28 decide not to loan +2025-10-24 17:33:03,815 - Stocklogger - INFO - INFO: Agent 28 decide not to loan +2025-10-24 17:33:04,215 - Stocklogger - INFO - INFO: Agent 29 decide not to loan +2025-10-24 17:33:04,215 - Stocklogger - INFO - INFO: Agent 29 decide not to loan +2025-10-24 17:33:05,931 - Stocklogger - INFO - INFO: Agent 30 decide to loan: {'loan': 'yes', 'loan_type': 0, 'amount': 2806811.511804586, 'repayment_date': 23} +2025-10-24 17:33:05,931 - Stocklogger - INFO - INFO: Agent 30 decide to loan: {'loan': 'yes', 'loan_type': 0, 'amount': 2806811.511804586, 'repayment_date': 23} +2025-10-24 17:33:06,461 - Stocklogger - INFO - INFO: Agent 31 decide not to loan +2025-10-24 17:33:06,461 - Stocklogger - INFO - INFO: Agent 31 decide not to loan +2025-10-24 17:33:07,848 - Stocklogger - INFO - INFO: Agent 32 decide to loan: {'loan': 'yes', 'loan_type': 1, 'amount': 1500000, 'repayment_date': 45} +2025-10-24 17:33:07,848 - Stocklogger - INFO - INFO: Agent 32 decide to loan: {'loan': 'yes', 'loan_type': 1, 'amount': 1500000, 'repayment_date': 45} +2025-10-24 17:33:09,159 - Stocklogger - INFO - INFO: Agent 33 decide not to loan +2025-10-24 17:33:09,159 - Stocklogger - INFO - INFO: Agent 33 decide not to loan +2025-10-24 17:33:09,912 - Stocklogger - INFO - INFO: Agent 34 decide not to loan +2025-10-24 17:33:09,912 - Stocklogger - INFO - INFO: Agent 34 decide not to loan +2025-10-24 17:33:10,575 - Stocklogger - INFO - INFO: Agent 35 decide not to loan +2025-10-24 17:33:10,575 - Stocklogger - INFO - INFO: Agent 35 decide not to loan +2025-10-24 17:33:11,287 - Stocklogger - INFO - INFO: Agent 36 decide not to loan +2025-10-24 17:33:11,287 - Stocklogger - INFO - INFO: Agent 36 decide not to loan +2025-10-24 17:33:11,833 - Stocklogger - INFO - INFO: Agent 37 decide not to loan +2025-10-24 17:33:11,833 - Stocklogger - INFO - INFO: Agent 37 decide not to loan +2025-10-24 17:33:12,302 - Stocklogger - INFO - INFO: Agent 38 decide not to loan +2025-10-24 17:33:12,302 - Stocklogger - INFO - INFO: Agent 38 decide not to loan +2025-10-24 17:33:13,101 - Stocklogger - INFO - INFO: Agent 39 decide not to loan +2025-10-24 17:33:13,101 - Stocklogger - INFO - INFO: Agent 39 decide not to loan +2025-10-24 17:33:14,109 - Stocklogger - INFO - INFO: Agent 40 decide to loan: {'loan': 'yes', 'loan_type': 1, 'amount': 1800000, 'repayment_date': 45} +2025-10-24 17:33:14,109 - Stocklogger - INFO - INFO: Agent 40 decide to loan: {'loan': 'yes', 'loan_type': 1, 'amount': 1800000, 'repayment_date': 45} +2025-10-24 17:33:15,215 - Stocklogger - INFO - INFO: Agent 41 decide not to loan +2025-10-24 17:33:15,215 - Stocklogger - INFO - INFO: Agent 41 decide not to loan +2025-10-24 17:33:15,949 - Stocklogger - INFO - INFO: Agent 42 decide not to loan +2025-10-24 17:33:15,949 - Stocklogger - INFO - INFO: Agent 42 decide not to loan +2025-10-24 17:33:16,973 - Stocklogger - INFO - INFO: Agent 43 decide to loan: {'loan': 'yes', 'loan_type': 1, 'amount': 2304505.0200585444, 'repayment_date': 45} +2025-10-24 17:33:16,973 - Stocklogger - INFO - INFO: Agent 43 decide to loan: {'loan': 'yes', 'loan_type': 1, 'amount': 2304505.0200585444, 'repayment_date': 45} +2025-10-24 17:33:17,521 - Stocklogger - INFO - INFO: Agent 44 decide not to loan +2025-10-24 17:33:17,521 - Stocklogger - INFO - INFO: Agent 44 decide not to loan +2025-10-24 17:33:18,154 - Stocklogger - INFO - INFO: Agent 45 decide not to loan +2025-10-24 17:33:18,154 - Stocklogger - INFO - INFO: Agent 45 decide not to loan +2025-10-24 17:33:18,828 - Stocklogger - INFO - INFO: Agent 46 decide not to loan +2025-10-24 17:33:18,828 - Stocklogger - INFO - INFO: Agent 46 decide not to loan +2025-10-24 17:33:19,336 - Stocklogger - INFO - INFO: Agent 47 decide not to loan +2025-10-24 17:33:19,336 - Stocklogger - INFO - INFO: Agent 47 decide not to loan +2025-10-24 17:33:20,299 - Stocklogger - INFO - INFO: Agent 48 decide to loan: {'loan': 'yes', 'loan_type': 1, 'amount': 200000, 'repayment_date': 45} +2025-10-24 17:33:20,299 - Stocklogger - INFO - INFO: Agent 48 decide to loan: {'loan': 'yes', 'loan_type': 1, 'amount': 200000, 'repayment_date': 45} +2025-10-24 17:33:20,802 - Stocklogger - INFO - INFO: Agent 49 decide not to loan +2025-10-24 17:33:20,802 - Stocklogger - INFO - INFO: Agent 49 decide not to loan +2025-10-24 17:33:21,798 - Stocklogger - INFO - INFO: Agent 44 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 50, 'price': 41.0} +2025-10-24 17:33:21,798 - Stocklogger - INFO - INFO: Agent 44 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 50, 'price': 41.0} +2025-10-24 17:33:23,734 - Stocklogger - INFO - INFO: Agent 46 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 1000, 'price': 30.5} +2025-10-24 17:33:23,734 - Stocklogger - INFO - INFO: Agent 46 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 1000, 'price': 30.5} +2025-10-24 17:33:24,847 - Stocklogger - INFO - INFO: Agent 0 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 5000, 'price': 31.0} +2025-10-24 17:33:24,847 - Stocklogger - INFO - INFO: Agent 0 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 5000, 'price': 31.0} +2025-10-24 17:33:26,044 - Stocklogger - INFO - INFO: Agent 15 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 5000, 'price': 31.0} +2025-10-24 17:33:26,044 - Stocklogger - INFO - INFO: Agent 15 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 5000, 'price': 31.0} +2025-10-24 17:33:27,101 - Stocklogger - INFO - INFO: Agent 5 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 5000, 'price': 31.0} +2025-10-24 17:33:27,101 - Stocklogger - INFO - INFO: Agent 5 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 5000, 'price': 31.0} +2025-10-24 17:33:28,365 - Stocklogger - INFO - INFO: Agent 30 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 10000, 'price': 32.0} +2025-10-24 17:33:28,365 - Stocklogger - INFO - INFO: Agent 30 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 10000, 'price': 32.0} +2025-10-24 17:33:29,844 - Stocklogger - INFO - INFO: Agent 20 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 5000, 'price': 32.0} +2025-10-24 17:33:29,844 - Stocklogger - INFO - INFO: Agent 20 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 5000, 'price': 32.0} +2025-10-24 17:33:31,029 - Stocklogger - INFO - INFO: Agent 43 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 10000, 'price': 32.5} +2025-10-24 17:33:31,029 - Stocklogger - INFO - INFO: Agent 43 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 10000, 'price': 32.5} +2025-10-24 17:33:33,202 - Stocklogger - INFO - INFO: Agent 7 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 5000, 'price': 32.0} +2025-10-24 17:33:33,202 - Stocklogger - INFO - INFO: Agent 7 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 5000, 'price': 32.0} +2025-10-24 17:33:34,216 - Stocklogger - INFO - INFO: Agent 22 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 1000, 'price': 32.0} +2025-10-24 17:33:34,216 - Stocklogger - INFO - INFO: Agent 22 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 1000, 'price': 32.0} +2025-10-24 17:33:35,174 - Stocklogger - INFO - INFO: Agent 26 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 10000, 'price': 32.5} +2025-10-24 17:33:35,174 - Stocklogger - INFO - INFO: Agent 26 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 10000, 'price': 32.5} +2025-10-24 17:33:36,608 - Stocklogger - INFO - INFO: Agent 35 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 10000, 'price': 32.0} +2025-10-24 17:33:36,608 - Stocklogger - INFO - INFO: Agent 35 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 10000, 'price': 32.0} +2025-10-24 17:33:38,018 - Stocklogger - INFO - INFO: Agent 10 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 5000, 'price': 32.5} +2025-10-24 17:33:38,018 - Stocklogger - INFO - INFO: Agent 10 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 5000, 'price': 32.5} +2025-10-24 17:33:39,125 - Stocklogger - INFO - INFO: Agent 41 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 10000, 'price': 32.0} +2025-10-24 17:33:39,125 - Stocklogger - INFO - INFO: Agent 41 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 10000, 'price': 32.0} +2025-10-24 17:33:40,124 - Stocklogger - INFO - INFO: Agent 40 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 10000, 'price': 32.0} +2025-10-24 17:33:40,124 - Stocklogger - INFO - INFO: Agent 40 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 10000, 'price': 32.0} +2025-10-24 17:33:41,291 - Stocklogger - INFO - INFO: Agent 36 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 10000, 'price': 32.5} +2025-10-24 17:33:41,291 - Stocklogger - INFO - INFO: Agent 36 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 10000, 'price': 32.5} +2025-10-24 17:33:42,455 - Stocklogger - INFO - INFO: Agent 14 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 10000, 'price': 32.0} +2025-10-24 17:33:42,455 - Stocklogger - INFO - INFO: Agent 14 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 10000, 'price': 32.0} +2025-10-24 17:33:43,522 - Stocklogger - DEBUG - Sell more than hold: {"action_type": "sell", "stock": "A", "amount": 5000, "price": 32.0} +2025-10-24 17:33:43,522 - Stocklogger - DEBUG - Sell more than hold: {"action_type": "sell", "stock": "A", "amount": 5000, "price": 32.0} +2025-10-24 17:33:44,762 - Stocklogger - INFO - INFO: Agent 16 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 4917, 'price': 32.0} +2025-10-24 17:33:44,762 - Stocklogger - INFO - INFO: Agent 16 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 4917, 'price': 32.0} +2025-10-24 17:33:48,485 - Stocklogger - INFO - INFO: Agent 27 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 10000, 'price': 32.0} +2025-10-24 17:33:48,485 - Stocklogger - INFO - INFO: Agent 27 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 10000, 'price': 32.0} +2025-10-24 17:33:49,887 - Stocklogger - INFO - INFO: Agent 6 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 10000, 'price': 32.5} +2025-10-24 17:33:49,887 - Stocklogger - INFO - INFO: Agent 6 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 10000, 'price': 32.5} +2025-10-24 17:33:53,730 - Stocklogger - INFO - INFO: Agent 42 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 10000, 'price': 32.0} +2025-10-24 17:33:53,730 - Stocklogger - INFO - INFO: Agent 42 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 10000, 'price': 32.0} +2025-10-24 17:33:56,491 - Stocklogger - INFO - INFO: Agent 8 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 10000, 'price': 32.5} +2025-10-24 17:33:56,491 - Stocklogger - INFO - INFO: Agent 8 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 10000, 'price': 32.5} +2025-10-24 17:33:57,511 - Stocklogger - INFO - INFO: Agent 48 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 10000, 'price': 32.5} +2025-10-24 17:33:57,511 - Stocklogger - INFO - INFO: Agent 48 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 10000, 'price': 32.5} +2025-10-24 17:33:58,691 - Stocklogger - INFO - INFO: Agent 18 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 10000, 'price': 32.0} +2025-10-24 17:33:58,691 - Stocklogger - INFO - INFO: Agent 18 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 10000, 'price': 32.0} +2025-10-24 17:33:59,884 - Stocklogger - INFO - INFO: Agent 12 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 10000, 'price': 32.5} +2025-10-24 17:33:59,884 - Stocklogger - INFO - INFO: Agent 12 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 10000, 'price': 32.5} +2025-10-24 17:34:00,838 - Stocklogger - INFO - INFO: Agent 3 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 10000, 'price': 32.5} +2025-10-24 17:34:00,838 - Stocklogger - INFO - INFO: Agent 3 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 10000, 'price': 32.5} +2025-10-24 17:34:01,985 - Stocklogger - INFO - INFO: Agent 34 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 10000, 'price': 32.5} +2025-10-24 17:34:01,985 - Stocklogger - INFO - INFO: Agent 34 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 10000, 'price': 32.5} +2025-10-24 17:34:02,782 - Stocklogger - INFO - INFO: Agent 24 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 1000, 'price': 32.5} +2025-10-24 17:34:02,782 - Stocklogger - INFO - INFO: Agent 24 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 1000, 'price': 32.5} +2025-10-24 17:34:04,304 - Stocklogger - DEBUG - Sell more than hold: ```json +{"action_type":"sell", "stock":"A", "amount":10000, "price":32.0} +``` +2025-10-24 17:34:04,304 - Stocklogger - DEBUG - Sell more than hold: ```json +{"action_type":"sell", "stock":"A", "amount":10000, "price":32.0} +``` +2025-10-24 17:34:08,442 - Stocklogger - INFO - INFO: Agent 11 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 5000, 'price': 32.0} +2025-10-24 17:34:08,442 - Stocklogger - INFO - INFO: Agent 11 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 5000, 'price': 32.0} +2025-10-24 17:34:09,424 - Stocklogger - INFO - INFO: Agent 32 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 10000, 'price': 32.5} +2025-10-24 17:34:09,424 - Stocklogger - INFO - INFO: Agent 32 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 10000, 'price': 32.5} +2025-10-24 17:34:10,594 - Stocklogger - INFO - INFO: Agent 29 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 10000, 'price': 32.5} +2025-10-24 17:34:10,594 - Stocklogger - INFO - INFO: Agent 29 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 10000, 'price': 32.5} +2025-10-24 17:34:13,211 - Stocklogger - INFO - INFO: Agent 45 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 10000, 'price': 32.5} +2025-10-24 17:34:13,211 - Stocklogger - INFO - INFO: Agent 45 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 10000, 'price': 32.5} +2025-10-24 17:34:14,208 - Stocklogger - INFO - INFO: Agent 13 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 10000, 'price': 32.5} +2025-10-24 17:34:14,208 - Stocklogger - INFO - INFO: Agent 13 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 10000, 'price': 32.5} +2025-10-24 17:34:15,688 - Stocklogger - INFO - INFO: Agent 47 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 10000, 'price': 32.5} +2025-10-24 17:34:15,688 - Stocklogger - INFO - INFO: Agent 47 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 10000, 'price': 32.5} +2025-10-24 17:34:16,854 - Stocklogger - INFO - INFO: Agent 1 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 100, 'price': 32.5} +2025-10-24 17:34:16,854 - Stocklogger - INFO - INFO: Agent 1 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 100, 'price': 32.5} +2025-10-24 17:34:18,074 - Stocklogger - INFO - INFO: Agent 23 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 10000, 'price': 32.5} +2025-10-24 17:34:18,074 - Stocklogger - INFO - INFO: Agent 23 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 10000, 'price': 32.5} +2025-10-24 17:34:22,070 - Stocklogger - INFO - INFO: Agent 25 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 5000, 'price': 32.0} +2025-10-24 17:34:22,070 - Stocklogger - INFO - INFO: Agent 25 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 5000, 'price': 32.0} +2025-10-24 17:34:23,394 - Stocklogger - INFO - INFO: Agent 31 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 10000, 'price': 32.5} +2025-10-24 17:34:23,394 - Stocklogger - INFO - INFO: Agent 31 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 10000, 'price': 32.5} +2025-10-24 17:34:24,329 - Stocklogger - INFO - INFO: Agent 38 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 10000, 'price': 32.5} +2025-10-24 17:34:24,329 - Stocklogger - INFO - INFO: Agent 38 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 10000, 'price': 32.5} +2025-10-24 17:34:31,069 - Stocklogger - INFO - INFO: Agent 49 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 10000, 'price': 32.5} +2025-10-24 17:34:31,069 - Stocklogger - INFO - INFO: Agent 49 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 10000, 'price': 32.5} +2025-10-24 17:34:32,075 - Stocklogger - INFO - INFO: Agent 28 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 10000, 'price': 32.5} +2025-10-24 17:34:32,075 - Stocklogger - INFO - INFO: Agent 28 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 10000, 'price': 32.5} +2025-10-24 17:34:33,231 - Stocklogger - INFO - INFO: Agent 17 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 10000, 'price': 32.5} +2025-10-24 17:34:33,231 - Stocklogger - INFO - INFO: Agent 17 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 10000, 'price': 32.5} +2025-10-24 17:34:34,204 - Stocklogger - INFO - INFO: Agent 33 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 10000, 'price': 32.5} +2025-10-24 17:34:34,204 - Stocklogger - INFO - INFO: Agent 33 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 10000, 'price': 32.5} +2025-10-24 17:34:35,236 - Stocklogger - INFO - INFO: Agent 37 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 10000, 'price': 32.5} +2025-10-24 17:34:35,236 - Stocklogger - INFO - INFO: Agent 37 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 10000, 'price': 32.5} +2025-10-24 17:34:36,121 - Stocklogger - INFO - INFO: Agent 21 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 1000, 'price': 32.5} +2025-10-24 17:34:36,121 - Stocklogger - INFO - INFO: Agent 21 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 1000, 'price': 32.5} +2025-10-24 17:34:37,361 - Stocklogger - INFO - INFO: Agent 39 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 10000, 'price': 32.5} +2025-10-24 17:34:37,361 - Stocklogger - INFO - INFO: Agent 39 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 10000, 'price': 32.5} +2025-10-24 17:34:38,489 - Stocklogger - INFO - INFO: Agent 9 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 10000, 'price': 32.5} +2025-10-24 17:34:38,489 - Stocklogger - INFO - INFO: Agent 9 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 10000, 'price': 32.5} +2025-10-24 17:34:39,631 - Stocklogger - INFO - INFO: Agent 4 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 10000, 'price': 32.0} +2025-10-24 17:34:39,631 - Stocklogger - INFO - INFO: Agent 4 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 10000, 'price': 32.0} +2025-10-24 17:34:42,436 - Stocklogger - INFO - INFO: Agent 2 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 10000, 'price': 32.5} +2025-10-24 17:34:42,436 - Stocklogger - INFO - INFO: Agent 2 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 10000, 'price': 32.5} +2025-10-24 17:34:43,744 - Stocklogger - INFO - INFO: Agent 19 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 10000, 'price': 32.5} +2025-10-24 17:34:43,744 - Stocklogger - INFO - INFO: Agent 19 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 10000, 'price': 32.5} +2025-10-24 17:34:45,962 - Stocklogger - INFO - INFO: Agent 22 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 2000, 'price': 32.5} +2025-10-24 17:34:45,962 - Stocklogger - INFO - INFO: Agent 22 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 2000, 'price': 32.5} +2025-10-24 17:34:47,057 - Stocklogger - INFO - INFO: Agent 39 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 10000, 'price': 32.0} +2025-10-24 17:34:47,057 - Stocklogger - INFO - INFO: Agent 39 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 10000, 'price': 32.0} +2025-10-24 17:34:48,555 - Stocklogger - INFO - INFO: Agent 35 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 10000, 'price': 32.5} +2025-10-24 17:34:48,555 - Stocklogger - INFO - INFO: Agent 35 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 10000, 'price': 32.5} +2025-10-24 17:34:49,714 - Stocklogger - INFO - INFO: Agent 17 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 10000, 'price': 32.5} +2025-10-24 17:34:49,714 - Stocklogger - INFO - INFO: Agent 17 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 10000, 'price': 32.5} +2025-10-24 17:34:50,951 - Stocklogger - INFO - INFO: Agent 34 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 100, 'price': 42.0} +2025-10-24 17:34:50,951 - Stocklogger - INFO - INFO: Agent 34 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 100, 'price': 42.0} +2025-10-24 17:34:52,299 - Stocklogger - INFO - INFO: Agent 16 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 100, 'price': 42.0} +2025-10-24 17:34:52,299 - Stocklogger - INFO - INFO: Agent 16 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 100, 'price': 42.0} +2025-10-24 17:34:53,744 - Stocklogger - INFO - INFO: Agent 6 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 10000, 'price': 32.0} +2025-10-24 17:34:53,744 - Stocklogger - INFO - INFO: Agent 6 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 10000, 'price': 32.0} +2025-10-24 17:34:54,870 - Stocklogger - INFO - INFO: Agent 2 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 100, 'price': 42.0} +2025-10-24 17:34:54,870 - Stocklogger - INFO - INFO: Agent 2 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 100, 'price': 42.0} +2025-10-24 17:34:56,147 - Stocklogger - INFO - INFO: Agent 42 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 100, 'price': 42.0} +2025-10-24 17:34:56,147 - Stocklogger - INFO - INFO: Agent 42 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 100, 'price': 42.0} +2025-10-24 17:34:57,033 - Stocklogger - INFO - INFO: Agent 23 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 100, 'price': 42.0} +2025-10-24 17:34:57,033 - Stocklogger - INFO - INFO: Agent 23 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 100, 'price': 42.0} +2025-10-24 17:34:59,148 - Stocklogger - INFO - INFO: Agent 0 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 150, 'price': 42.5} +2025-10-24 17:34:59,148 - Stocklogger - INFO - INFO: Agent 0 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 150, 'price': 42.5} +2025-10-24 17:35:00,405 - Stocklogger - INFO - INFO: Agent 27 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 10000, 'price': 32.5} +2025-10-24 17:35:00,405 - Stocklogger - INFO - INFO: Agent 27 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 10000, 'price': 32.5} +2025-10-24 17:35:01,619 - Stocklogger - INFO - INFO: Agent 46 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 100, 'price': 42.0} +2025-10-24 17:35:01,619 - Stocklogger - INFO - INFO: Agent 46 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 100, 'price': 42.0} +2025-10-24 17:35:03,124 - Stocklogger - INFO - INFO: Agent 32 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 200, 'price': 42.0} +2025-10-24 17:35:03,124 - Stocklogger - INFO - INFO: Agent 32 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 200, 'price': 42.0} +2025-10-24 17:35:04,824 - Stocklogger - INFO - INFO: Agent 24 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 100, 'price': 42.0} +2025-10-24 17:35:04,824 - Stocklogger - INFO - INFO: Agent 24 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 100, 'price': 42.0} +2025-10-24 17:35:05,889 - Stocklogger - INFO - INFO: Agent 8 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 150, 'price': 42.5} +2025-10-24 17:35:05,889 - Stocklogger - INFO - INFO: Agent 8 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 150, 'price': 42.5} +2025-10-24 17:35:07,172 - Stocklogger - INFO - INFO: Agent 7 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 200, 'price': 42.0} +2025-10-24 17:35:07,172 - Stocklogger - INFO - INFO: Agent 7 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 200, 'price': 42.0} +2025-10-24 17:35:08,145 - Stocklogger - INFO - INFO: Agent 30 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 200, 'price': 42.0} +2025-10-24 17:35:08,145 - Stocklogger - INFO - INFO: Agent 30 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 200, 'price': 42.0} +2025-10-24 17:35:09,370 - Stocklogger - INFO - INFO: Agent 45 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 100, 'price': 42.0} +2025-10-24 17:35:09,370 - Stocklogger - INFO - INFO: Agent 45 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 100, 'price': 42.0} +2025-10-24 17:35:10,641 - Stocklogger - INFO - INFO: Agent 25 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 200, 'price': 42.0} +2025-10-24 17:35:10,641 - Stocklogger - INFO - INFO: Agent 25 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 200, 'price': 42.0} +2025-10-24 17:35:12,477 - Stocklogger - INFO - INFO: Agent 26 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 100, 'price': 42.0} +2025-10-24 17:35:12,477 - Stocklogger - INFO - INFO: Agent 26 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 100, 'price': 42.0} +2025-10-24 17:35:13,373 - Stocklogger - INFO - INFO: Agent 4 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 100, 'price': 42.0} +2025-10-24 17:35:13,373 - Stocklogger - INFO - INFO: Agent 4 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 100, 'price': 42.0} +2025-10-24 17:35:15,653 - Stocklogger - INFO - INFO: Agent 28 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 150, 'price': 42.5} +2025-10-24 17:35:15,653 - Stocklogger - INFO - INFO: Agent 28 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 150, 'price': 42.5} +2025-10-24 17:35:16,778 - Stocklogger - INFO - INFO: Agent 21 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 200, 'price': 42.0} +2025-10-24 17:35:16,778 - Stocklogger - INFO - INFO: Agent 21 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 200, 'price': 42.0} +2025-10-24 17:35:19,082 - Stocklogger - INFO - INFO: Agent 44 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 1000, 'price': 32.5} +2025-10-24 17:35:19,082 - Stocklogger - INFO - INFO: Agent 44 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 1000, 'price': 32.5} +2025-10-24 17:35:20,907 - Stocklogger - INFO - INFO: Agent 18 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 200, 'price': 42.0} +2025-10-24 17:35:20,907 - Stocklogger - INFO - INFO: Agent 18 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 200, 'price': 42.0} +2025-10-24 17:35:22,310 - Stocklogger - INFO - INFO: Agent 38 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 200, 'price': 42.0} +2025-10-24 17:35:22,310 - Stocklogger - INFO - INFO: Agent 38 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 200, 'price': 42.0} +2025-10-24 17:35:23,966 - Stocklogger - INFO - INFO: Agent 15 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 100, 'price': 42.0} +2025-10-24 17:35:23,966 - Stocklogger - INFO - INFO: Agent 15 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 100, 'price': 42.0} +2025-10-24 17:35:25,055 - Stocklogger - INFO - INFO: Agent 14 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 200, 'price': 42.0} +2025-10-24 17:35:25,055 - Stocklogger - INFO - INFO: Agent 14 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 200, 'price': 42.0} +2025-10-24 17:35:26,582 - Stocklogger - INFO - INFO: Agent 11 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 100, 'price': 42.0} +2025-10-24 17:35:26,582 - Stocklogger - INFO - INFO: Agent 11 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 100, 'price': 42.0} +2025-10-24 17:35:28,060 - Stocklogger - INFO - INFO: Agent 47 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 200, 'price': 42.0} +2025-10-24 17:35:28,060 - Stocklogger - INFO - INFO: Agent 47 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 200, 'price': 42.0} +2025-10-24 17:35:30,151 - Stocklogger - INFO - INFO: Agent 37 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 100, 'price': 42.0} +2025-10-24 17:35:30,151 - Stocklogger - INFO - INFO: Agent 37 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 100, 'price': 42.0} +2025-10-24 17:35:31,431 - Stocklogger - INFO - INFO: Agent 43 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 200, 'price': 42.0} +2025-10-24 17:35:31,431 - Stocklogger - INFO - INFO: Agent 43 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 200, 'price': 42.0} +2025-10-24 17:35:34,513 - Stocklogger - INFO - INFO: Agent 9 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 100, 'price': 42.0} +2025-10-24 17:35:34,513 - Stocklogger - INFO - INFO: Agent 9 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 100, 'price': 42.0} +2025-10-24 17:35:37,950 - Stocklogger - INFO - INFO: Agent 12 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 200, 'price': 42.0} +2025-10-24 17:35:37,950 - Stocklogger - INFO - INFO: Agent 12 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 200, 'price': 42.0} +2025-10-24 17:35:39,429 - Stocklogger - INFO - INFO: Agent 5 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 200, 'price': 42.0} +2025-10-24 17:35:39,429 - Stocklogger - INFO - INFO: Agent 5 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 200, 'price': 42.0} +2025-10-24 17:35:41,794 - Stocklogger - INFO - INFO: Agent 31 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 200, 'price': 42.0} +2025-10-24 17:35:41,794 - Stocklogger - INFO - INFO: Agent 31 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 200, 'price': 42.0} +2025-10-24 17:35:42,858 - Stocklogger - INFO - INFO: Agent 3 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 200, 'price': 42.0} +2025-10-24 17:35:42,858 - Stocklogger - INFO - INFO: Agent 3 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 200, 'price': 42.0} +2025-10-24 17:35:43,904 - Stocklogger - INFO - INFO: Agent 20 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 200, 'price': 42.0} +2025-10-24 17:35:43,904 - Stocklogger - INFO - INFO: Agent 20 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 200, 'price': 42.0} +2025-10-24 17:35:45,982 - Stocklogger - INFO - INFO: Agent 41 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 200, 'price': 42.0} +2025-10-24 17:35:45,982 - Stocklogger - INFO - INFO: Agent 41 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 200, 'price': 42.0} +2025-10-24 17:35:47,009 - Stocklogger - INFO - INFO: Agent 29 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 200, 'price': 42.0} +2025-10-24 17:35:47,009 - Stocklogger - INFO - INFO: Agent 29 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 200, 'price': 42.0} +2025-10-24 17:35:48,172 - Stocklogger - INFO - INFO: Agent 13 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 200, 'price': 42.0} +2025-10-24 17:35:48,172 - Stocklogger - INFO - INFO: Agent 13 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 200, 'price': 42.0} +2025-10-24 17:35:49,348 - Stocklogger - INFO - INFO: Agent 10 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 200, 'price': 42.0} +2025-10-24 17:35:49,348 - Stocklogger - INFO - INFO: Agent 10 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 200, 'price': 42.0} +2025-10-24 17:35:52,969 - Stocklogger - INFO - INFO: Agent 48 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 200, 'price': 42.0} +2025-10-24 17:35:52,969 - Stocklogger - INFO - INFO: Agent 48 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 200, 'price': 42.0} +2025-10-24 17:41:29,495 - Stocklogger - INFO - 🤖 Initializing LLM agents... +2025-10-24 17:41:29,495 - Stocklogger - INFO - 🤖 Initializing LLM agents... +2025-10-24 17:41:30,122 - Stocklogger - INFO - INFO: Agent 0 decide not to loan +2025-10-24 17:41:30,122 - Stocklogger - INFO - INFO: Agent 0 decide not to loan +2025-10-24 17:41:30,796 - Stocklogger - INFO - INFO: Agent 1 decide not to loan +2025-10-24 17:41:30,796 - Stocklogger - INFO - INFO: Agent 1 decide not to loan +2025-10-24 17:41:31,831 - Stocklogger - INFO - INFO: Agent 2 decide to loan: {'loan': 'yes', 'loan_type': 2, 'amount': 1535527.084900642, 'repayment_date': 67} +2025-10-24 17:41:31,831 - Stocklogger - INFO - INFO: Agent 2 decide to loan: {'loan': 'yes', 'loan_type': 2, 'amount': 1535527.084900642, 'repayment_date': 67} +2025-10-24 17:41:32,726 - Stocklogger - INFO - INFO: Agent 3 decide to loan: {'loan': 'yes', 'loan_type': 1, 'amount': 1500000, 'repayment_date': 45} +2025-10-24 17:41:32,726 - Stocklogger - INFO - INFO: Agent 3 decide to loan: {'loan': 'yes', 'loan_type': 1, 'amount': 1500000, 'repayment_date': 45} +2025-10-24 17:41:33,452 - Stocklogger - INFO - INFO: Agent 4 decide not to loan +2025-10-24 17:41:33,452 - Stocklogger - INFO - INFO: Agent 4 decide not to loan +2025-10-24 17:41:34,043 - Stocklogger - INFO - INFO: Agent 5 decide not to loan +2025-10-24 17:41:34,043 - Stocklogger - INFO - INFO: Agent 5 decide not to loan +2025-10-24 17:41:35,946 - Stocklogger - INFO - INFO: Agent 6 decide not to loan +2025-10-24 17:41:35,946 - Stocklogger - INFO - INFO: Agent 6 decide not to loan +2025-10-24 17:41:36,574 - Stocklogger - INFO - INFO: Agent 7 decide not to loan +2025-10-24 17:41:36,574 - Stocklogger - INFO - INFO: Agent 7 decide not to loan +2025-10-24 17:41:37,073 - Stocklogger - INFO - INFO: Agent 8 decide not to loan +2025-10-24 17:41:37,073 - Stocklogger - INFO - INFO: Agent 8 decide not to loan +2025-10-24 17:41:38,259 - Stocklogger - INFO - INFO: Agent 9 decide not to loan +2025-10-24 17:41:38,259 - Stocklogger - INFO - INFO: Agent 9 decide not to loan +2025-10-24 17:41:38,730 - Stocklogger - INFO - INFO: Agent 10 decide not to loan +2025-10-24 17:41:38,730 - Stocklogger - INFO - INFO: Agent 10 decide not to loan +2025-10-24 17:41:39,671 - Stocklogger - INFO - INFO: Agent 11 decide to loan: {'loan': 'yes', 'loan_type': 1, 'amount': 613969.1926603939, 'repayment_date': 45} +2025-10-24 17:41:39,671 - Stocklogger - INFO - INFO: Agent 11 decide to loan: {'loan': 'yes', 'loan_type': 1, 'amount': 613969.1926603939, 'repayment_date': 45} +2025-10-24 17:41:40,195 - Stocklogger - INFO - INFO: Agent 12 decide not to loan +2025-10-24 17:41:40,195 - Stocklogger - INFO - INFO: Agent 12 decide not to loan +2025-10-24 17:41:40,693 - Stocklogger - INFO - INFO: Agent 13 decide not to loan +2025-10-24 17:41:40,693 - Stocklogger - INFO - INFO: Agent 13 decide not to loan +2025-10-24 17:41:41,110 - Stocklogger - INFO - INFO: Agent 14 decide not to loan +2025-10-24 17:41:41,110 - Stocklogger - INFO - INFO: Agent 14 decide not to loan +2025-10-24 17:41:42,456 - Stocklogger - DEBUG - Wrong json content in response: {"loan": "yes", "loan_type": 1, "amount": 2500000} +2025-10-24 17:41:42,456 - Stocklogger - DEBUG - Wrong json content in response: {"loan": "yes", "loan_type": 1, "amount": 2500000} +2025-10-24 17:41:43,545 - Stocklogger - DEBUG - Wrong json content in response: 1 +2025-10-24 17:41:43,545 - Stocklogger - DEBUG - Wrong json content in response: 1 +2025-10-24 17:41:44,766 - Stocklogger - DEBUG - Wrong json content in response: 1 +2025-10-24 17:41:44,766 - Stocklogger - DEBUG - Wrong json content in response: 1 +2025-10-24 17:41:45,626 - Stocklogger - DEBUG - Wrong json content in response: 1 +2025-10-24 17:41:45,626 - Stocklogger - DEBUG - Wrong json content in response: 1 +2025-10-24 17:41:45,626 - Stocklogger - WARNING - WARNING: Loan format try times > MAX_TRY_TIMES. Skip as no loan today. +2025-10-24 17:41:45,626 - Stocklogger - WARNING - WARNING: Loan format try times > MAX_TRY_TIMES. Skip as no loan today. +2025-10-24 17:41:45,626 - Stocklogger - INFO - INFO: Agent 15 decide not to loan +2025-10-24 17:41:45,626 - Stocklogger - INFO - INFO: Agent 15 decide not to loan +2025-10-24 17:41:46,243 - Stocklogger - INFO - INFO: Agent 16 decide not to loan +2025-10-24 17:41:46,243 - Stocklogger - INFO - INFO: Agent 16 decide not to loan +2025-10-24 17:41:46,756 - Stocklogger - INFO - INFO: Agent 17 decide not to loan +2025-10-24 17:41:46,756 - Stocklogger - INFO - INFO: Agent 17 decide not to loan +2025-10-24 17:41:47,446 - Stocklogger - INFO - INFO: Agent 18 decide not to loan +2025-10-24 17:41:47,446 - Stocklogger - INFO - INFO: Agent 18 decide not to loan +2025-10-24 17:41:48,229 - Stocklogger - INFO - INFO: Agent 19 decide to loan: {'loan': 'yes', 'loan_type': 1, 'amount': 300000, 'repayment_date': 45} +2025-10-24 17:41:48,229 - Stocklogger - INFO - INFO: Agent 19 decide to loan: {'loan': 'yes', 'loan_type': 1, 'amount': 300000, 'repayment_date': 45} +2025-10-24 17:41:48,680 - Stocklogger - INFO - INFO: Agent 20 decide not to loan +2025-10-24 17:41:48,680 - Stocklogger - INFO - INFO: Agent 20 decide not to loan +2025-10-24 17:41:49,568 - Stocklogger - INFO - INFO: Agent 21 decide to loan: {'loan': 'yes', 'loan_type': 0, 'amount': 1963085.8964035409, 'repayment_date': 23} +2025-10-24 17:41:49,568 - Stocklogger - INFO - INFO: Agent 21 decide to loan: {'loan': 'yes', 'loan_type': 0, 'amount': 1963085.8964035409, 'repayment_date': 23} +2025-10-24 17:41:50,122 - Stocklogger - INFO - INFO: Agent 22 decide not to loan +2025-10-24 17:41:50,122 - Stocklogger - INFO - INFO: Agent 22 decide not to loan +2025-10-24 17:41:50,891 - Stocklogger - INFO - INFO: Agent 23 decide not to loan +2025-10-24 17:41:50,891 - Stocklogger - INFO - INFO: Agent 23 decide not to loan +2025-10-24 17:41:51,838 - Stocklogger - INFO - INFO: Agent 24 decide not to loan +2025-10-24 17:41:51,838 - Stocklogger - INFO - INFO: Agent 24 decide not to loan +2025-10-24 17:41:52,538 - Stocklogger - INFO - INFO: Agent 25 decide not to loan +2025-10-24 17:41:52,538 - Stocklogger - INFO - INFO: Agent 25 decide not to loan +2025-10-24 17:41:53,636 - Stocklogger - INFO - INFO: Agent 26 decide not to loan +2025-10-24 17:41:53,636 - Stocklogger - INFO - INFO: Agent 26 decide not to loan +2025-10-24 17:41:54,255 - Stocklogger - INFO - INFO: Agent 27 decide not to loan +2025-10-24 17:41:54,255 - Stocklogger - INFO - INFO: Agent 27 decide not to loan +2025-10-24 17:41:54,886 - Stocklogger - INFO - INFO: Agent 28 decide not to loan +2025-10-24 17:41:54,886 - Stocklogger - INFO - INFO: Agent 28 decide not to loan +2025-10-24 17:41:55,477 - Stocklogger - INFO - INFO: Agent 29 decide not to loan +2025-10-24 17:41:55,477 - Stocklogger - INFO - INFO: Agent 29 decide not to loan +2025-10-24 17:41:55,887 - Stocklogger - INFO - INFO: Agent 30 decide not to loan +2025-10-24 17:41:55,887 - Stocklogger - INFO - INFO: Agent 30 decide not to loan +2025-10-24 17:41:56,257 - Stocklogger - INFO - INFO: Agent 31 decide not to loan +2025-10-24 17:41:56,257 - Stocklogger - INFO - INFO: Agent 31 decide not to loan +2025-10-24 17:41:57,120 - Stocklogger - INFO - INFO: Agent 32 decide to loan: {'loan': 'yes', 'loan_type': 2, 'amount': 1419975.7602803316, 'repayment_date': 67} +2025-10-24 17:41:57,120 - Stocklogger - INFO - INFO: Agent 32 decide to loan: {'loan': 'yes', 'loan_type': 2, 'amount': 1419975.7602803316, 'repayment_date': 67} +2025-10-24 17:41:57,680 - Stocklogger - INFO - INFO: Agent 33 decide not to loan +2025-10-24 17:41:57,680 - Stocklogger - INFO - INFO: Agent 33 decide not to loan +2025-10-24 17:41:58,135 - Stocklogger - INFO - INFO: Agent 34 decide not to loan +2025-10-24 17:41:58,135 - Stocklogger - INFO - INFO: Agent 34 decide not to loan +2025-10-24 17:41:58,894 - Stocklogger - INFO - INFO: Agent 35 decide to loan: {'loan': 'yes', 'loan_type': 1, 'amount': 100000, 'repayment_date': 45} +2025-10-24 17:41:58,894 - Stocklogger - INFO - INFO: Agent 35 decide to loan: {'loan': 'yes', 'loan_type': 1, 'amount': 100000, 'repayment_date': 45} +2025-10-24 17:41:59,537 - Stocklogger - INFO - INFO: Agent 36 decide not to loan +2025-10-24 17:41:59,537 - Stocklogger - INFO - INFO: Agent 36 decide not to loan +2025-10-24 17:42:00,065 - Stocklogger - INFO - INFO: Agent 37 decide not to loan +2025-10-24 17:42:00,065 - Stocklogger - INFO - INFO: Agent 37 decide not to loan +2025-10-24 17:42:00,609 - Stocklogger - INFO - INFO: Agent 38 decide not to loan +2025-10-24 17:42:00,609 - Stocklogger - INFO - INFO: Agent 38 decide not to loan +2025-10-24 17:42:01,167 - Stocklogger - INFO - INFO: Agent 39 decide not to loan +2025-10-24 17:42:01,167 - Stocklogger - INFO - INFO: Agent 39 decide not to loan +2025-10-24 17:42:02,078 - Stocklogger - INFO - INFO: Agent 40 decide to loan: {'loan': 'yes', 'loan_type': 1, 'amount': 2000000, 'repayment_date': 45} +2025-10-24 17:42:02,078 - Stocklogger - INFO - INFO: Agent 40 decide to loan: {'loan': 'yes', 'loan_type': 1, 'amount': 2000000, 'repayment_date': 45} +2025-10-24 17:42:02,603 - Stocklogger - INFO - INFO: Agent 41 decide not to loan +2025-10-24 17:42:02,603 - Stocklogger - INFO - INFO: Agent 41 decide not to loan +2025-10-24 17:42:03,549 - Stocklogger - INFO - INFO: Agent 42 decide not to loan +2025-10-24 17:42:03,549 - Stocklogger - INFO - INFO: Agent 42 decide not to loan +2025-10-24 17:42:04,303 - Stocklogger - INFO - INFO: Agent 43 decide not to loan +2025-10-24 17:42:04,303 - Stocklogger - INFO - INFO: Agent 43 decide not to loan +2025-10-24 17:42:05,173 - Stocklogger - INFO - INFO: Agent 44 decide to loan: {'loan': 'yes', 'loan_type': 2, 'amount': 4714070.392596438, 'repayment_date': 67} +2025-10-24 17:42:05,173 - Stocklogger - INFO - INFO: Agent 44 decide to loan: {'loan': 'yes', 'loan_type': 2, 'amount': 4714070.392596438, 'repayment_date': 67} +2025-10-24 17:42:05,794 - Stocklogger - INFO - INFO: Agent 45 decide to loan: {'loan': 'yes', 'loan_type': 1, 'amount': 2000000, 'repayment_date': 45} +2025-10-24 17:42:05,794 - Stocklogger - INFO - INFO: Agent 45 decide to loan: {'loan': 'yes', 'loan_type': 1, 'amount': 2000000, 'repayment_date': 45} +2025-10-24 17:42:06,630 - Stocklogger - INFO - INFO: Agent 46 decide not to loan +2025-10-24 17:42:06,630 - Stocklogger - INFO - INFO: Agent 46 decide not to loan +2025-10-24 17:42:07,112 - Stocklogger - INFO - INFO: Agent 47 decide not to loan +2025-10-24 17:42:07,112 - Stocklogger - INFO - INFO: Agent 47 decide not to loan +2025-10-24 17:42:07,986 - Stocklogger - INFO - INFO: Agent 48 decide to loan: {'loan': 'yes', 'loan_type': 1, 'amount': 1175968.1859453148, 'repayment_date': 45} +2025-10-24 17:42:07,986 - Stocklogger - INFO - INFO: Agent 48 decide to loan: {'loan': 'yes', 'loan_type': 1, 'amount': 1175968.1859453148, 'repayment_date': 45} +2025-10-24 17:42:08,472 - Stocklogger - INFO - INFO: Agent 49 decide not to loan +2025-10-24 17:42:08,472 - Stocklogger - INFO - INFO: Agent 49 decide not to loan +2025-10-24 17:42:09,466 - Stocklogger - INFO - INFO: Agent 36 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 50, 'price': 42.0} +2025-10-24 17:42:09,466 - Stocklogger - INFO - INFO: Agent 36 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 50, 'price': 42.0} +2025-10-24 17:42:10,295 - Stocklogger - INFO - INFO: Agent 29 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 1000, 'price': 30.5} +2025-10-24 17:42:10,295 - Stocklogger - INFO - INFO: Agent 29 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 1000, 'price': 30.5} +2025-10-24 17:42:11,293 - Stocklogger - INFO - INFO: Agent 15 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 1000, 'price': 31.0} +2025-10-24 17:42:11,293 - Stocklogger - INFO - INFO: Agent 15 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 1000, 'price': 31.0} +2025-10-24 17:42:12,962 - Stocklogger - INFO - INFO: Agent 4 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 1000, 'price': 31.5} +2025-10-24 17:42:12,962 - Stocklogger - INFO - INFO: Agent 4 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 1000, 'price': 31.5} +2025-10-24 17:42:13,821 - Stocklogger - INFO - INFO: Agent 48 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 3000, 'price': 31.0} +2025-10-24 17:42:13,821 - Stocklogger - INFO - INFO: Agent 48 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 3000, 'price': 31.0} +2025-10-24 17:42:14,937 - Stocklogger - INFO - INFO: Agent 17 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 2000, 'price': 31.0} +2025-10-24 17:42:14,937 - Stocklogger - INFO - INFO: Agent 17 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 2000, 'price': 31.0} +2025-10-24 17:42:16,559 - Stocklogger - INFO - INFO: Agent 21 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 3000, 'price': 31.5} +2025-10-24 17:42:16,559 - Stocklogger - INFO - INFO: Agent 21 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 3000, 'price': 31.5} +2025-10-24 17:42:17,850 - Stocklogger - DEBUG - Sell more than hold: {"action_type": "sell", "stock": "A", "amount": 1000, "price": 31.5} +2025-10-24 17:42:17,850 - Stocklogger - DEBUG - Sell more than hold: {"action_type": "sell", "stock": "A", "amount": 1000, "price": 31.5} +2025-10-24 17:42:18,398 - Stocklogger - INFO - INFO: Agent 8 decide not to action +2025-10-24 17:42:18,398 - Stocklogger - INFO - INFO: Agent 8 decide not to action +2025-10-24 17:42:19,272 - Stocklogger - INFO - INFO: Agent 11 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 3000, 'price': 31.5} +2025-10-24 17:42:19,272 - Stocklogger - INFO - INFO: Agent 11 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 3000, 'price': 31.5} +2025-10-24 17:42:20,327 - Stocklogger - INFO - INFO: Agent 40 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 3000, 'price': 32.0} +2025-10-24 17:42:20,327 - Stocklogger - INFO - INFO: Agent 40 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 3000, 'price': 32.0} +2025-10-24 17:42:21,319 - Stocklogger - INFO - INFO: Agent 26 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 3000, 'price': 32.0} +2025-10-24 17:42:21,319 - Stocklogger - INFO - INFO: Agent 26 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 3000, 'price': 32.0} +2025-10-24 17:42:23,461 - Stocklogger - INFO - INFO: Agent 30 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 3000, 'price': 32.0} +2025-10-24 17:42:23,461 - Stocklogger - INFO - INFO: Agent 30 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 3000, 'price': 32.0} +2025-10-24 17:42:25,095 - Stocklogger - INFO - INFO: Agent 38 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 3000, 'price': 32.0} +2025-10-24 17:42:25,095 - Stocklogger - INFO - INFO: Agent 38 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 3000, 'price': 32.0} +2025-10-24 17:42:25,916 - Stocklogger - INFO - INFO: Agent 6 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 3000, 'price': 32.0} +2025-10-24 17:42:25,916 - Stocklogger - INFO - INFO: Agent 6 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 3000, 'price': 32.0} +2025-10-24 17:42:26,836 - Stocklogger - INFO - INFO: Agent 1 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 3000, 'price': 32.0} +2025-10-24 17:42:26,836 - Stocklogger - INFO - INFO: Agent 1 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 3000, 'price': 32.0} +2025-10-24 17:42:28,122 - Stocklogger - INFO - INFO: Agent 13 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 3000, 'price': 32.0} +2025-10-24 17:42:28,122 - Stocklogger - INFO - INFO: Agent 13 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 3000, 'price': 32.0} +2025-10-24 17:42:29,030 - Stocklogger - INFO - INFO: Agent 46 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 3000, 'price': 32.0} +2025-10-24 17:42:29,030 - Stocklogger - INFO - INFO: Agent 46 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 3000, 'price': 32.0} +2025-10-24 17:42:30,555 - Stocklogger - INFO - INFO: Agent 42 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 3000, 'price': 32.0} +2025-10-24 17:42:30,555 - Stocklogger - INFO - INFO: Agent 42 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 3000, 'price': 32.0} +2025-10-24 17:42:31,605 - Stocklogger - INFO - INFO: Agent 27 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 6000, 'price': 32.0} +2025-10-24 17:42:31,605 - Stocklogger - INFO - INFO: Agent 27 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 6000, 'price': 32.0} +2025-10-24 17:42:32,688 - Stocklogger - INFO - INFO: Agent 24 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 6000, 'price': 32.5} +2025-10-24 17:42:32,688 - Stocklogger - INFO - INFO: Agent 24 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 6000, 'price': 32.5} +2025-10-24 17:42:33,547 - Stocklogger - INFO - INFO: Agent 22 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 6000, 'price': 32.5} +2025-10-24 17:42:33,547 - Stocklogger - INFO - INFO: Agent 22 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 6000, 'price': 32.5} +2025-10-24 17:42:34,836 - Stocklogger - INFO - INFO: Agent 47 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 6000, 'price': 32.0} +2025-10-24 17:42:34,836 - Stocklogger - INFO - INFO: Agent 47 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 6000, 'price': 32.0} +2025-10-24 17:42:35,951 - Stocklogger - INFO - INFO: Agent 10 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 6000, 'price': 32.5} +2025-10-24 17:42:35,951 - Stocklogger - INFO - INFO: Agent 10 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 6000, 'price': 32.5} +2025-10-24 17:42:37,477 - Stocklogger - INFO - INFO: Agent 3 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 6000, 'price': 32.5} +2025-10-24 17:42:37,477 - Stocklogger - INFO - INFO: Agent 3 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 6000, 'price': 32.5} +2025-10-24 17:42:39,108 - Stocklogger - INFO - INFO: Agent 12 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 6000, 'price': 32.5} +2025-10-24 17:42:39,108 - Stocklogger - INFO - INFO: Agent 12 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 6000, 'price': 32.5} +2025-10-24 17:42:40,254 - Stocklogger - INFO - INFO: Agent 32 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 6000, 'price': 32.5} +2025-10-24 17:42:40,254 - Stocklogger - INFO - INFO: Agent 32 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 6000, 'price': 32.5} +2025-10-24 17:42:41,036 - Stocklogger - INFO - INFO: Agent 31 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 6000, 'price': 32.5} +2025-10-24 17:42:41,036 - Stocklogger - INFO - INFO: Agent 31 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 6000, 'price': 32.5} +2025-10-24 17:42:41,987 - Stocklogger - INFO - INFO: Agent 19 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 6000, 'price': 32.5} +2025-10-24 17:42:41,987 - Stocklogger - INFO - INFO: Agent 19 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 6000, 'price': 32.5} +2025-10-24 17:42:43,206 - Stocklogger - INFO - INFO: Agent 18 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 6000, 'price': 32.5} +2025-10-24 17:42:43,206 - Stocklogger - INFO - INFO: Agent 18 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 6000, 'price': 32.5} +2025-10-24 17:42:45,707 - Stocklogger - INFO - INFO: Agent 2 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 6000, 'price': 32.5} +2025-10-24 17:42:45,707 - Stocklogger - INFO - INFO: Agent 2 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 6000, 'price': 32.5} +2025-10-24 17:42:47,078 - Stocklogger - INFO - INFO: Agent 34 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 6000, 'price': 32.5} +2025-10-24 17:42:47,078 - Stocklogger - INFO - INFO: Agent 34 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 6000, 'price': 32.5} +2025-10-24 17:42:48,492 - Stocklogger - INFO - INFO: Agent 23 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 6000, 'price': 32.5} +2025-10-24 17:42:48,492 - Stocklogger - INFO - INFO: Agent 23 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 6000, 'price': 32.5} +2025-10-24 17:42:49,523 - Stocklogger - INFO - INFO: Agent 43 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 6000, 'price': 32.5} +2025-10-24 17:42:49,523 - Stocklogger - INFO - INFO: Agent 43 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 6000, 'price': 32.5} +2025-10-24 17:42:50,720 - Stocklogger - INFO - INFO: Agent 35 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 6000, 'price': 32.5} +2025-10-24 17:42:50,720 - Stocklogger - INFO - INFO: Agent 35 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 6000, 'price': 32.5} +2025-10-24 17:42:51,649 - Stocklogger - INFO - INFO: Agent 28 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 6000, 'price': 32.5} +2025-10-24 17:42:51,649 - Stocklogger - INFO - INFO: Agent 28 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 6000, 'price': 32.5} +2025-10-24 17:42:53,498 - Stocklogger - INFO - INFO: Agent 37 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 6000, 'price': 32.0} +2025-10-24 17:42:53,498 - Stocklogger - INFO - INFO: Agent 37 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 6000, 'price': 32.0} +2025-10-24 17:42:54,541 - Stocklogger - INFO - INFO: Agent 5 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 6000, 'price': 32.5} +2025-10-24 17:42:54,541 - Stocklogger - INFO - INFO: Agent 5 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 6000, 'price': 32.5} +2025-10-24 17:42:55,699 - Stocklogger - INFO - INFO: Agent 44 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 6000, 'price': 32.5} +2025-10-24 17:42:55,699 - Stocklogger - INFO - INFO: Agent 44 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 6000, 'price': 32.5} +2025-10-24 17:42:56,542 - Stocklogger - INFO - INFO: Agent 14 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 6000, 'price': 32.5} +2025-10-24 17:42:56,542 - Stocklogger - INFO - INFO: Agent 14 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 6000, 'price': 32.5} +2025-10-24 17:42:57,367 - Stocklogger - INFO - INFO: Agent 25 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 6000, 'price': 32.5} +2025-10-24 17:42:57,367 - Stocklogger - INFO - INFO: Agent 25 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 6000, 'price': 32.5} +2025-10-24 17:42:58,269 - Stocklogger - INFO - INFO: Agent 20 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 6000, 'price': 32.5} +2025-10-24 17:42:58,269 - Stocklogger - INFO - INFO: Agent 20 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 6000, 'price': 32.5} +2025-10-24 17:43:00,098 - Stocklogger - INFO - INFO: Agent 9 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 6000, 'price': 32.5} +2025-10-24 17:43:00,098 - Stocklogger - INFO - INFO: Agent 9 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 6000, 'price': 32.5} +2025-10-24 17:43:01,050 - Stocklogger - INFO - INFO: Agent 41 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 6000, 'price': 32.5} +2025-10-24 17:43:01,050 - Stocklogger - INFO - INFO: Agent 41 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 6000, 'price': 32.5} +2025-10-24 17:43:01,912 - Stocklogger - INFO - INFO: Agent 49 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 1000, 'price': 32.0} +2025-10-24 17:43:01,912 - Stocklogger - INFO - INFO: Agent 49 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 1000, 'price': 32.0} +2025-10-24 17:43:03,132 - Stocklogger - INFO - INFO: Agent 0 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 6000, 'price': 32.5} +2025-10-24 17:43:03,132 - Stocklogger - INFO - INFO: Agent 0 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 6000, 'price': 32.5} +2025-10-24 17:43:04,942 - Stocklogger - INFO - INFO: Agent 45 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 6000, 'price': 32.5} +2025-10-24 17:43:04,942 - Stocklogger - INFO - INFO: Agent 45 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 6000, 'price': 32.5} +2025-10-24 17:43:06,101 - Stocklogger - INFO - INFO: Agent 7 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 6000, 'price': 32.5} +2025-10-24 17:43:06,101 - Stocklogger - INFO - INFO: Agent 7 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 6000, 'price': 32.5} +2025-10-24 17:43:07,077 - Stocklogger - INFO - INFO: Agent 33 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 3000, 'price': 32.5} +2025-10-24 17:43:07,077 - Stocklogger - INFO - INFO: Agent 33 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 3000, 'price': 32.5} +2025-10-24 17:43:08,316 - Stocklogger - INFO - INFO: Agent 16 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 3000, 'price': 32.5} +2025-10-24 17:43:08,316 - Stocklogger - INFO - INFO: Agent 16 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 3000, 'price': 32.5} +2025-10-24 17:43:09,771 - Stocklogger - INFO - INFO: Agent 39 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 6000, 'price': 32.5} +2025-10-24 17:43:09,771 - Stocklogger - INFO - INFO: Agent 39 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 6000, 'price': 32.5} +2025-10-24 17:43:11,127 - Stocklogger - INFO - INFO: Agent 38 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 100, 'price': 41.5} +2025-10-24 17:43:11,127 - Stocklogger - INFO - INFO: Agent 38 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 100, 'price': 41.5} +2025-10-24 17:43:12,082 - Stocklogger - INFO - INFO: Agent 18 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 100, 'price': 41.5} +2025-10-24 17:43:12,082 - Stocklogger - INFO - INFO: Agent 18 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 100, 'price': 41.5} +2025-10-24 17:43:13,385 - Stocklogger - INFO - INFO: Agent 9 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 100, 'price': 41.5} +2025-10-24 17:43:13,385 - Stocklogger - INFO - INFO: Agent 9 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 100, 'price': 41.5} +2025-10-24 17:43:14,584 - Stocklogger - INFO - INFO: Agent 39 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 6000, 'price': 32.5} +2025-10-24 17:43:14,584 - Stocklogger - INFO - INFO: Agent 39 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 6000, 'price': 32.5} +2025-10-24 17:43:15,721 - Stocklogger - INFO - INFO: Agent 16 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 3000, 'price': 31.5} +2025-10-24 17:43:15,721 - Stocklogger - INFO - INFO: Agent 16 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 3000, 'price': 31.5} +2025-10-24 17:43:16,792 - Stocklogger - INFO - INFO: Agent 30 decide to action: {'action_type': 'sell', 'stock': 'B', 'amount': 1000, 'price': 42.0} +2025-10-24 17:43:16,792 - Stocklogger - INFO - INFO: Agent 30 decide to action: {'action_type': 'sell', 'stock': 'B', 'amount': 1000, 'price': 42.0} +2025-10-24 17:43:16,802 - Stocklogger - INFO - ACTION - BUY:36, SELL:30, STOCK:B, PRICE:42.0, AMOUNT:50 +2025-10-24 17:43:16,802 - Stocklogger - INFO - ACTION - BUY:36, SELL:30, STOCK:B, PRICE:42.0, AMOUNT:50 +2025-10-24 17:43:18,041 - Stocklogger - INFO - INFO: Agent 14 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 100, 'price': 41.5} +2025-10-24 17:43:18,041 - Stocklogger - INFO - INFO: Agent 14 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 100, 'price': 41.5} +2025-10-24 17:43:19,031 - Stocklogger - INFO - INFO: Agent 13 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 200, 'price': 41.5} +2025-10-24 17:43:19,031 - Stocklogger - INFO - INFO: Agent 13 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 200, 'price': 41.5} +2025-10-24 17:43:20,316 - Stocklogger - INFO - INFO: Agent 25 decide to action: {'action_type': 'sell', 'stock': 'B', 'amount': 100, 'price': 41.5} +2025-10-24 17:43:20,316 - Stocklogger - INFO - INFO: Agent 25 decide to action: {'action_type': 'sell', 'stock': 'B', 'amount': 100, 'price': 41.5} +2025-10-24 17:43:20,328 - Stocklogger - INFO - ACTION - BUY:38, SELL:25, STOCK:B, PRICE:41.5, AMOUNT:100 +2025-10-24 17:43:20,328 - Stocklogger - INFO - ACTION - BUY:38, SELL:25, STOCK:B, PRICE:41.5, AMOUNT:100 +2025-10-24 17:43:22,042 - Stocklogger - INFO - INFO: Agent 0 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 6000, 'price': 32.0} +2025-10-24 17:43:22,042 - Stocklogger - INFO - INFO: Agent 0 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 6000, 'price': 32.0} +2025-10-24 17:43:23,015 - Stocklogger - INFO - INFO: Agent 2 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 200, 'price': 41.5} +2025-10-24 17:43:23,015 - Stocklogger - INFO - INFO: Agent 2 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 200, 'price': 41.5} +2025-10-24 17:43:24,397 - Stocklogger - INFO - INFO: Agent 36 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 1000, 'price': 32.5} +2025-10-24 17:43:24,397 - Stocklogger - INFO - INFO: Agent 36 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 1000, 'price': 32.5} +2025-10-24 17:44:07,323 - Stocklogger - INFO - 🤖 Initializing LLM agents... +2025-10-24 17:44:07,323 - Stocklogger - INFO - 🤖 Initializing LLM agents... +2025-10-24 17:44:08,084 - Stocklogger - INFO - INFO: Agent 0 decide not to loan +2025-10-24 17:44:08,084 - Stocklogger - INFO - INFO: Agent 0 decide not to loan +2025-10-24 17:44:09,297 - Stocklogger - INFO - INFO: Agent 1 decide to loan: {'loan': 'yes', 'loan_type': 1, 'amount': 706263.3939059642, 'repayment_date': 45} +2025-10-24 17:44:09,297 - Stocklogger - INFO - INFO: Agent 1 decide to loan: {'loan': 'yes', 'loan_type': 1, 'amount': 706263.3939059642, 'repayment_date': 45} +2025-10-24 17:44:09,890 - Stocklogger - INFO - INFO: Agent 2 decide not to loan +2025-10-24 17:44:09,890 - Stocklogger - INFO - INFO: Agent 2 decide not to loan +2025-10-24 17:44:10,388 - Stocklogger - INFO - INFO: Agent 3 decide not to loan +2025-10-24 17:44:10,388 - Stocklogger - INFO - INFO: Agent 3 decide not to loan +2025-10-24 17:44:10,998 - Stocklogger - INFO - INFO: Agent 4 decide not to loan +2025-10-24 17:44:10,998 - Stocklogger - INFO - INFO: Agent 4 decide not to loan +2025-10-24 17:44:11,662 - Stocklogger - INFO - INFO: Agent 5 decide not to loan +2025-10-24 17:44:11,662 - Stocklogger - INFO - INFO: Agent 5 decide not to loan +2025-10-24 17:44:12,545 - Stocklogger - INFO - INFO: Agent 0 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 100, 'price': 41.0} +2025-10-24 17:44:12,545 - Stocklogger - INFO - INFO: Agent 0 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 100, 'price': 41.0} +2025-10-24 17:44:13,596 - Stocklogger - INFO - INFO: Agent 5 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 100, 'price': 41.5} +2025-10-24 17:44:13,596 - Stocklogger - INFO - INFO: Agent 5 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 100, 'price': 41.5} +2025-10-24 17:44:14,498 - Stocklogger - INFO - INFO: Agent 3 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 200, 'price': 41.0} +2025-10-24 17:44:14,498 - Stocklogger - INFO - INFO: Agent 3 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 200, 'price': 41.0} +2025-10-24 17:44:15,591 - Stocklogger - INFO - INFO: Agent 1 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 200, 'price': 42.0} +2025-10-24 17:44:15,591 - Stocklogger - INFO - INFO: Agent 1 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 200, 'price': 42.0} +2025-10-24 17:44:16,611 - Stocklogger - INFO - INFO: Agent 2 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 150, 'price': 42.5} +2025-10-24 17:44:16,611 - Stocklogger - INFO - INFO: Agent 2 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 150, 'price': 42.5} +2025-10-24 17:44:18,013 - Stocklogger - INFO - INFO: Agent 4 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 100, 'price': 42.0} +2025-10-24 17:44:18,013 - Stocklogger - INFO - INFO: Agent 4 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 100, 'price': 42.0} +2025-10-24 17:44:19,410 - Stocklogger - INFO - INFO: Agent 3 decide to action: {'action_type': 'sell', 'stock': 'B', 'amount': 100, 'price': 42.0} +2025-10-24 17:44:19,410 - Stocklogger - INFO - INFO: Agent 3 decide to action: {'action_type': 'sell', 'stock': 'B', 'amount': 100, 'price': 42.0} +2025-10-24 17:44:19,421 - Stocklogger - INFO - ACTION - BUY:1, SELL:3, STOCK:B, PRICE:42.0, AMOUNT:100 +2025-10-24 17:44:19,421 - Stocklogger - INFO - ACTION - BUY:1, SELL:3, STOCK:B, PRICE:42.0, AMOUNT:100 +2025-10-24 17:44:20,696 - Stocklogger - INFO - INFO: Agent 4 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 150, 'price': 42.5} +2025-10-24 17:44:20,696 - Stocklogger - INFO - INFO: Agent 4 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 150, 'price': 42.5} +2025-10-24 17:44:22,093 - Stocklogger - INFO - INFO: Agent 1 decide to action: {'action_type': 'sell', 'stock': 'B', 'amount': 200, 'price': 43.0} +2025-10-24 17:44:22,093 - Stocklogger - INFO - INFO: Agent 1 decide to action: {'action_type': 'sell', 'stock': 'B', 'amount': 200, 'price': 43.0} +2025-10-24 17:44:23,513 - Stocklogger - INFO - INFO: Agent 2 decide to action: {'action_type': 'sell', 'stock': 'B', 'amount': 100, 'price': 43.0} +2025-10-24 17:44:23,513 - Stocklogger - INFO - INFO: Agent 2 decide to action: {'action_type': 'sell', 'stock': 'B', 'amount': 100, 'price': 43.0} +2025-10-24 17:44:25,419 - Stocklogger - INFO - INFO: Agent 0 decide to action: {'action_type': 'sell', 'stock': 'B', 'amount': 200, 'price': 43.5} +2025-10-24 17:44:25,419 - Stocklogger - INFO - INFO: Agent 0 decide to action: {'action_type': 'sell', 'stock': 'B', 'amount': 200, 'price': 43.5} +2025-10-24 17:44:27,764 - Stocklogger - INFO - INFO: Agent 5 decide to action: {'action_type': 'sell', 'stock': 'B', 'amount': 100, 'price': 43.0} +2025-10-24 17:44:27,764 - Stocklogger - INFO - INFO: Agent 5 decide to action: {'action_type': 'sell', 'stock': 'B', 'amount': 100, 'price': 43.0} +2025-10-24 17:44:52,623 - Stocklogger - INFO - INFO: Agent 0 decide not to loan +2025-10-24 17:44:52,623 - Stocklogger - INFO - INFO: Agent 0 decide not to loan +2025-10-24 17:44:53,647 - Stocklogger - INFO - INFO: Agent 2 decide not to loan +2025-10-24 17:44:53,647 - Stocklogger - INFO - INFO: Agent 2 decide not to loan +2025-10-24 17:44:57,828 - Stocklogger - INFO - INFO: Agent 3 decide not to loan +2025-10-24 17:44:57,828 - Stocklogger - INFO - INFO: Agent 3 decide not to loan +2025-10-24 17:45:00,938 - Stocklogger - INFO - INFO: Agent 4 decide to loan: {'loan': 'yes', 'loan_type': 0, 'amount': 2000000, 'repayment_date': 24} +2025-10-24 17:45:00,938 - Stocklogger - INFO - INFO: Agent 4 decide to loan: {'loan': 'yes', 'loan_type': 0, 'amount': 2000000, 'repayment_date': 24} +2025-10-24 17:45:02,265 - Stocklogger - INFO - INFO: Agent 5 decide not to loan +2025-10-24 17:45:02,265 - Stocklogger - INFO - INFO: Agent 5 decide not to loan +2025-10-24 17:45:03,836 - Stocklogger - INFO - INFO: Agent 2 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 100, 'price': 42.5} +2025-10-24 17:45:03,836 - Stocklogger - INFO - INFO: Agent 2 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 100, 'price': 42.5} +2025-10-24 17:49:34,400 - Stocklogger - INFO - 🤖 Initializing LLM agents... +2025-10-24 17:49:34,400 - Stocklogger - INFO - 🤖 Initializing LLM agents... +2025-10-24 17:49:35,123 - Stocklogger - INFO - INFO: Agent 0 decide not to loan +2025-10-24 17:49:35,123 - Stocklogger - INFO - INFO: Agent 0 decide not to loan +2025-10-24 17:49:35,859 - Stocklogger - INFO - INFO: Agent 1 decide not to loan +2025-10-24 17:49:35,859 - Stocklogger - INFO - INFO: Agent 1 decide not to loan +2025-10-24 17:49:36,466 - Stocklogger - INFO - INFO: Agent 2 decide not to loan +2025-10-24 17:49:36,466 - Stocklogger - INFO - INFO: Agent 2 decide not to loan +2025-10-24 17:49:37,230 - Stocklogger - INFO - INFO: Agent 3 decide not to loan +2025-10-24 17:49:37,230 - Stocklogger - INFO - INFO: Agent 3 decide not to loan +2025-10-24 17:49:37,971 - Stocklogger - INFO - INFO: Agent 4 decide not to loan +2025-10-24 17:49:37,971 - Stocklogger - INFO - INFO: Agent 4 decide not to loan +2025-10-24 17:49:38,551 - Stocklogger - INFO - INFO: Agent 5 decide not to loan +2025-10-24 17:49:38,551 - Stocklogger - INFO - INFO: Agent 5 decide not to loan +2025-10-24 17:49:39,279 - Stocklogger - INFO - INFO: Agent 1 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 50, 'price': 40.5} +2025-10-24 17:49:39,279 - Stocklogger - INFO - INFO: Agent 1 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 50, 'price': 40.5} +2025-10-24 17:49:40,309 - Stocklogger - INFO - INFO: Agent 5 decide to action: {'action_type': 'sell', 'stock': 'B', 'amount': 100, 'price': 45.0} +2025-10-24 17:49:40,309 - Stocklogger - INFO - INFO: Agent 5 decide to action: {'action_type': 'sell', 'stock': 'B', 'amount': 100, 'price': 45.0} +2025-10-24 17:49:41,606 - Stocklogger - INFO - INFO: Agent 4 decide to action: {'action_type': 'sell', 'stock': 'B', 'amount': 50, 'price': 45.0} +2025-10-24 17:49:41,606 - Stocklogger - INFO - INFO: Agent 4 decide to action: {'action_type': 'sell', 'stock': 'B', 'amount': 50, 'price': 45.0} +2025-10-24 17:49:43,314 - Stocklogger - INFO - INFO: Agent 3 decide to action: {'action_type': 'sell', 'stock': 'B', 'amount': 100, 'price': 45.5} +2025-10-24 17:49:43,314 - Stocklogger - INFO - INFO: Agent 3 decide to action: {'action_type': 'sell', 'stock': 'B', 'amount': 100, 'price': 45.5} +2025-10-24 17:49:44,370 - Stocklogger - INFO - INFO: Agent 2 decide to action: {'action_type': 'sell', 'stock': 'B', 'amount': 100, 'price': 45.5} +2025-10-24 17:49:44,370 - Stocklogger - INFO - INFO: Agent 2 decide to action: {'action_type': 'sell', 'stock': 'B', 'amount': 100, 'price': 45.5} +2025-10-24 17:49:45,434 - Stocklogger - INFO - INFO: Agent 0 decide to action: {'action_type': 'sell', 'stock': 'B', 'amount': 100, 'price': 45.5} +2025-10-24 17:49:45,434 - Stocklogger - INFO - INFO: Agent 0 decide to action: {'action_type': 'sell', 'stock': 'B', 'amount': 100, 'price': 45.5} +2025-10-24 17:49:46,604 - Stocklogger - INFO - INFO: Agent 4 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 100, 'price': 41.0} +2025-10-24 17:49:46,604 - Stocklogger - INFO - INFO: Agent 4 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 100, 'price': 41.0} +2025-10-24 17:49:47,663 - Stocklogger - INFO - INFO: Agent 5 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 100, 'price': 41.5} +2025-10-24 17:49:47,663 - Stocklogger - INFO - INFO: Agent 5 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 100, 'price': 41.5} +2025-10-24 17:49:48,693 - Stocklogger - INFO - INFO: Agent 3 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 100, 'price': 41.0} +2025-10-24 17:49:48,693 - Stocklogger - INFO - INFO: Agent 3 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 100, 'price': 41.0} +2025-10-24 17:49:49,537 - Stocklogger - INFO - INFO: Agent 1 decide to action: {'action_type': 'sell', 'stock': 'B', 'amount': 100, 'price': 45.5} +2025-10-24 17:49:49,537 - Stocklogger - INFO - INFO: Agent 1 decide to action: {'action_type': 'sell', 'stock': 'B', 'amount': 100, 'price': 45.5} +2025-10-24 17:49:50,524 - Stocklogger - INFO - INFO: Agent 2 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 100, 'price': 41.0} +2025-10-24 17:49:50,524 - Stocklogger - INFO - INFO: Agent 2 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 100, 'price': 41.0} +2025-10-24 17:49:51,549 - Stocklogger - INFO - INFO: Agent 0 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 100, 'price': 41.0} +2025-10-24 17:49:51,549 - Stocklogger - INFO - INFO: Agent 0 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 100, 'price': 41.0} +2025-10-24 17:50:25,926 - Stocklogger - INFO - INFO: Agent 0 decide not to loan +2025-10-24 17:50:25,926 - Stocklogger - INFO - INFO: Agent 0 decide not to loan +2025-10-24 17:50:26,975 - Stocklogger - INFO - INFO: Agent 1 decide not to loan +2025-10-24 17:50:26,975 - Stocklogger - INFO - INFO: Agent 1 decide not to loan +2025-10-24 17:50:28,118 - Stocklogger - INFO - INFO: Agent 2 decide not to loan +2025-10-24 17:50:28,118 - Stocklogger - INFO - INFO: Agent 2 decide not to loan +2025-10-24 17:50:29,232 - Stocklogger - INFO - INFO: Agent 3 decide to loan: {'loan': 'yes', 'loan_type': 2, 'amount': 2251678.8665688443, 'repayment_date': 68} +2025-10-24 17:50:29,232 - Stocklogger - INFO - INFO: Agent 3 decide to loan: {'loan': 'yes', 'loan_type': 2, 'amount': 2251678.8665688443, 'repayment_date': 68} +2025-10-24 17:50:30,188 - Stocklogger - INFO - INFO: Agent 4 decide not to loan +2025-10-24 17:50:30,188 - Stocklogger - INFO - INFO: Agent 4 decide not to loan +2025-10-24 17:50:31,313 - Stocklogger - INFO - INFO: Agent 5 decide not to loan +2025-10-24 17:50:31,313 - Stocklogger - INFO - INFO: Agent 5 decide not to loan +2025-10-24 17:50:32,711 - Stocklogger - INFO - INFO: Agent 2 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 100, 'price': 41.0} +2025-10-24 17:50:32,711 - Stocklogger - INFO - INFO: Agent 2 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 100, 'price': 41.0} +2025-10-24 17:50:33,968 - Stocklogger - INFO - INFO: Agent 1 decide to action: {'action_type': 'sell', 'stock': 'B', 'amount': 100, 'price': 45.5} +2025-10-24 17:50:33,968 - Stocklogger - INFO - INFO: Agent 1 decide to action: {'action_type': 'sell', 'stock': 'B', 'amount': 100, 'price': 45.5} +2025-10-24 17:50:35,416 - Stocklogger - INFO - INFO: Agent 4 decide to action: {'action_type': 'sell', 'stock': 'B', 'amount': 100, 'price': 45.5} +2025-10-24 17:50:35,416 - Stocklogger - INFO - INFO: Agent 4 decide to action: {'action_type': 'sell', 'stock': 'B', 'amount': 100, 'price': 45.5} +2025-10-24 17:50:36,659 - Stocklogger - INFO - INFO: Agent 0 decide to action: {'action_type': 'sell', 'stock': 'B', 'amount': 100, 'price': 45.0} +2025-10-24 17:50:36,659 - Stocklogger - INFO - INFO: Agent 0 decide to action: {'action_type': 'sell', 'stock': 'B', 'amount': 100, 'price': 45.0} +2025-10-24 17:50:37,478 - Stocklogger - INFO - INFO: Agent 3 decide to action: {'action_type': 'sell', 'stock': 'B', 'amount': 100, 'price': 45.5} +2025-10-24 17:50:37,478 - Stocklogger - INFO - INFO: Agent 3 decide to action: {'action_type': 'sell', 'stock': 'B', 'amount': 100, 'price': 45.5} +2025-10-24 17:50:38,644 - Stocklogger - INFO - INFO: Agent 5 decide to action: {'action_type': 'sell', 'stock': 'B', 'amount': 1000, 'price': 45.0} +2025-10-24 17:50:38,644 - Stocklogger - INFO - INFO: Agent 5 decide to action: {'action_type': 'sell', 'stock': 'B', 'amount': 1000, 'price': 45.0} +2025-10-24 17:50:40,472 - Stocklogger - INFO - INFO: Agent 2 decide to action: {'action_type': 'sell', 'stock': 'B', 'amount': 100, 'price': 45.5} +2025-10-24 17:50:40,472 - Stocklogger - INFO - INFO: Agent 2 decide to action: {'action_type': 'sell', 'stock': 'B', 'amount': 100, 'price': 45.5} +2025-10-24 17:50:41,616 - Stocklogger - INFO - INFO: Agent 1 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 200, 'price': 41.0} +2025-10-24 17:50:41,616 - Stocklogger - INFO - INFO: Agent 1 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 200, 'price': 41.0} +2025-10-24 17:50:42,497 - Stocklogger - INFO - INFO: Agent 3 decide to action: {'action_type': 'sell', 'stock': 'B', 'amount': 200, 'price': 45.0} +2025-10-24 17:50:42,497 - Stocklogger - INFO - INFO: Agent 3 decide to action: {'action_type': 'sell', 'stock': 'B', 'amount': 200, 'price': 45.0} +2025-10-24 17:50:44,323 - Stocklogger - INFO - INFO: Agent 4 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 200, 'price': 41.0} +2025-10-24 17:50:44,323 - Stocklogger - INFO - INFO: Agent 4 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 200, 'price': 41.0} +2025-10-24 17:50:45,482 - Stocklogger - INFO - INFO: Agent 5 decide to action: {'action_type': 'sell', 'stock': 'B', 'amount': 500, 'price': 45.0} +2025-10-24 17:50:45,482 - Stocklogger - INFO - INFO: Agent 5 decide to action: {'action_type': 'sell', 'stock': 'B', 'amount': 500, 'price': 45.0} +2025-10-24 17:50:46,498 - Stocklogger - INFO - INFO: Agent 0 decide to action: {'action_type': 'sell', 'stock': 'B', 'amount': 100, 'price': 45.0} +2025-10-24 17:50:46,498 - Stocklogger - INFO - INFO: Agent 0 decide to action: {'action_type': 'sell', 'stock': 'B', 'amount': 100, 'price': 45.0} +2025-10-24 17:51:27,496 - Stocklogger - INFO - INFO: Agent 0 decide not to loan +2025-10-24 17:51:27,496 - Stocklogger - INFO - INFO: Agent 0 decide not to loan +2025-10-24 17:51:28,320 - Stocklogger - INFO - INFO: Agent 1 decide not to loan +2025-10-24 17:51:28,320 - Stocklogger - INFO - INFO: Agent 1 decide not to loan +2025-10-24 17:51:28,825 - Stocklogger - INFO - INFO: Agent 2 decide not to loan +2025-10-24 17:51:28,825 - Stocklogger - INFO - INFO: Agent 2 decide not to loan +2025-10-24 17:51:29,601 - Stocklogger - INFO - INFO: Agent 4 decide not to loan +2025-10-24 17:51:29,601 - Stocklogger - INFO - INFO: Agent 4 decide not to loan +2025-10-24 17:51:30,609 - Stocklogger - INFO - INFO: Agent 5 decide to loan: {'loan': 'yes', 'loan_type': 2, 'amount': 3034247.749039442, 'repayment_date': 69} +2025-10-24 17:51:30,609 - Stocklogger - INFO - INFO: Agent 5 decide to loan: {'loan': 'yes', 'loan_type': 2, 'amount': 3034247.749039442, 'repayment_date': 69} +2025-10-24 17:51:31,846 - Stocklogger - INFO - INFO: Agent 1 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 50, 'price': 41.0} +2025-10-24 17:51:31,846 - Stocklogger - INFO - INFO: Agent 1 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 50, 'price': 41.0} +2025-10-24 17:51:32,802 - Stocklogger - INFO - INFO: Agent 0 decide to action: {'action_type': 'sell', 'stock': 'B', 'amount': 50, 'price': 41.5} +2025-10-24 17:51:32,802 - Stocklogger - INFO - INFO: Agent 0 decide to action: {'action_type': 'sell', 'stock': 'B', 'amount': 50, 'price': 41.5} +2025-10-24 17:51:38,284 - Stocklogger - INFO - INFO: Agent 3 decide to action: {'action_type': 'sell', 'stock': 'B', 'amount': 50, 'price': 41.5} +2025-10-24 17:51:38,284 - Stocklogger - INFO - INFO: Agent 3 decide to action: {'action_type': 'sell', 'stock': 'B', 'amount': 50, 'price': 41.5} +2025-10-24 17:51:39,431 - Stocklogger - INFO - INFO: Agent 5 decide to action: {'action_type': 'sell', 'stock': 'B', 'amount': 100, 'price': 41.5} +2025-10-24 17:51:39,431 - Stocklogger - INFO - INFO: Agent 5 decide to action: {'action_type': 'sell', 'stock': 'B', 'amount': 100, 'price': 41.5} +2025-10-24 17:51:40,844 - Stocklogger - INFO - INFO: Agent 2 decide to action: {'action_type': 'sell', 'stock': 'B', 'amount': 100, 'price': 41.5} +2025-10-24 17:51:40,844 - Stocklogger - INFO - INFO: Agent 2 decide to action: {'action_type': 'sell', 'stock': 'B', 'amount': 100, 'price': 41.5} +2025-10-24 17:51:42,156 - Stocklogger - INFO - INFO: Agent 4 decide to action: {'action_type': 'sell', 'stock': 'B', 'amount': 100, 'price': 41.5} +2025-10-24 17:51:42,156 - Stocklogger - INFO - INFO: Agent 4 decide to action: {'action_type': 'sell', 'stock': 'B', 'amount': 100, 'price': 41.5} +2025-10-24 17:51:43,446 - Stocklogger - INFO - INFO: Agent 5 decide to action: {'action_type': 'sell', 'stock': 'B', 'amount': 100, 'price': 41.5} +2025-10-24 17:51:43,446 - Stocklogger - INFO - INFO: Agent 5 decide to action: {'action_type': 'sell', 'stock': 'B', 'amount': 100, 'price': 41.5} +2025-10-24 17:51:44,538 - Stocklogger - INFO - INFO: Agent 4 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 100, 'price': 39.5} +2025-10-24 17:51:44,538 - Stocklogger - INFO - INFO: Agent 4 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 100, 'price': 39.5} +2025-10-24 17:51:45,694 - Stocklogger - INFO - INFO: Agent 1 decide to action: {'action_type': 'sell', 'stock': 'B', 'amount': 100, 'price': 41.5} +2025-10-24 17:51:45,694 - Stocklogger - INFO - INFO: Agent 1 decide to action: {'action_type': 'sell', 'stock': 'B', 'amount': 100, 'price': 41.5} +2025-10-24 17:51:48,680 - Stocklogger - INFO - INFO: Agent 3 decide to action: {'action_type': 'sell', 'stock': 'B', 'amount': 100, 'price': 41.5} +2025-10-24 17:51:48,680 - Stocklogger - INFO - INFO: Agent 3 decide to action: {'action_type': 'sell', 'stock': 'B', 'amount': 100, 'price': 41.5} +2025-10-24 17:51:49,966 - Stocklogger - INFO - INFO: Agent 2 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 100, 'price': 39.0} +2025-10-24 17:51:49,966 - Stocklogger - INFO - INFO: Agent 2 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 100, 'price': 39.0} +2025-10-24 17:51:50,998 - Stocklogger - INFO - INFO: Agent 0 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 100, 'price': 39.0} +2025-10-24 17:51:50,998 - Stocklogger - INFO - INFO: Agent 0 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 100, 'price': 39.0} +2025-10-24 17:53:59,891 - Stocklogger - INFO - 🤖 Initializing LLM agents... +2025-10-24 17:53:59,891 - Stocklogger - INFO - 🤖 Initializing LLM agents... +2025-10-24 17:54:00,625 - Stocklogger - INFO - INFO: Agent 0 decide not to loan +2025-10-24 17:54:00,625 - Stocklogger - INFO - INFO: Agent 0 decide not to loan +2025-10-24 17:54:01,684 - Stocklogger - INFO - INFO: Agent 1 decide to loan: {'loan': 'yes', 'loan_type': 1, 'amount': 200000, 'repayment_date': 45} +2025-10-24 17:54:01,684 - Stocklogger - INFO - INFO: Agent 1 decide to loan: {'loan': 'yes', 'loan_type': 1, 'amount': 200000, 'repayment_date': 45} +2025-10-24 17:54:02,371 - Stocklogger - INFO - INFO: Agent 2 decide not to loan +2025-10-24 17:54:02,371 - Stocklogger - INFO - INFO: Agent 2 decide not to loan +2025-10-24 17:54:03,195 - Stocklogger - INFO - INFO: Agent 3 decide not to loan +2025-10-24 17:54:03,195 - Stocklogger - INFO - INFO: Agent 3 decide not to loan +2025-10-24 17:54:03,859 - Stocklogger - INFO - INFO: Agent 4 decide not to loan +2025-10-24 17:54:03,859 - Stocklogger - INFO - INFO: Agent 4 decide not to loan +2025-10-24 17:54:04,633 - Stocklogger - INFO - INFO: Agent 5 decide not to loan +2025-10-24 17:54:04,633 - Stocklogger - INFO - INFO: Agent 5 decide not to loan +2025-10-24 17:54:05,318 - Stocklogger - INFO - INFO: Agent 6 decide not to loan +2025-10-24 17:54:05,318 - Stocklogger - INFO - INFO: Agent 6 decide not to loan +2025-10-24 17:54:06,545 - Stocklogger - INFO - INFO: Agent 7 decide not to loan +2025-10-24 17:54:06,545 - Stocklogger - INFO - INFO: Agent 7 decide not to loan +2025-10-24 17:54:07,282 - Stocklogger - INFO - INFO: Agent 8 decide not to loan +2025-10-24 17:54:07,282 - Stocklogger - INFO - INFO: Agent 8 decide not to loan +2025-10-24 17:54:07,748 - Stocklogger - INFO - INFO: Agent 9 decide not to loan +2025-10-24 17:54:07,748 - Stocklogger - INFO - INFO: Agent 9 decide not to loan +2025-10-24 17:54:09,384 - Stocklogger - INFO - INFO: Agent 2 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 100, 'price': 41.0} +2025-10-24 17:54:09,384 - Stocklogger - INFO - INFO: Agent 2 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 100, 'price': 41.0} +2025-10-24 17:54:10,426 - Stocklogger - INFO - INFO: Agent 0 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 50, 'price': 30.5} +2025-10-24 17:54:10,426 - Stocklogger - INFO - INFO: Agent 0 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 50, 'price': 30.5} +2025-10-24 17:54:11,790 - Stocklogger - INFO - INFO: Agent 5 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 100, 'price': 31.0} +2025-10-24 17:54:11,790 - Stocklogger - INFO - INFO: Agent 5 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 100, 'price': 31.0} +2025-10-24 17:54:13,612 - Stocklogger - INFO - INFO: Agent 9 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 100, 'price': 31.5} +2025-10-24 17:54:13,612 - Stocklogger - INFO - INFO: Agent 9 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 100, 'price': 31.5} +2025-10-24 17:54:14,757 - Stocklogger - INFO - INFO: Agent 1 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 200, 'price': 32.0} +2025-10-24 17:54:14,757 - Stocklogger - INFO - INFO: Agent 1 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 200, 'price': 32.0} +2025-10-24 17:54:15,381 - Stocklogger - INFO - INFO: Agent 4 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 100, 'price': 31.5} +2025-10-24 17:54:15,381 - Stocklogger - INFO - INFO: Agent 4 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 100, 'price': 31.5} +2025-10-24 17:54:16,610 - Stocklogger - INFO - INFO: Agent 3 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 200, 'price': 32.0} +2025-10-24 17:54:16,610 - Stocklogger - INFO - INFO: Agent 3 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 200, 'price': 32.0} +2025-10-24 17:54:17,681 - Stocklogger - INFO - INFO: Agent 8 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 200, 'price': 32.0} +2025-10-24 17:54:17,681 - Stocklogger - INFO - INFO: Agent 8 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 200, 'price': 32.0} +2025-10-24 17:54:18,741 - Stocklogger - INFO - INFO: Agent 6 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 200, 'price': 32.0} +2025-10-24 17:54:18,741 - Stocklogger - INFO - INFO: Agent 6 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 200, 'price': 32.0} +2025-10-24 17:54:20,000 - Stocklogger - INFO - INFO: Agent 7 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 200, 'price': 32.0} +2025-10-24 17:54:20,000 - Stocklogger - INFO - INFO: Agent 7 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 200, 'price': 32.0} +2025-10-24 17:54:21,480 - Stocklogger - INFO - INFO: Agent 4 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 200, 'price': 32.0} +2025-10-24 17:54:21,480 - Stocklogger - INFO - INFO: Agent 4 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 200, 'price': 32.0} +2025-10-24 17:54:22,906 - Stocklogger - INFO - INFO: Agent 2 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 100, 'price': 32.5} +2025-10-24 17:54:22,906 - Stocklogger - INFO - INFO: Agent 2 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 100, 'price': 32.5} +2025-10-24 17:54:23,860 - Stocklogger - INFO - INFO: Agent 8 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 200, 'price': 32.0} +2025-10-24 17:54:23,860 - Stocklogger - INFO - INFO: Agent 8 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 200, 'price': 32.0} +2025-10-24 17:54:25,036 - Stocklogger - INFO - INFO: Agent 5 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 150, 'price': 41.5} +2025-10-24 17:54:25,036 - Stocklogger - INFO - INFO: Agent 5 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 150, 'price': 41.5} +2025-10-24 17:54:26,017 - Stocklogger - INFO - INFO: Agent 1 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 200, 'price': 41.0} +2025-10-24 17:54:26,017 - Stocklogger - INFO - INFO: Agent 1 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 200, 'price': 41.0} +2025-10-24 17:54:26,904 - Stocklogger - INFO - INFO: Agent 7 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 200, 'price': 32.0} +2025-10-24 17:54:26,904 - Stocklogger - INFO - INFO: Agent 7 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 200, 'price': 32.0} +2025-10-24 17:54:27,767 - Stocklogger - INFO - INFO: Agent 9 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 150, 'price': 41.0} +2025-10-24 17:54:27,767 - Stocklogger - INFO - INFO: Agent 9 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 150, 'price': 41.0} +2025-10-24 17:54:28,834 - Stocklogger - INFO - INFO: Agent 6 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 200, 'price': 41.0} +2025-10-24 17:54:28,834 - Stocklogger - INFO - INFO: Agent 6 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 200, 'price': 41.0} +2025-10-24 17:54:30,736 - Stocklogger - INFO - INFO: Agent 0 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 100, 'price': 41.5} +2025-10-24 17:54:30,736 - Stocklogger - INFO - INFO: Agent 0 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 100, 'price': 41.5} +2025-10-24 17:54:32,183 - Stocklogger - INFO - INFO: Agent 3 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 100, 'price': 41.0} +2025-10-24 17:54:32,183 - Stocklogger - INFO - INFO: Agent 3 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 100, 'price': 41.0} +2025-10-24 17:54:33,344 - Stocklogger - INFO - INFO: Agent 4 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 150, 'price': 41.0} +2025-10-24 17:54:33,344 - Stocklogger - INFO - INFO: Agent 4 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 150, 'price': 41.0} +2025-10-24 17:54:34,381 - Stocklogger - INFO - INFO: Agent 8 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 100, 'price': 41.0} +2025-10-24 17:54:34,381 - Stocklogger - INFO - INFO: Agent 8 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 100, 'price': 41.0} +2025-10-24 17:54:36,893 - Stocklogger - INFO - INFO: Agent 5 decide to action: {'action_type': 'sell', 'stock': 'B', 'amount': 200, 'price': 42.0} +2025-10-24 17:54:36,893 - Stocklogger - INFO - INFO: Agent 5 decide to action: {'action_type': 'sell', 'stock': 'B', 'amount': 200, 'price': 42.0} +2025-10-24 17:54:37,784 - Stocklogger - INFO - INFO: Agent 2 decide to action: {'action_type': 'sell', 'stock': 'B', 'amount': 100, 'price': 42.5} +2025-10-24 17:54:37,784 - Stocklogger - INFO - INFO: Agent 2 decide to action: {'action_type': 'sell', 'stock': 'B', 'amount': 100, 'price': 42.5} +2025-10-24 17:54:38,893 - Stocklogger - INFO - INFO: Agent 0 decide to action: {'action_type': 'sell', 'stock': 'B', 'amount': 100, 'price': 42.0} +2025-10-24 17:54:38,893 - Stocklogger - INFO - INFO: Agent 0 decide to action: {'action_type': 'sell', 'stock': 'B', 'amount': 100, 'price': 42.0} +2025-10-24 17:54:39,948 - Stocklogger - INFO - INFO: Agent 7 decide to action: {'action_type': 'sell', 'stock': 'B', 'amount': 100, 'price': 42.0} +2025-10-24 17:54:39,948 - Stocklogger - INFO - INFO: Agent 7 decide to action: {'action_type': 'sell', 'stock': 'B', 'amount': 100, 'price': 42.0} +2025-10-24 17:54:40,962 - Stocklogger - INFO - INFO: Agent 9 decide to action: {'action_type': 'sell', 'stock': 'B', 'amount': 100, 'price': 42.5} +2025-10-24 17:54:40,962 - Stocklogger - INFO - INFO: Agent 9 decide to action: {'action_type': 'sell', 'stock': 'B', 'amount': 100, 'price': 42.5} +2025-10-24 17:54:41,942 - Stocklogger - INFO - INFO: Agent 6 decide to action: {'action_type': 'sell', 'stock': 'B', 'amount': 100, 'price': 42.0} +2025-10-24 17:54:41,942 - Stocklogger - INFO - INFO: Agent 6 decide to action: {'action_type': 'sell', 'stock': 'B', 'amount': 100, 'price': 42.0} +2025-10-24 17:54:43,207 - Stocklogger - INFO - INFO: Agent 1 decide to action: {'action_type': 'sell', 'stock': 'B', 'amount': 200, 'price': 42.5} +2025-10-24 17:54:43,207 - Stocklogger - INFO - INFO: Agent 1 decide to action: {'action_type': 'sell', 'stock': 'B', 'amount': 200, 'price': 42.5} +2025-10-24 17:54:44,708 - Stocklogger - INFO - INFO: Agent 3 decide to action: {'action_type': 'sell', 'stock': 'B', 'amount': 200, 'price': 42.0} +2025-10-24 17:54:44,708 - Stocklogger - INFO - INFO: Agent 3 decide to action: {'action_type': 'sell', 'stock': 'B', 'amount': 200, 'price': 42.0} +2025-10-24 17:54:45,559 - Stocklogger - INFO - INFO: Agent 2 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 100, 'price': 41.5} +2025-10-24 17:54:45,559 - Stocklogger - INFO - INFO: Agent 2 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 100, 'price': 41.5} +2025-10-24 17:54:46,810 - Stocklogger - INFO - INFO: Agent 3 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 100, 'price': 41.0} +2025-10-24 17:54:46,810 - Stocklogger - INFO - INFO: Agent 3 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 100, 'price': 41.0} +2025-10-24 17:54:48,445 - Stocklogger - INFO - INFO: Agent 9 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 100, 'price': 41.5} +2025-10-24 17:54:48,445 - Stocklogger - INFO - INFO: Agent 9 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 100, 'price': 41.5} +2025-10-24 17:54:50,367 - Stocklogger - INFO - INFO: Agent 0 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 100, 'price': 41.5} +2025-10-24 17:54:50,367 - Stocklogger - INFO - INFO: Agent 0 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 100, 'price': 41.5} +2025-10-24 17:54:54,183 - Stocklogger - INFO - INFO: Agent 6 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 150, 'price': 41.0} +2025-10-24 17:54:54,183 - Stocklogger - INFO - INFO: Agent 6 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 150, 'price': 41.0} +2025-10-24 17:54:56,539 - Stocklogger - INFO - INFO: Agent 5 decide to action: {'action_type': 'sell', 'stock': 'B', 'amount': 250, 'price': 42.5} +2025-10-24 17:54:56,539 - Stocklogger - INFO - INFO: Agent 5 decide to action: {'action_type': 'sell', 'stock': 'B', 'amount': 250, 'price': 42.5} +2025-10-24 17:54:58,058 - Stocklogger - INFO - INFO: Agent 7 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 100, 'price': 41.0} +2025-10-24 17:54:58,058 - Stocklogger - INFO - INFO: Agent 7 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 100, 'price': 41.0} +2025-10-24 17:54:58,983 - Stocklogger - INFO - INFO: Agent 4 decide to action: {'action_type': 'sell', 'stock': 'B', 'amount': 100, 'price': 42.0} +2025-10-24 17:54:58,983 - Stocklogger - INFO - INFO: Agent 4 decide to action: {'action_type': 'sell', 'stock': 'B', 'amount': 100, 'price': 42.0} +2025-10-24 17:55:00,072 - Stocklogger - INFO - INFO: Agent 8 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 100, 'price': 41.0} +2025-10-24 17:55:00,072 - Stocklogger - INFO - INFO: Agent 8 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 100, 'price': 41.0} +2025-10-24 17:55:01,389 - Stocklogger - INFO - INFO: Agent 1 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 300, 'price': 41.5} +2025-10-24 17:55:01,389 - Stocklogger - INFO - INFO: Agent 1 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 300, 'price': 41.5} +2025-10-24 17:55:03,713 - Stocklogger - INFO - INFO: Agent 6 decide to action: {'action_type': 'sell', 'stock': 'B', 'amount': 200, 'price': 42.5} +2025-10-24 17:55:03,713 - Stocklogger - INFO - INFO: Agent 6 decide to action: {'action_type': 'sell', 'stock': 'B', 'amount': 200, 'price': 42.5} +2025-10-24 17:55:04,634 - Stocklogger - INFO - INFO: Agent 0 decide to action: {'action_type': 'sell', 'stock': 'B', 'amount': 100, 'price': 42.5} +2025-10-24 17:55:04,634 - Stocklogger - INFO - INFO: Agent 0 decide to action: {'action_type': 'sell', 'stock': 'B', 'amount': 100, 'price': 42.5} +2025-10-24 17:55:06,847 - Stocklogger - INFO - INFO: Agent 3 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 200, 'price': 41.0} +2025-10-24 17:55:06,847 - Stocklogger - INFO - INFO: Agent 3 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 200, 'price': 41.0} +2025-10-24 17:55:08,255 - Stocklogger - INFO - INFO: Agent 9 decide to action: {'action_type': 'sell', 'stock': 'B', 'amount': 200, 'price': 42.5} +2025-10-24 17:55:08,255 - Stocklogger - INFO - INFO: Agent 9 decide to action: {'action_type': 'sell', 'stock': 'B', 'amount': 200, 'price': 42.5} +2025-10-24 17:55:09,953 - Stocklogger - INFO - INFO: Agent 7 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 150, 'price': 41.5} +2025-10-24 17:55:09,953 - Stocklogger - INFO - INFO: Agent 7 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 150, 'price': 41.5} +2025-10-24 17:55:11,749 - Stocklogger - INFO - INFO: Agent 1 decide to action: {'action_type': 'sell', 'stock': 'B', 'amount': 300, 'price': 42.5} +2025-10-24 17:55:11,749 - Stocklogger - INFO - INFO: Agent 1 decide to action: {'action_type': 'sell', 'stock': 'B', 'amount': 300, 'price': 42.5} +2025-10-24 17:55:12,739 - Stocklogger - INFO - INFO: Agent 4 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 200, 'price': 41.0} +2025-10-24 17:55:12,739 - Stocklogger - INFO - INFO: Agent 4 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 200, 'price': 41.0} +2025-10-24 17:55:14,325 - Stocklogger - INFO - INFO: Agent 5 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 300, 'price': 41.0} +2025-10-24 17:55:14,325 - Stocklogger - INFO - INFO: Agent 5 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 300, 'price': 41.0} +2025-10-24 17:55:15,538 - Stocklogger - INFO - INFO: Agent 8 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 200, 'price': 41.0} +2025-10-24 17:55:15,538 - Stocklogger - INFO - INFO: Agent 8 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 200, 'price': 41.0} +2025-10-24 17:55:16,775 - Stocklogger - INFO - INFO: Agent 2 decide to action: {'action_type': 'sell', 'stock': 'B', 'amount': 200, 'price': 42.5} +2025-10-24 17:55:16,775 - Stocklogger - INFO - INFO: Agent 2 decide to action: {'action_type': 'sell', 'stock': 'B', 'amount': 200, 'price': 42.5} +2025-10-24 18:10:23,077 - Stocklogger - INFO - 🤖 Initializing LLM agents... +2025-10-24 18:10:23,077 - Stocklogger - INFO - 🤖 Initializing LLM agents... +2025-10-24 18:10:25,453 - Stocklogger - INFO - INFO: Agent 0 decide to loan: {'loan': 'yes', 'loan_type': 1, 'amount': 648148.4391193846, 'repayment_date': 45} +2025-10-24 18:10:25,453 - Stocklogger - INFO - INFO: Agent 0 decide to loan: {'loan': 'yes', 'loan_type': 1, 'amount': 648148.4391193846, 'repayment_date': 45} +2025-10-24 18:10:26,971 - Stocklogger - INFO - INFO: Agent 1 decide not to loan +2025-10-24 18:10:26,971 - Stocklogger - INFO - INFO: Agent 1 decide not to loan +2025-10-24 18:10:27,578 - Stocklogger - INFO - INFO: Agent 2 decide not to loan +2025-10-24 18:10:27,578 - Stocklogger - INFO - INFO: Agent 2 decide not to loan +2025-10-24 18:10:29,094 - Stocklogger - INFO - INFO: Agent 3 decide to loan: {'loan': 'yes', 'loan_type': 1, 'amount': 1415116.1833927361, 'repayment_date': 45} +2025-10-24 18:10:29,094 - Stocklogger - INFO - INFO: Agent 3 decide to loan: {'loan': 'yes', 'loan_type': 1, 'amount': 1415116.1833927361, 'repayment_date': 45} +2025-10-24 18:10:30,195 - Stocklogger - INFO - INFO: Agent 4 decide not to loan +2025-10-24 18:10:30,195 - Stocklogger - INFO - INFO: Agent 4 decide not to loan +2025-10-24 18:10:31,223 - Stocklogger - INFO - INFO: Agent 5 decide not to loan +2025-10-24 18:10:31,223 - Stocklogger - INFO - INFO: Agent 5 decide not to loan +2025-10-24 18:10:32,541 - Stocklogger - INFO - INFO: Agent 6 decide not to loan +2025-10-24 18:10:32,541 - Stocklogger - INFO - INFO: Agent 6 decide not to loan +2025-10-24 18:10:33,966 - Stocklogger - INFO - INFO: Agent 7 decide not to loan +2025-10-24 18:10:33,966 - Stocklogger - INFO - INFO: Agent 7 decide not to loan +2025-10-24 18:10:35,336 - Stocklogger - INFO - INFO: Agent 8 decide not to loan +2025-10-24 18:10:35,336 - Stocklogger - INFO - INFO: Agent 8 decide not to loan +2025-10-24 18:10:36,058 - Stocklogger - INFO - INFO: Agent 9 decide to loan: {'loan': 'yes', 'loan_type': 2, 'amount': 200000, 'repayment_date': 67} +2025-10-24 18:10:36,058 - Stocklogger - INFO - INFO: Agent 9 decide to loan: {'loan': 'yes', 'loan_type': 2, 'amount': 200000, 'repayment_date': 67} +2025-10-24 18:10:36,990 - Stocklogger - INFO - INFO: Agent 5 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 100, 'price': 42.0} +2025-10-24 18:10:36,990 - Stocklogger - INFO - INFO: Agent 5 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 100, 'price': 42.0} +2025-10-24 18:10:37,957 - Stocklogger - INFO - INFO: Agent 9 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 100, 'price': 42.5} +2025-10-24 18:10:37,957 - Stocklogger - INFO - INFO: Agent 9 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 100, 'price': 42.5} +2025-10-24 18:10:39,026 - Stocklogger - INFO - INFO: Agent 3 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 3000, 'price': 30.5} +2025-10-24 18:10:39,026 - Stocklogger - INFO - INFO: Agent 3 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 3000, 'price': 30.5} +2025-10-24 18:10:40,628 - Stocklogger - INFO - INFO: Agent 7 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 3000, 'price': 30.5} +2025-10-24 18:10:40,628 - Stocklogger - INFO - INFO: Agent 7 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 3000, 'price': 30.5} +2025-10-24 18:10:42,339 - Stocklogger - INFO - INFO: Agent 2 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 6000, 'price': 30.5} +2025-10-24 18:10:42,339 - Stocklogger - INFO - INFO: Agent 2 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 6000, 'price': 30.5} +2025-10-24 18:10:43,538 - Stocklogger - INFO - INFO: Agent 4 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 6000, 'price': 30.5} +2025-10-24 18:10:43,538 - Stocklogger - INFO - INFO: Agent 4 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 6000, 'price': 30.5} +2025-10-24 18:10:45,255 - Stocklogger - DEBUG - Sell more than hold: {"action_type": "sell", "stock": "A", "amount": 6000, "price": 30.5} +2025-10-24 18:10:45,255 - Stocklogger - DEBUG - Sell more than hold: {"action_type": "sell", "stock": "A", "amount": 6000, "price": 30.5} +2025-10-24 18:10:47,101 - Stocklogger - INFO - INFO: Agent 1 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 1000, 'price': 30.5} +2025-10-24 18:10:47,101 - Stocklogger - INFO - INFO: Agent 1 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 1000, 'price': 30.5} +2025-10-24 18:10:48,228 - Stocklogger - INFO - INFO: Agent 0 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 6000, 'price': 30.5} +2025-10-24 18:10:48,228 - Stocklogger - INFO - INFO: Agent 0 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 6000, 'price': 30.5} +2025-10-24 18:10:49,390 - Stocklogger - INFO - INFO: Agent 6 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 6000, 'price': 30.5} +2025-10-24 18:10:49,390 - Stocklogger - INFO - INFO: Agent 6 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 6000, 'price': 30.5} +2025-10-24 18:10:50,435 - Stocklogger - INFO - INFO: Agent 8 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 6000, 'price': 30.5} +2025-10-24 18:10:50,435 - Stocklogger - INFO - INFO: Agent 8 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 6000, 'price': 30.5} +2025-10-24 18:10:51,897 - Stocklogger - INFO - INFO: Agent 9 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 3000, 'price': 31.0} +2025-10-24 18:10:51,897 - Stocklogger - INFO - INFO: Agent 9 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 3000, 'price': 31.0} +2025-10-24 18:10:53,619 - Stocklogger - INFO - INFO: Agent 0 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 200, 'price': 41.5} +2025-10-24 18:10:53,619 - Stocklogger - INFO - INFO: Agent 0 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 200, 'price': 41.5} +2025-10-24 18:10:55,906 - Stocklogger - INFO - INFO: Agent 1 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 1000, 'price': 30.0} +2025-10-24 18:10:55,906 - Stocklogger - INFO - INFO: Agent 1 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 1000, 'price': 30.0} +2025-10-24 18:10:57,219 - Stocklogger - INFO - INFO: Agent 2 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 200, 'price': 41.5} +2025-10-24 18:10:57,219 - Stocklogger - INFO - INFO: Agent 2 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 200, 'price': 41.5} +2025-10-24 18:10:58,138 - Stocklogger - INFO - INFO: Agent 8 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 200, 'price': 41.5} +2025-10-24 18:10:58,138 - Stocklogger - INFO - INFO: Agent 8 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 200, 'price': 41.5} +2025-10-24 18:11:00,202 - Stocklogger - INFO - INFO: Agent 6 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 200, 'price': 41.5} +2025-10-24 18:11:00,202 - Stocklogger - INFO - INFO: Agent 6 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 200, 'price': 41.5} +2025-10-24 18:11:01,251 - Stocklogger - INFO - INFO: Agent 4 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 500, 'price': 41.5} +2025-10-24 18:11:01,251 - Stocklogger - INFO - INFO: Agent 4 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 500, 'price': 41.5} +2025-10-24 18:11:02,373 - Stocklogger - INFO - INFO: Agent 7 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 200, 'price': 41.5} +2025-10-24 18:11:02,373 - Stocklogger - INFO - INFO: Agent 7 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 200, 'price': 41.5} +2025-10-24 18:11:04,534 - Stocklogger - INFO - INFO: Agent 5 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 6000, 'price': 31.0} +2025-10-24 18:11:04,534 - Stocklogger - INFO - INFO: Agent 5 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 6000, 'price': 31.0} +2025-10-24 18:11:06,196 - Stocklogger - INFO - INFO: Agent 3 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 300, 'price': 41.0} +2025-10-24 18:11:06,196 - Stocklogger - INFO - INFO: Agent 3 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 300, 'price': 41.0} +2025-10-24 18:11:08,868 - Stocklogger - INFO - INFO: Agent 2 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 6000, 'price': 30.5} +2025-10-24 18:11:08,868 - Stocklogger - INFO - INFO: Agent 2 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 6000, 'price': 30.5} +2025-10-24 18:11:10,631 - Stocklogger - INFO - INFO: Agent 9 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 200, 'price': 41.0} +2025-10-24 18:11:10,631 - Stocklogger - INFO - INFO: Agent 9 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 200, 'price': 41.0} +2025-10-24 18:11:11,946 - Stocklogger - INFO - INFO: Agent 8 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 6000, 'price': 30.5} +2025-10-24 18:11:11,946 - Stocklogger - INFO - INFO: Agent 8 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 6000, 'price': 30.5} +2025-10-24 18:11:13,181 - Stocklogger - INFO - INFO: Agent 3 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 400, 'price': 41.0} +2025-10-24 18:11:13,181 - Stocklogger - INFO - INFO: Agent 3 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 400, 'price': 41.0} +2025-10-24 18:11:14,121 - Stocklogger - INFO - INFO: Agent 7 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 300, 'price': 41.0} +2025-10-24 18:11:14,121 - Stocklogger - INFO - INFO: Agent 7 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 300, 'price': 41.0} +2025-10-24 18:11:15,952 - Stocklogger - INFO - INFO: Agent 6 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 6000, 'price': 31.0} +2025-10-24 18:11:15,952 - Stocklogger - INFO - INFO: Agent 6 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 6000, 'price': 31.0} +2025-10-24 18:11:17,102 - Stocklogger - INFO - INFO: Agent 5 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 400, 'price': 41.0} +2025-10-24 18:11:17,102 - Stocklogger - INFO - INFO: Agent 5 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 400, 'price': 41.0} +2025-10-24 18:11:21,756 - Stocklogger - INFO - INFO: Agent 0 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 300, 'price': 41.0} +2025-10-24 18:11:21,756 - Stocklogger - INFO - INFO: Agent 0 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 300, 'price': 41.0} +2025-10-24 18:11:22,982 - Stocklogger - INFO - INFO: Agent 4 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 6000, 'price': 31.0} +2025-10-24 18:11:22,982 - Stocklogger - INFO - INFO: Agent 4 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 6000, 'price': 31.0} +2025-10-24 18:11:24,245 - Stocklogger - INFO - INFO: Agent 1 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 1000, 'price': 41.0} +2025-10-24 18:11:24,245 - Stocklogger - INFO - INFO: Agent 1 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 1000, 'price': 41.0} +2025-10-24 18:11:25,297 - Stocklogger - INFO - INFO: Agent 6 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 400, 'price': 41.0} +2025-10-24 18:11:25,297 - Stocklogger - INFO - INFO: Agent 6 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 400, 'price': 41.0} +2025-10-24 18:11:26,552 - Stocklogger - INFO - INFO: Agent 8 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 300, 'price': 41.0} +2025-10-24 18:11:26,552 - Stocklogger - INFO - INFO: Agent 8 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 300, 'price': 41.0} +2025-10-24 18:11:28,091 - Stocklogger - INFO - INFO: Agent 1 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 500, 'price': 41.0} +2025-10-24 18:11:28,091 - Stocklogger - INFO - INFO: Agent 1 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 500, 'price': 41.0} +2025-10-24 18:11:29,578 - Stocklogger - INFO - INFO: Agent 2 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 500, 'price': 41.0} +2025-10-24 18:11:29,578 - Stocklogger - INFO - INFO: Agent 2 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 500, 'price': 41.0} +2025-10-24 18:11:31,086 - Stocklogger - INFO - INFO: Agent 9 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 300, 'price': 41.0} +2025-10-24 18:11:31,086 - Stocklogger - INFO - INFO: Agent 9 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 300, 'price': 41.0} +2025-10-24 18:11:42,385 - Stocklogger - INFO - INFO: Agent 0 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 5000, 'price': 31.0} +2025-10-24 18:11:42,385 - Stocklogger - INFO - INFO: Agent 0 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 5000, 'price': 31.0} +2025-10-24 18:11:44,490 - Stocklogger - INFO - INFO: Agent 5 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 500, 'price': 41.0} +2025-10-24 18:11:44,490 - Stocklogger - INFO - INFO: Agent 5 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 500, 'price': 41.0} +2025-10-24 18:11:45,612 - Stocklogger - INFO - INFO: Agent 3 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 600, 'price': 41.0} +2025-10-24 18:11:45,612 - Stocklogger - INFO - INFO: Agent 3 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 600, 'price': 41.0} +2025-10-24 18:11:47,460 - Stocklogger - INFO - INFO: Agent 4 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 600, 'price': 41.0} +2025-10-24 18:11:47,460 - Stocklogger - INFO - INFO: Agent 4 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 600, 'price': 41.0} +2025-10-24 18:11:49,284 - Stocklogger - INFO - INFO: Agent 7 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 600, 'price': 41.0} +2025-10-24 18:11:49,284 - Stocklogger - INFO - INFO: Agent 7 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 600, 'price': 41.0} +2025-10-24 18:11:50,722 - Stocklogger - INFO - INFO: Agent 4 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 600, 'price': 41.0} +2025-10-24 18:11:50,722 - Stocklogger - INFO - INFO: Agent 4 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 600, 'price': 41.0} +2025-10-24 18:11:52,412 - Stocklogger - INFO - INFO: Agent 5 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 5000, 'price': 31.0} +2025-10-24 18:11:52,412 - Stocklogger - INFO - INFO: Agent 5 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 5000, 'price': 31.0} +2025-10-24 18:11:53,444 - Stocklogger - INFO - INFO: Agent 3 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 5000, 'price': 31.0} +2025-10-24 18:11:53,444 - Stocklogger - INFO - INFO: Agent 3 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 5000, 'price': 31.0} +2025-10-24 18:11:54,929 - Stocklogger - INFO - INFO: Agent 1 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 1000, 'price': 41.0} +2025-10-24 18:11:54,929 - Stocklogger - INFO - INFO: Agent 1 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 1000, 'price': 41.0} +2025-10-24 18:12:07,331 - Stocklogger - INFO - INFO: Agent 0 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 8000, 'price': 31.5} +2025-10-24 18:12:07,331 - Stocklogger - INFO - INFO: Agent 0 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 8000, 'price': 31.5} +2025-10-24 18:12:09,200 - Stocklogger - INFO - INFO: Agent 6 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 5000, 'price': 31.0} +2025-10-24 18:12:09,200 - Stocklogger - INFO - INFO: Agent 6 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 5000, 'price': 31.0} +2025-10-24 18:12:10,677 - Stocklogger - INFO - INFO: Agent 8 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 5000, 'price': 31.0} +2025-10-24 18:12:10,677 - Stocklogger - INFO - INFO: Agent 8 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 5000, 'price': 31.0} +2025-10-24 18:12:12,418 - Stocklogger - INFO - INFO: Agent 9 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 5000, 'price': 31.5} +2025-10-24 18:12:12,418 - Stocklogger - INFO - INFO: Agent 9 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 5000, 'price': 31.5} +2025-10-24 18:12:16,954 - Stocklogger - INFO - INFO: Agent 2 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 600, 'price': 41.0} +2025-10-24 18:12:16,954 - Stocklogger - INFO - INFO: Agent 2 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 600, 'price': 41.0} +2025-10-24 18:12:18,100 - Stocklogger - INFO - INFO: Agent 7 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 5000, 'price': 31.5} +2025-10-24 18:12:18,100 - Stocklogger - INFO - INFO: Agent 7 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 5000, 'price': 31.5} +2025-10-24 18:13:04,861 - Stocklogger - INFO - INFO: Agent 1 decide not to loan +2025-10-24 18:13:04,861 - Stocklogger - INFO - INFO: Agent 1 decide not to loan +2025-10-24 18:13:05,469 - Stocklogger - INFO - INFO: Agent 2 decide not to loan +2025-10-24 18:13:05,469 - Stocklogger - INFO - INFO: Agent 2 decide not to loan +2025-10-24 18:13:45,252 - Stocklogger - INFO - 🤖 Initializing LLM agents... +2025-10-24 18:13:45,252 - Stocklogger - INFO - 🤖 Initializing LLM agents... +2025-10-24 18:13:45,931 - Stocklogger - INFO - INFO: Agent 0 decide not to loan +2025-10-24 18:13:45,931 - Stocklogger - INFO - INFO: Agent 0 decide not to loan +2025-10-24 18:13:46,756 - Stocklogger - INFO - INFO: Agent 1 decide not to loan +2025-10-24 18:13:46,756 - Stocklogger - INFO - INFO: Agent 1 decide not to loan +2025-10-24 18:13:47,352 - Stocklogger - INFO - INFO: Agent 2 decide not to loan +2025-10-24 18:13:47,352 - Stocklogger - INFO - INFO: Agent 2 decide not to loan +2025-10-24 18:13:48,030 - Stocklogger - INFO - INFO: Agent 3 decide not to loan +2025-10-24 18:13:48,030 - Stocklogger - INFO - INFO: Agent 3 decide not to loan +2025-10-24 18:13:49,189 - Stocklogger - INFO - INFO: Agent 4 decide not to loan +2025-10-24 18:13:49,189 - Stocklogger - INFO - INFO: Agent 4 decide not to loan +2025-10-24 18:13:50,174 - Stocklogger - INFO - INFO: Agent 3 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 100, 'price': 41.0} +2025-10-24 18:13:50,174 - Stocklogger - INFO - INFO: Agent 3 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 100, 'price': 41.0} +2025-10-24 18:13:52,047 - Stocklogger - INFO - INFO: Agent 4 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 100, 'price': 30.5} +2025-10-24 18:13:52,047 - Stocklogger - INFO - INFO: Agent 4 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 100, 'price': 30.5} +2025-10-24 18:13:52,907 - Stocklogger - INFO - INFO: Agent 2 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 100, 'price': 30.5} +2025-10-24 18:13:52,907 - Stocklogger - INFO - INFO: Agent 2 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 100, 'price': 30.5} +2025-10-24 18:13:53,771 - Stocklogger - INFO - INFO: Agent 0 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 200, 'price': 30.8} +2025-10-24 18:13:53,771 - Stocklogger - INFO - INFO: Agent 0 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 200, 'price': 30.8} +2025-10-24 18:13:54,691 - Stocklogger - INFO - INFO: Agent 1 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 200, 'price': 30.9} +2025-10-24 18:13:54,691 - Stocklogger - INFO - INFO: Agent 1 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 200, 'price': 30.9} +2025-10-24 18:13:55,603 - Stocklogger - INFO - INFO: Agent 4 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 150, 'price': 41.5} +2025-10-24 18:13:55,603 - Stocklogger - INFO - INFO: Agent 4 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 150, 'price': 41.5} +2025-10-24 18:13:57,112 - Stocklogger - INFO - INFO: Agent 3 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 200, 'price': 31.0} +2025-10-24 18:13:57,112 - Stocklogger - INFO - INFO: Agent 3 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 200, 'price': 31.0} +2025-10-24 18:13:58,327 - Stocklogger - INFO - INFO: Agent 0 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 150, 'price': 41.0} +2025-10-24 18:13:58,327 - Stocklogger - INFO - INFO: Agent 0 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 150, 'price': 41.0} +2025-10-24 18:13:59,477 - Stocklogger - INFO - INFO: Agent 1 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 200, 'price': 41.5} +2025-10-24 18:13:59,477 - Stocklogger - INFO - INFO: Agent 1 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 200, 'price': 41.5} +2025-10-24 18:14:00,461 - Stocklogger - INFO - INFO: Agent 2 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 100, 'price': 41.5} +2025-10-24 18:14:00,461 - Stocklogger - INFO - INFO: Agent 2 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 100, 'price': 41.5} +2025-10-24 18:14:30,504 - Stocklogger - INFO - INFO: Agent 0 decide not to loan +2025-10-24 18:14:30,504 - Stocklogger - INFO - INFO: Agent 0 decide not to loan +2025-10-24 18:14:34,542 - Stocklogger - INFO - INFO: Agent 1 decide to loan: {'loan': 'yes', 'loan_type': 1, 'amount': 205178.1873629014, 'repayment_date': 46} +2025-10-24 18:14:34,542 - Stocklogger - INFO - INFO: Agent 1 decide to loan: {'loan': 'yes', 'loan_type': 1, 'amount': 205178.1873629014, 'repayment_date': 46} +2025-10-24 18:14:35,675 - Stocklogger - INFO - INFO: Agent 2 decide to loan: {'loan': 'yes', 'loan_type': 0, 'amount': 100000, 'repayment_date': 24} +2025-10-24 18:14:35,675 - Stocklogger - INFO - INFO: Agent 2 decide to loan: {'loan': 'yes', 'loan_type': 0, 'amount': 100000, 'repayment_date': 24} +2025-10-24 18:14:36,279 - Stocklogger - INFO - INFO: Agent 3 decide not to loan +2025-10-24 18:14:36,279 - Stocklogger - INFO - INFO: Agent 3 decide not to loan +2025-10-24 18:14:37,821 - Stocklogger - INFO - INFO: Agent 4 decide not to loan +2025-10-24 18:14:37,821 - Stocklogger - INFO - INFO: Agent 4 decide not to loan +2025-10-24 18:14:38,967 - Stocklogger - INFO - INFO: Agent 3 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 100, 'price': 40.5} +2025-10-24 18:14:38,967 - Stocklogger - INFO - INFO: Agent 3 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 100, 'price': 40.5} +2025-10-24 18:14:42,031 - Stocklogger - INFO - INFO: Agent 1 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 200, 'price': 40.5} +2025-10-24 18:14:42,031 - Stocklogger - INFO - INFO: Agent 1 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 200, 'price': 40.5} +2025-10-24 18:14:42,660 - Stocklogger - INFO - INFO: Agent 2 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 250, 'price': 41.0} +2025-10-24 18:14:42,660 - Stocklogger - INFO - INFO: Agent 2 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 250, 'price': 41.0} +2025-10-24 18:14:43,637 - Stocklogger - INFO - INFO: Agent 0 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 300, 'price': 41.0} +2025-10-24 18:14:43,637 - Stocklogger - INFO - INFO: Agent 0 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 300, 'price': 41.0} +2025-10-24 18:14:44,775 - Stocklogger - INFO - INFO: Agent 4 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 1000, 'price': 30.5} +2025-10-24 18:14:44,775 - Stocklogger - INFO - INFO: Agent 4 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 1000, 'price': 30.5} +2025-10-24 18:14:50,903 - Stocklogger - INFO - INFO: Agent 1 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 300, 'price': 41.0} +2025-10-24 18:14:50,903 - Stocklogger - INFO - INFO: Agent 1 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 300, 'price': 41.0} +2025-10-24 18:14:51,698 - Stocklogger - INFO - INFO: Agent 0 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 1000, 'price': 30.5} +2025-10-24 18:14:51,698 - Stocklogger - INFO - INFO: Agent 0 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 1000, 'price': 30.5} +2025-10-24 18:14:52,728 - Stocklogger - INFO - INFO: Agent 3 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 1000, 'price': 30.5} +2025-10-24 18:14:52,728 - Stocklogger - INFO - INFO: Agent 3 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 1000, 'price': 30.5} +2025-10-24 18:14:54,158 - Stocklogger - INFO - INFO: Agent 2 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 1000, 'price': 31.0} +2025-10-24 18:14:54,158 - Stocklogger - INFO - INFO: Agent 2 decide to action: {'action_type': 'sell', 'stock': 'A', 'amount': 1000, 'price': 31.0} +2025-10-24 18:14:55,663 - Stocklogger - INFO - INFO: Agent 4 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 500, 'price': 41.0} +2025-10-24 18:14:55,663 - Stocklogger - INFO - INFO: Agent 4 decide to action: {'action_type': 'buy', 'stock': 'B', 'amount': 500, 'price': 41.0} diff --git a/examples/Stockagent/main.py b/examples/Stockagent/main.py new file mode 100644 index 0000000..f5a1ec0 --- /dev/null +++ b/examples/Stockagent/main.py @@ -0,0 +1,208 @@ +import argparse +import random +import pandas as pd +import openai +import tiktoken + +import util +from agent import Agent +from secretary import Secretary +from stock import Stock +from log.custom_logger import log +from record import create_stock_record, create_trade_record, AgentRecordDaily, create_agentses_record + +def get_agent(all_agents, order): + for agent in all_agents: + if agent.order == order: + return agent + return None + +def handle_action(action, stock_deals, all_agents, stock, session): + # action = JSON{"agent": 1, "action_type": "buy"|"sell", "stock": "A"|"B", "amount": 10, "price": 10} + try: + if action["action_type"] == "buy": + for sell_action in stock_deals["sell"][:]: + if action["price"] == sell_action["price"]: + # 交易成交 + close_amount = min(action["amount"], sell_action["amount"]) + get_agent(all_agents, action["agent"]).buy_stock(stock.name, close_amount, action["price"]) + if not sell_action["agent"] == -1: # B发行 + get_agent(all_agents, sell_action["agent"]).sell_stock(stock.name, close_amount, action["price"]) + stock.add_session_deal({"price": action["price"], "amount": close_amount}) + create_trade_record(action["date"], session, stock.name, action["agent"], sell_action["agent"], + close_amount, action["price"]) + + if action["amount"] > close_amount: # 买单未结束,卖单结束,继续循环 + log.logger.info(f"ACTION - BUY:{action['agent']}, SELL:{sell_action['agent']}, " + f"STOCK:{stock.name}, PRICE:{action['price']}, AMOUNT:{close_amount}") + stock_deals["sell"].remove(sell_action) + action["amount"] -= close_amount + else: # 卖单未结束,买单结束 + log.logger.info(f"ACTION - BUY:{action['agent']}, SELL:{sell_action['agent']}, " + f"STOCK:{stock.name}, PRICE:{action['price']}, AMOUNT:{close_amount}") + sell_action["amount"] -= close_amount + return + # 遍历卖单后仍然有剩余 + stock_deals["buy"].append(action) + + else: + for buy_action in stock_deals["buy"][:]: + if action["price"] == buy_action["price"]: + # 交易成交 + close_amount = min(action["amount"], buy_action["amount"]) + get_agent(all_agents, action["agent"]).sell_stock(stock.name, close_amount, action["price"]) + get_agent(all_agents, buy_action["agent"]).buy_stock(stock.name, close_amount, action["price"]) + stock.add_session_deal({"price": action["price"], "amount": close_amount}) + create_trade_record(action["date"], session, stock.name, buy_action["agent"], action["agent"], + close_amount, action["price"]) + + if action["amount"] > close_amount: # 卖单未结束,买单结束,继续循环 + log.logger.info(f"ACTION - BUY:{buy_action['agent']}, SELL:{action['agent']}, " + f"STOCK:{stock.name}, PRICE:{action['price']}, AMOUNT:{close_amount}") + stock_deals["buy"].remove(buy_action) + action["amount"] -= close_amount + else: # 买单未结束,卖单结束 + log.logger.info(f"ACTION - BUY:{buy_action['agent']}, SELL:{action['agent']}, " + f"STOCK:{stock.name}, PRICE:{action['price']}, AMOUNT:{close_amount}") + buy_action["amount"] -= close_amount + return + stock_deals["sell"].append(action) + except Exception as e: + log.logger.error(f"handle_action error: {e}") + return + + +def simulation(args): + # init + secretary = Secretary(args.model) + stock_a = Stock("A", util.STOCK_A_INITIAL_PRICE, 0, is_new=False) + #stock_b = Stock("B", util.STOCK_B_INITIAL_PRICE, util.STOCK_B_PUBLISH, is_new=True) + stock_b = Stock("B", util.STOCK_B_INITIAL_PRICE, 0, is_new=False) + all_agents = [] + log.logger.debug("Agents initial...") + for i in range(0, util.AGENTS_NUM): # agents start from 0, -1 refers to admin + agent = Agent(i, stock_a.get_price(), stock_b.get_price(), secretary, args.model) + all_agents.append(agent) + log.logger.debug("cash: {}, stock a: {}, stock b:{}, debt: {}".format(agent.cash, agent.stock_a_amount, + agent.stock_b_amount, agent.loans)) + + # start simulation + last_day_forum_message = [] + stock_a_deals = {"sell": [], "buy": []} + stock_b_deals = {"sell": [], "buy": []} + # stock b publish + # stock_b_deals["sell"].append({"agent": -1, "amount": util.STOCK_B_PUBLISH, "price": util.STOCK_B_INITIAL_PRICE}) + + log.logger.debug("--------Simulation Start!--------") + for date in range(1, util.TOTAL_DATE + 1): + + log.logger.debug(f"--------DAY {date}---------") + # 除b发行外,删除前一天的所有交易 + stock_a_deals["sell"].clear() + stock_a_deals["buy"].clear() + stock_b_deals["buy"].clear() + + # tmp_action = next((action for action in stock_b_deals["sell"] if action["agent"] == -1), None) + stock_b_deals["sell"].clear() + # if tmp_action: + # tmp_action["price"] *= 0.9 # B发行折价 + # if tmp_action["price"] < 1: + # log.logger.warning("WARNING: STOCK B WITHDRAW FROM MARKET!!!") + # stock_b_deals["sell"].append(tmp_action) + + # check if an agent needs to repay loans + for agent in all_agents[:]: + agent.chat_history.clear() # 只保存当天的聊天记录 + agent.loan_repayment(date) + + # repayment days + if date in util.REPAYMENT_DAYS: + for agent in all_agents[:]: + agent.interest_payment() + + # deal with cash<0 agents + for agent in all_agents[:]: + if agent.is_bankrupt: + quit_sig = agent.bankrupt_process(stock_a.get_price(), stock_b.get_price()) + if quit_sig: + agent.quit = True + all_agents.remove(agent) + + # special events + if date == util.EVENT_1_DAY: + util.LOAN_RATE = util.EVENT_1_LOAN_RATE + last_day_forum_message.append({"name": -1, "message": util.EVENT_1_MESSAGE}) + if date == util.EVENT_2_DAY: + util.LOAN_RATE = util.EVENT_2_LOAN_RATE + last_day_forum_message.append({"name": -1, "message": util.EVENT_2_MESSAGE}) + + # agent decide whether to loan + daily_agent_records = [] + for agent in all_agents: + loan = agent.plan_loan(date, stock_a.get_price(), stock_b.get_price(), last_day_forum_message) + daily_agent_records.append(AgentRecordDaily(date, agent.order, loan)) + + for session in range(1, util.TOTAL_SESSION + 1): + log.logger.debug(f"SESSION {session}") + # 随机定义交易顺序 + sequence = list(range(len(all_agents))) + random.shuffle(sequence) + for i in sequence: + agent = all_agents[i] + # if agent.is_bankrupt: # cash<0的当天停止交易,交易时段结束后贩卖股票 + # continue + + action = agent.plan_stock(date, session, stock_a, stock_b, stock_a_deals, stock_b_deals) + proper, cash, valua_a, value_b = agent.get_proper_cash_value(stock_a.get_price(), stock_b.get_price()) + create_agentses_record(agent.order, date, session, proper, cash, valua_a, value_b, action) + action["agent"] = agent.order + action["date"] = date + if not action["action_type"] == "no": + if action["stock"] == 'A': + handle_action(action, stock_a_deals, all_agents, stock_a, session) + else: + handle_action(action, stock_b_deals, all_agents, stock_b, session) + + # 交易时段结束,更新股票价格 + stock_a.update_price(date) + stock_b.update_price(date) + create_stock_record(date, session, stock_a.get_price(), stock_b.get_price()) + + + # agent预测明天行动 + for idx, agent in enumerate(all_agents): + estimation = agent.next_day_estimate() + log.logger.info("Agent {} tomorrow estimation: {}".format(agent.order, estimation)) + if idx >= len(daily_agent_records): + break + daily_agent_records[idx].add_estimate(estimation) + daily_agent_records[idx].write_to_excel() + daily_agent_records.clear() + + # 交易日结束,论坛信息更新 + last_day_forum_message.clear() + log.logger.debug(f"DAY {date} ends, display forum messages...") + for agent in all_agents: + chat_history = agent.chat_history + message = agent.post_message() + log.logger.info("Agent {} says: {}".format(agent.order, message)) + last_day_forum_message.append({"name": agent.order, "message": message}) + + + + log.logger.debug("--------Simulation finished!--------") + log.logger.debug("--------Agents action history--------") + # for agent in all_agents: + # log.logger.debug(f"Agent {agent.order} action history:") + # log.logger.info(agent.action_history) + # log.logger.debug("--------Stock deal history--------") + # for stock in [stock_a, stock_b]: + # log.logger.debug(f"Stock {stock.name} deal history:") + # log.logger.info(stock.history) + + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument("--model", type=str, default="gemini-pro", help="model name") + args = parser.parse_args() + simulation(args) diff --git a/examples/Stockagent/prompt/agent_prompt.py b/examples/Stockagent/prompt/agent_prompt.py new file mode 100644 index 0000000..c363584 --- /dev/null +++ b/examples/Stockagent/prompt/agent_prompt.py @@ -0,0 +1,292 @@ +from procoder.prompt import * + +# BACKGROUND_PROMPT = NamedBlock( +# name="Background", +# content=""" +# 你是一名股票交易员,接下来你将在市场中模拟与其他交易员的交互。市场中一共有两支股票,分别为A和B,其中B为新上市的股票。 +# 接下来,请根据指令完成你的交易行动。 +# """ +# ) + +BACKGROUND_PROMPT = NamedBlock( + name="Background", + content=""" + You are a stock trader, and next you will simulate interactions with other traders in the market. + There are two stocks in the market, A and B, where B is the newly listed stock. + Next, please complete your trading actions according to the order. + """ +) + +# LASTDAY_FORUM_AND_STOCK_PROMPT = NamedBlock( +# name="Last Day Forum and Stock", +# content=""" +# 昨天交易截止后,A公司股票和B公司股票的股价分别是{stock_a_price}元/股和{stock_b_price}元/股。 +# 其他交易员在论坛上发布的帖子如下: +# {lastday_forum_message} +# """ +# ) + +LASTDAY_FORUM_AND_STOCK_PROMPT = NamedBlock( + name="Last Day Forum and Stock", + content=""" + After the close of trading yesterday, the stock prices of Company A and Company B + were {stock_a_price} dollars per share and {stock_b_price} dollars per share, respectively. + Posts by other traders on the forum are as follows: {lastday_forum_message} + """ +) + +# LOAN_TYPE_PROMPT = NamedVariable( +# refname="loan_type_prompt", +# name="Loan Type", +# content=""" +# 0. 1年期,基准利率{loan_rate1} +# 1. 2年期,基准利率{loan_rate2} +# 2. 3年期,基准利率{loan_rate3} +# """ +# ) + +LOAN_TYPE_PROMPT = NamedVariable( + refname="loan_type_prompt", + name="Loan Type", + content=""" + 0. 22days, the benchmark interest rate {loan_rate1} + 1. 44days, the benchmark interest rate {loan_rate2} + 2. 66days, the benchmark interest rate {loan_rate3} + """ +) + +# DECIDE_IF_LOAN_PROMPT = NamedBlock( +# name="Instruction", +# content=""" +# 现在是第{date}天,你当前的性格是{character},持有{stock_a}股A公司股票,持有{stock_b}股B公司股票, +# 现在你有{cash}元现金,贷款情况为{debt}。 +# 你需要决定是否继续贷款和贷款金额。 +# 可供选择的种类为{loan_type_prompt},你应当用编号选择一个贷款种类。贷款金额不得超过{max_loan}。 +# 用json形式返回结果,例如: +# {{"loan": "yes", "loan_type": 3, "amount": 1000}} +# 如果不需贷款,则返回: +# {{"loan" : "no"}} +# """ +# ) + +DECIDE_IF_LOAN_PROMPT = NamedBlock( + name="Instruction", + content=""" + It is the {date} day, and your current character is {character}. + You hold {stock_a} shares of Company A, {stock_b} shares of Company B, + Now you have {cash} dollars in cash and {debt} in your loan situation. + You need to decide whether to continue the loan and the amount of the loan. + The alternative type is {loan_type_prompt}, and you should use the number to select a loan type. + The loan amount shall not exceed {max_loan}. + + Return the result as json, for example: + {{"loan": "yes", "loan_type": 3, "amount": 1000}} + + If no loan is required, return: + {{"loan" : "no"}} + """ +) + +# LOAN_RETRY_PROMPT = NamedBlock( +# name="Instruction", +# content=""" +# The following questions appeared in the loan format you last answered: {fail_response}. +# 你应当用json形式返回结果,例如: +# {{"loan": "yes", "loan_type": 2, "amount": 1000}} +# 如果不需贷款,则返回: +# {{"loan" : "no"}} +# Please answer again.""" +# ) + +LOAN_RETRY_PROMPT = NamedBlock( + name="Instruction", + content=""" + The following questions appeared in the loan format you last answered: {fail_response}. + You should return the results as json, for example: + {{"loan": "yes", "loan_type": 2, "amount": 1000}} + If no loan is required, return: + {{"loan" : "no"}} + Please answer again.""" +) + +# DECIDE_BUY_STOCK_PROMPT = NamedBlock( +# name="Instruction", +# content=""" +# 现在是第{date}天的{time}交易时段,前一时段结束后,A公司的股票股价为{stock_a_price},B公司的股票股价为{stock_b_price}。 +# 在目前时段,股票A的买卖盘为{stock_a_deals},股票B的买卖盘为{stock_b_deals} +# 你当前持有{stock_a}股A公司股票,持有{stock_b}股B公司股票,{cash}元现金。 +# 你需要决定是否购买/卖出A公司或B公司的股票,以及购买/卖出的数量与价格。你可以参考当前股价和大盘自己决定价格,无需确定为当前股价。数量必须为整数。 +# 鼓励尽可能多地买入和卖出。 +# 用json形式返回结果,例如: +# {{"action_type":"buy"|"sell", "stock":"A"|"B", amount: 100, price : 30}} +# 如果既不购买也不卖出,则返回: +# {{"action_type" : "no"}}""" +# ) + +DECIDE_BUY_STOCK_PROMPT = NamedBlock( + name="Instruction", + content=""" + It is the {time} trading session on the {date} day, and after the previous session, + the stock price of Company A is {stock_a_price} and the stock price of Company B is {stock_b_price}. + In the current session, the buy and sell order of stock A is {stock_a_deals}, + and the buy and sell order of stock B is {stock_b_deals} + You currently hold {stock_a} shares of Company A, {stock_b} shares of Company B, and {cash} yuan in cash. + You need to decide whether to buy/sell shares of Company A or Company B, and how much to buy/sell and at what price. + You can refer to the current share price and the market to determine the price yourself, not the current share price. + The quantity must be an integer. + We encourage you to buy and sell more. You can only answer one json action. + Return the result as json, for example: + {{"action_type":"buy"|"sell", "stock":"A"|"B", amount: 100, price : 30.1}} + If neither buy nor sell, return: + {{"action_type" : "no"}} + """ +) + +# BUY_STOCK_RETRY_PROMPT = NamedBlock( +# name="Instruction", +# content=""" +# The following questions appeared in the action format you last answered: {fail_response}. +# 你应当用json形式返回结果,例如: +# {{"action_type":"buy"|"sell", "stock":"A"|"B", amount: 100, price: 30}} +# 如果既不购买也不卖出,则返回: +# {{"action_type" : "no"}} +# Please answer again.""" +# ) + +BUY_STOCK_RETRY_PROMPT = NamedBlock( + name="Instruction", + content=""" + The following questions appeared in the action format you last answered: {fail_response}. + You should return the result as json, for example: + {{"action_type":"buy"|"sell", "stock":"A"|"B", amount: 100, price: 30.1}} + If neither buy nor sell, return: + {{"action_type" : "no"}} + Please answer again. You can only answer one json action. + """ +) + +# FIRST_DAY_FINANCIAL_REPORT = NamedVariable( +# refname="first_day_financial_report", +# name="The initial financial situation of Stock A and B", +# content=""" +# ●公司A:这只股票超级棒! +# ●公司B:这只股票风险大收益大!""" +# ) + +FIRST_DAY_FINANCIAL_REPORT = NamedVariable( + refname="first_day_financial_prompt", + name="The last 3 years financial report of Stock A and B", + content=""" + The following lists the financial data for the past three years, covering a total of twelve quarters. + Stock A: + Revenue million: 3696.19, 3578.00, 3595.49, 3215.64, 3973.40, 3810.57, 3840.70, 3433.02, 4344.52, 4095.22, 4114.16, 3717.96 + Net profit million: 127.711441, 217.9586418, 360.756337, 358.08228, 650.8868033, 693.3022798, 433.2338757, 517.0593354, 712.7358875, 628.310145, 250.5046675, 325.5147258 + Cash flow million: 30.0950631, 135.4141818, 344.3249477, 279.5563512, 564.624197, 642.8122273, 350.3899245, 493.4058465, 650.6526937, 579.0037013, 185.7066407, 273.1287018 + Stock B: + Revenue million: 570.00, 774.00, 643.00, 995.00, 684.46, 934.37, 782.08, 1204.05, 788.29, 1100.32, 914.96, 1418.37 + Net profit million: 85.9691, 142.086, 87.5419224, 135.7643678, 132.7973368, 169.6505746, 194.9436163, 272.1084953, 225.1707811, 356.7201332 + Cash flow million: 68.97, 90.171, 82.1754, 124.773, 75.4954968, 123.5240842, 132.7191287, 153.7571212, 194.9436163, 261.1053212, 216.3871992, 345.6568448 + """ +) + +FIRST_DAY_BACKGROUND_KNOWLEDGE = NamedBlock( + name="The initial financial situation of Stock A and B", + content=""" + + Company A has been listed for 10 years, deeply rooted in the chemical industry. However, the company's operations + have encountered bottlenecks, with revenues declining over the past three years. + Although Company A's performance has declined over the past five years, the overall trend is stable. With the recent + CEO change and the exploration of new business avenues, the new CEO appears more proactive compared to the + previous one. The future operational outlook is expected to improve. + + Company B, as a technology company, has just been listed for three years and is in a period of business growth. + Last year, its revenue declined due to the overall tech environment, but the company's operations remain robust. + According to the latest corporate news, it is expected that the future revenue growth rate will return to over 20%. + In the short term, the stock price is expected to continue rising. + While Company B's operations are good, there is a history of concealing critical data before its IPO, casting doubt + on the reliability of its revenue. + Company B recently received government inquiries regarding recent operational and stock price fluctuations, and it + provided explanations while committing to allocate more resources to social services. + + The government recently held talks with both Company A and Company B, actively encouraging their contributions + to society. Subsequently, agreements on government subsidies were signed with both companies. + + The last 3 years financial report of stock A and B is listed in {first_day_financial_prompt}. + """ +) + +# SEASONAL_FINANCIAL_REPORT = NamedVariable( +# refname="seasonal_financial_report", +# name="The Seasonal financial report of Stock A and B", +# content=""" +# Stock A: {stock_a_report} +# Stock B: {stock_b_report} +# """ +# ) + +SEASONAL_FINANCIAL_REPORT = NamedVariable( + refname="seasonal_financial_report", + name="The Seasonal financial report of Stock A and B", + content=""" + Stock A: {stock_a_report} + Stock B: {stock_b_report} + """ +) + +# POST_MESSAGE_PROMPT = NamedBlock( +# refname="post_message", +# name="Instruction", +# content=""" +# 当前交易日结束了,请在论坛上简短地发表你的交易心得,并将其发布在论坛上。你发布的内容将对所有交易员公开可见。回答中只包含需要发布的内容。""" +# ) + +POST_MESSAGE_PROMPT = NamedBlock( + refname="post_message", + name="Instruction", + content=""" + The current trading day is over, please briefly post your trading tips on the forum and post them on the forum. + What you post will be publicly visible to all traders. The responses contain only what needs to be posted. + """ +) + +# NEXT_DAY_ESTIMATE_PROMPT = NamedBlock( +# refname="next_day_estimate", +# name="Instruction", +# content=""" +# 请根据当前交易日的大盘信息和论坛信息,预估明天你是否会买入、卖出股票A和股票B,以及是否会选择贷款。预计会进行的行动标记为yes,不会进行标记为no。 +# 用json格式返回结果,例如: +# {{"buy_A":"yes", "buy_B":"no", "sell_A":"yes", "sell_B": "no", "loan": "yes"}} +# """ +# ) + +NEXT_DAY_ESTIMATE_PROMPT = NamedBlock( + refname="next_day_estimate", + name="Instruction", + content=""" + Based on the market information and forum information of the current trading day, + please estimate whether you will buy and sell stock A and stock B tomorrow, and whether you will choose loan. + Actions that are expected to take place are marked yes, and actions that will not take place are marked no. + Return the result in json format, for example: + {{"buy_A":"yes", "buy_B":"no", "sell_A":"yes", "sell_B": "no", "loan": "yes"}} + """ +) + +# NEXT_DAY_ESTIMATE_RETRY = NamedBlock( +# refname="next_day_estimate_retry", +# name="Instruction", +# content=""" +# The following questions appeared in the JSON format you last answered: {fail_response}. +# 用json格式返回结果,例如: +# {{"buy_A":"yes", "buy_B":"no", "sell_A":"yes", "sell_B": "no", "loan": "yes"}} +# """ +# ) + +NEXT_DAY_ESTIMATE_RETRY = NamedBlock( + refname="next_day_estimate_retry", + name="Instruction", + content=""" + The following questions appeared in the JSON format you last answered: {fail_response}. + Return the result in json format, for example: + {{"buy_A":"yes", "buy_B":"no", "sell_A":"yes", "sell_B": "no", "loan": "yes"}} + """ +) \ No newline at end of file diff --git a/examples/Stockagent/record.py b/examples/Stockagent/record.py new file mode 100644 index 0000000..13685e8 --- /dev/null +++ b/examples/Stockagent/record.py @@ -0,0 +1,141 @@ +import pandas as pd +import os + +# 交易记录 +class TradeRecord: + def __init__(self, date, session, stock_type, buyer, seller, quantity, price): + self.date = date + self.session = session + self.stock_type = stock_type + self.buyer = buyer + self.seller = seller + self.quantity = quantity + self.price = price + + def write_to_excel(self, file_name="res/trades.xlsx"): + if os.path.isfile(file_name): + existing_df = pd.read_excel(file_name) + else: + existing_df = pd.DataFrame(columns=["交易日", "交易阶段", "股票类型", "买入交易员", "卖出交易员", "交易数量", "交易价格"]) + + # 将新的交易记录合并到现有DataFrame + new_records = [[self.date, self.session, self.stock_type, self.buyer, self.seller, self.quantity, self.price]] + new_df = pd.DataFrame(new_records, columns=existing_df.columns) + all_records_df = pd.concat([existing_df, new_df], ignore_index=True) + + # 将所有记录写入到Excel文件 + all_records_df.to_excel(file_name, index=False) + +def create_trade_record(date, stage, stock, buy_trader, sell_trader, amount, price): + record = TradeRecord(date, stage, stock, buy_trader, sell_trader, amount, price) + record.write_to_excel() + record = None + +# 将交易记录列表写入Excel文件(如果文件不存在则创建) + +class StockRecord: + def __init__(self, date, session, stock_a_price, stock_b_price): + self.date = date + self.session = session + self.stock_a_price = stock_a_price + self.stock_b_price = stock_b_price + + def write_to_excel(self, file_name="res/stocks.xlsx"): + if os.path.isfile(file_name): + existing_df = pd.read_excel(file_name) + else: + existing_df = pd.DataFrame(columns=["交易日", "第几个交易阶段", "阶段结束后股票A价格", "阶段结束后股票B价格"]) + + # 将新的交易记录合并到现有DataFrame + new_records = [[self.date, self.session, self.stock_a_price, self.stock_b_price]] + new_df = pd.DataFrame(new_records, columns=existing_df.columns) + all_records_df = pd.concat([existing_df, new_df], ignore_index=True) + + # 将所有记录写入到Excel文件 + all_records_df.to_excel(file_name, index=False) + +def create_stock_record(date, session, stock_a_price, stock_b_price): + record = StockRecord(date, session, stock_a_price, stock_b_price) + record.write_to_excel() + record = None + + +class AgentRecordDaily: + def __init__(self, agent, date, loan_json): + self.agent = agent + self.date = date + self.if_loan = loan_json["loan"] + self.loan_type = 0 + self.loan_amount = 0 + if self.if_loan == "yes": + self.loan_type = loan_json["loan_type"] + self.loan_amount = loan_json["amount"] + self.will_loan = "no" + self.will_buy_a = "no" + self.will_sell_a = "no" + self.will_buy_b = "no" + self.will_sell_b = "no" + + def add_estimate(self, js): + self.will_loan = js["loan"] + self.will_buy_a = js["buy_A"] + self.will_sell_a = js["sell_A"] + self.will_buy_b = js["buy_B"] + self.will_sell_b = js["sell_B"] + + def write_to_excel(self, file_name="res/agent_day_record.xlsx"): + if os.path.isfile(file_name): + existing_df = pd.read_excel(file_name) + else: + existing_df = pd.DataFrame(columns=["交易员", "交易日", "是否贷款", "贷款类型", "贷款数量", + "明日是否贷款", "明日是否买入A", "明日是否卖出A", "明日是否买入B", "明日是否卖出B"]) + + # 将新的交易记录合并到现有DataFrame + new_records = [[self.agent, self.date, self.if_loan, self.loan_type, self.loan_amount, + self.will_loan, self.will_buy_a, self.will_sell_a, self.will_buy_b, self.will_sell_b]] + new_df = pd.DataFrame(new_records, columns=existing_df.columns) + all_records_df = pd.concat([existing_df, new_df], ignore_index=True) + + # 将所有记录写入到Excel文件 + all_records_df.to_excel(file_name, index=False) + +class AgentRecordSession: + def __init__(self, agent, date, session, proper, cash, stock_a_value, stock_b_value, action_json): + self.agent = agent + self.date = date + self.session = session + self.proper = proper + self.cash = cash + self.stock_a_value = stock_a_value + self.stock_b_value = stock_b_value + self.action_stock = "-" + self.amount = 0 + self.price = 0 + self.action_type = action_json["action_type"] + if not self.action_type == "no": + self.action_stock = action_json["stock"] + self.amount = action_json["amount"] + self.price = action_json["price"] + + def write_to_excel(self, file_name="res/agent_session_record.xlsx"): + if os.path.isfile(file_name): + existing_df = pd.read_excel(file_name) + else: + existing_df = pd.DataFrame(columns=["交易员", "交易日", "交易阶段", "交易前资产总额", + "交易前持有现金", "交易前持有的A股价值", "交易前持有的B股价值", + "挂单类型", "挂单股票类别", "挂单数量", "挂单价格"]) + + # 将新的交易记录合并到现有DataFrame + new_records = [[self.agent, self.date, self.session, self.proper, self.cash, + self.stock_a_value, self.stock_b_value, self.action_type, self.action_stock, + self.amount, self.price]] + new_df = pd.DataFrame(new_records, columns=existing_df.columns) + all_records_df = pd.concat([existing_df, new_df], ignore_index=True) + + # 将所有记录写入到Excel文件 + all_records_df.to_excel(file_name, index=False) + +def create_agentses_record(agent, date, session, proper, cash, stock_a_value, stock_b_value, action_json): + record = AgentRecordSession(agent, date, session, proper, cash, stock_a_value, stock_b_value, action_json) + record.write_to_excel() + record = None diff --git a/examples/Stockagent/requirements.txt b/examples/Stockagent/requirements.txt new file mode 100644 index 0000000..5d5c00e --- /dev/null +++ b/examples/Stockagent/requirements.txt @@ -0,0 +1,58 @@ +annotated-types==0.7.0 +anyio==4.11.0 +black==25.9.0 +cachetools==6.2.1 +certifi==2025.10.5 +charset-normalizer==3.4.4 +click==8.3.0 +colorama==0.4.4 +distro==1.9.0 +et_xmlfile==2.0.0 +google-ai-generativelanguage==0.6.15 +google-api-core==2.27.0 +google-api-python-client==2.185.0 +google-auth==2.41.1 +google-auth-httplib2==0.2.0 +google-generativeai==0.8.5 +googleapis-common-protos==1.71.0 +grpcio==1.76.0 +grpcio-status==1.71.2 +h11==0.16.0 +httpcore==1.0.9 +httplib2==0.31.0 +httpx==0.28.1 +idna==3.11 +jiter==0.11.1 +mypy_extensions==1.1.0 +numpy==2.3.4 +openai==2.6.1 +openpyxl==3.1.5 +packaging==25.0 +pandas==2.3.3 +pathspec==0.12.1 +platformdirs==4.5.0 +procoder @ git+https://github.com/dhh1995/PromptCoder@87155427e93f6ab95dbd658d7f500c2cedc05af6 +proto-plus==1.26.1 +protobuf==5.29.5 +pyasn1==0.6.1 +pyasn1_modules==0.4.2 +pydantic==2.12.3 +pydantic_core==2.41.4 +pyparsing==3.2.5 +python-dateutil==2.9.0.post0 +python-dotenv==1.0.0 +pytokens==0.2.0 +pytz==2025.2 +regex==2025.10.23 +requests==2.31.0 +roman==5.1 +rsa==4.9.1 +six==1.17.0 +sniffio==1.3.1 +tiktoken==0.5.1 +tqdm==4.67.1 +typing-inspection==0.4.2 +typing_extensions==4.15.0 +tzdata==2025.2 +uritemplate==4.2.0 +urllib3==2.5.0 diff --git a/examples/Stockagent/res/stocks.xlsx b/examples/Stockagent/res/stocks.xlsx new file mode 100644 index 0000000..d60a1bc Binary files /dev/null and b/examples/Stockagent/res/stocks.xlsx differ diff --git a/examples/Stockagent/res/trades.xlsx b/examples/Stockagent/res/trades.xlsx new file mode 100644 index 0000000..0245047 Binary files /dev/null and b/examples/Stockagent/res/trades.xlsx differ diff --git a/examples/Stockagent/runagent.config.json b/examples/Stockagent/runagent.config.json new file mode 100644 index 0000000..cf4e4c0 --- /dev/null +++ b/examples/Stockagent/runagent.config.json @@ -0,0 +1,32 @@ +{ + "agent_name": "StockAgent Multi-Agent Trading Simulator", + "description": "LLM-based multi-agent stock trading simulation with real-world market factors", + "framework": "default", + "template": "stockagent", + "version": "1.0.0", + "created_at": "2025-10-24 00:00:00", + "template_source": { + "repo_url": "https://github.com/dhh1995/StockAgent", + "path": "templates/stockagent", + "author": "stockagent-team" + }, + "agent_architecture": { + "entrypoints": [ + + { + "file": "runagent_wrapper.py", + "module": "stream_simulation", + "tag": "simulate_stream" + }, + { + "file": "runagent_wrapper.py", + "module": "get_simulation_status", + "tag": "status" + } + ] + }, + "env_vars": { + "OPENAI_API_KEY": "", + "GOOGLE_API_KEY": "" + } +} \ No newline at end of file diff --git a/examples/Stockagent/runagent_wrapper.py b/examples/Stockagent/runagent_wrapper.py new file mode 100644 index 0000000..4d0d251 --- /dev/null +++ b/examples/Stockagent/runagent_wrapper.py @@ -0,0 +1,393 @@ +""" +RunAgent Wrapper for StockAgent - RUNS REAL SIMULATION WITH FULL LOGGING +""" +import json +import time +import sys +import io +import logging +from typing import Dict, Any, Iterator +from dataclasses import dataclass, asdict +import os +import random +from contextlib import redirect_stdout, redirect_stderr + +# Import StockAgent modules +from main import handle_action +from agent import Agent +from stock import Stock +import util + + +@dataclass +class SimulationUpdate: + """Structure for simulation updates""" + type: str + day: int + session: int = 0 + message: str = "" + data: Dict[str, Any] = None + timestamp: float = None + + def __post_init__(self): + if self.timestamp is None: + self.timestamp = time.time() + if self.data is None: + self.data = {} + + +class LogCapture: + """Captures all log output and yields it""" + def __init__(self): + self.buffer = io.StringIO() + self.logs = [] + + def write(self, text): + if text and text.strip(): + self.logs.append(text) + self.buffer.write(text) + + def flush(self): + self.buffer.flush() + + def get_logs(self): + """Get and clear accumulated logs""" + logs = self.logs.copy() + self.logs.clear() + return logs + + +class StockAgentRunner: + """Wrapper class to run StockAgent simulations""" + + def __init__(self): + self.simulation_state = { + "status": "ready", + "current_day": 0, + "total_days": 0, + "agents": [], + "stocks": {}, + "events": [] + } + + def stream_simulation( + self, + num_agents: int = 10, + total_days: int = 30, + sessions_per_day: int = 3, + model: str = "gpt-4o-mini", + stock_a_price: float = 30.0, + stock_b_price: float = 40.0, + enable_events: bool = True + ) -> Iterator[Dict[str, Any]]: + """Stream REAL simulation with LLM agents and ALL logs""" + + # FIX: Convert string parameters to appropriate types + try: + num_agents = int(num_agents) if isinstance(num_agents, str) else num_agents + total_days = int(total_days) if isinstance(total_days, str) else total_days + sessions_per_day = int(sessions_per_day) if isinstance(sessions_per_day, str) else sessions_per_day + stock_a_price = float(stock_a_price) if isinstance(stock_a_price, str) else stock_a_price + stock_b_price = float(stock_b_price) if isinstance(stock_b_price, str) else stock_b_price + + # Convert string boolean to actual boolean + if isinstance(enable_events, str): + enable_events = enable_events.lower() in ('true', '1', 'yes', 'on') + except (ValueError, TypeError) as e: + yield asdict(SimulationUpdate( + type="error", + day=0, + message=f"❌ Parameter conversion error: {str(e)}", + data={"error": str(e)} + )) + return + + # Setup log capturing + log_capture = LogCapture() + + # Get the StockAgent logger and add our handler + from log.custom_logger import log as stock_logger + capture_handler = logging.StreamHandler(log_capture) + capture_handler.setLevel(logging.DEBUG) + stock_logger.logger.addHandler(capture_handler) + + try: + yield asdict(SimulationUpdate( + type="init", + day=0, + message="🚀 Initializing REAL StockAgent simulation...", + data={ + "num_agents": num_agents, + "total_days": total_days, + "sessions_per_day": sessions_per_day, + "model": model + } + )) + + # Update util settings with converted values + util.AGENTS_NUM = num_agents + util.TOTAL_DATE = total_days + util.TOTAL_SESSION = sessions_per_day + util.STOCK_A_INITIAL_PRICE = stock_a_price + util.STOCK_B_INITIAL_PRICE = stock_b_price + + if not enable_events: + util.EVENT_1_DAY = total_days + 1 + util.EVENT_2_DAY = total_days + 2 + + # Run REAL simulation with log streaming + for update in self._run_real_simulation_streaming(model, log_capture): + yield asdict(update) + + yield asdict(SimulationUpdate( + type="complete", + day=total_days, + message="🎉 Real simulation completed!", + data=self._collect_results() + )) + + except Exception as e: + import traceback + yield asdict(SimulationUpdate( + type="error", + day=0, + message=f"❌ Error: {str(e)}", + data={"error": str(e), "traceback": traceback.format_exc()} + )) + finally: + # Remove our handler + stock_logger.logger.removeHandler(capture_handler) + + def _run_real_simulation_streaming(self, model: str, log_capture: LogCapture) -> Iterator[SimulationUpdate]: + """REAL simulation with actual LLM calls and log streaming""" + from secretary import Secretary + from log.custom_logger import log + from record import create_stock_record + + secretary = Secretary(model) + stock_a = Stock("A", util.STOCK_A_INITIAL_PRICE, 0, is_new=False) + stock_b = Stock("B", util.STOCK_B_INITIAL_PRICE, 0, is_new=False) + + all_agents = [] + log.logger.info("🤖 Initializing LLM agents...") + + # Yield any logs from initialization + for log_line in log_capture.get_logs(): + yield SimulationUpdate( + type="log", + day=0, + message=log_line.strip() + ) + + for i in range(util.AGENTS_NUM): + agent = Agent(i, stock_a.get_price(), stock_b.get_price(), secretary, model) + all_agents.append(agent) + + # Yield logs after each agent creation + for log_line in log_capture.get_logs(): + yield SimulationUpdate( + type="log", + day=0, + message=log_line.strip() + ) + + yield SimulationUpdate( + type="agents_initialized", + day=0, + message=f"✅ Initialized {len(all_agents)} REAL LLM agents", + data={"num_agents": len(all_agents)} + ) + + last_day_forum_message = [] + stock_a_deals = {"sell": [], "buy": []} + stock_b_deals = {"sell": [], "buy": []} + + for date in range(1, util.TOTAL_DATE + 1): + stock_a_deals["sell"].clear() + stock_a_deals["buy"].clear() + stock_b_deals["buy"].clear() + stock_b_deals["sell"].clear() + + for agent in all_agents[:]: + agent.chat_history.clear() + agent.loan_repayment(date) + + # Yield logs + for log_line in log_capture.get_logs(): + yield SimulationUpdate( + type="log", + day=date, + message=log_line.strip() + ) + + if date in util.REPAYMENT_DAYS: + for agent in all_agents[:]: + agent.interest_payment() + + # Yield logs + for log_line in log_capture.get_logs(): + yield SimulationUpdate( + type="log", + day=date, + message=log_line.strip() + ) + + for agent in all_agents[:]: + if agent.is_bankrupt: + quit_sig = agent.bankrupt_process(stock_a.get_price(), stock_b.get_price()) + if quit_sig: + agent.quit = True + all_agents.remove(agent) + + # Yield logs + for log_line in log_capture.get_logs(): + yield SimulationUpdate( + type="log", + day=date, + message=log_line.strip() + ) + + if date == util.EVENT_1_DAY: + util.LOAN_RATE = util.EVENT_1_LOAN_RATE + last_day_forum_message.append({"name": -1, "message": util.EVENT_1_MESSAGE}) + + if date == util.EVENT_2_DAY: + util.LOAN_RATE = util.EVENT_2_LOAN_RATE + last_day_forum_message.append({"name": -1, "message": util.EVENT_2_MESSAGE}) + + yield SimulationUpdate( + type="day_start", + day=date, + message=f"📅 Day {date}/{util.TOTAL_DATE}", + data={ + "stock_a_price": stock_a.get_price(), + "stock_b_price": stock_b.get_price(), + "active_agents": len(all_agents) + } + ) + + # REAL LLM CALLS FOR LOANS + for agent in all_agents: + loan = agent.plan_loan(date, stock_a.get_price(), stock_b.get_price(), last_day_forum_message) + + # Yield logs after each loan decision + for log_line in log_capture.get_logs(): + yield SimulationUpdate( + type="log", + day=date, + message=log_line.strip() + ) + + for session in range(1, util.TOTAL_SESSION + 1): + trades_count = 0 + sequence = list(range(len(all_agents))) + random.shuffle(sequence) + + # REAL LLM CALLS FOR TRADING + for i in sequence: + agent = all_agents[i] + action = agent.plan_stock(date, session, stock_a, stock_b, stock_a_deals, stock_b_deals) + + # Yield logs after each trading decision + for log_line in log_capture.get_logs(): + yield SimulationUpdate( + type="log", + day=date, + session=session, + message=log_line.strip() + ) + + if action.get("action_type") != "no": + trades_count += 1 + action["agent"] = agent.order + action["date"] = date + + if action["stock"] == 'A': + handle_action(action, stock_a_deals, all_agents, stock_a, session) + else: + handle_action(action, stock_b_deals, all_agents, stock_b, session) + + # Yield logs after action handling + for log_line in log_capture.get_logs(): + yield SimulationUpdate( + type="log", + day=date, + session=session, + message=log_line.strip() + ) + + stock_a.update_price(date) + stock_b.update_price(date) + create_stock_record(date, session, stock_a.get_price(), stock_b.get_price()) + + yield SimulationUpdate( + type="session", + day=date, + session=session, + message=f"⏰ Session {session} - {trades_count} trades", + data={ + "stock_a_price": stock_a.get_price(), + "stock_b_price": stock_b.get_price(), + "trades": trades_count + } + ) + + # Yield any remaining logs from session + for log_line in log_capture.get_logs(): + yield SimulationUpdate( + type="log", + day=date, + session=session, + message=log_line.strip() + ) + + # REAL LLM CALLS FOR FORUM POSTS + last_day_forum_message.clear() + for agent in all_agents: + message = agent.post_message() + last_day_forum_message.append({"name": agent.order, "message": message}) + + # Yield logs after each forum post + for log_line in log_capture.get_logs(): + yield SimulationUpdate( + type="log", + day=date, + message=log_line.strip() + ) + + yield SimulationUpdate( + type="day_end", + day=date, + message=f"✅ Day {date} done", + data={ + "stock_a_price": stock_a.get_price(), + "stock_b_price": stock_b.get_price(), + "surviving_agents": len(all_agents) + } + ) + + + def _collect_results(self) -> Dict[str, Any]: + results = {"trades": [], "stock_prices": [], "agent_performance": []} + try: + import pandas as pd + if os.path.exists("res/trades.xlsx"): + results["trades"] = pd.read_excel("res/trades.xlsx").to_dict('records') + if os.path.exists("res/stocks.xlsx"): + results["stock_prices"] = pd.read_excel("res/stocks.xlsx").to_dict('records') + except: pass + return results + + +runner = StockAgentRunner() + + +def stream_simulation(**kwargs) -> Iterator[Dict[str, Any]]: + """Streaming simulation - yields updates AND logs in real-time""" + for update in runner.stream_simulation(**kwargs): + yield update + + +def get_simulation_status() -> Dict[str, Any]: + """Get current simulation status""" + return runner.simulation_state \ No newline at end of file diff --git a/examples/Stockagent/sdk_test/python/test_agent.py b/examples/Stockagent/sdk_test/python/test_agent.py new file mode 100644 index 0000000..3a3e67c --- /dev/null +++ b/examples/Stockagent/sdk_test/python/test_agent.py @@ -0,0 +1,16 @@ +from runagent import RunAgentClient + +client = RunAgentClient( + agent_id="6cf5351f-b228-4648-9a07-20608ef490be", + entrypoint_tag="simulate_stream", + local=False +) + +# Run and print ALL output including logs +for update in client.run( + num_agents="5", + total_days="2", + sessions_per_day="2", + model="gpt-4o-mini" +): + print(update) diff --git a/examples/Stockagent/sdk_test/rust/Cargo.toml b/examples/Stockagent/sdk_test/rust/Cargo.toml new file mode 100644 index 0000000..6012193 --- /dev/null +++ b/examples/Stockagent/sdk_test/rust/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "temp_rust_test" +version = "0.1.1" +edition = "2021" + +[dependencies] +runagent = { path = "../../../../runagent-rust/runagent" } +tokio = { version = "1.0", features = ["full"] } +serde_json = "1.0" +anyhow = "1.0" +futures = "0.3" \ No newline at end of file diff --git a/examples/Stockagent/sdk_test/rust/src/main.rs b/examples/Stockagent/sdk_test/rust/src/main.rs new file mode 100644 index 0000000..120be64 --- /dev/null +++ b/examples/Stockagent/sdk_test/rust/src/main.rs @@ -0,0 +1,46 @@ +use runagent::client::RunAgentClient; +use serde_json::json; +use futures::StreamExt; + +#[tokio::main] +async fn main() -> Result<(), Box> { + println!("🧪 Testing StockAgent with Rust SDK"); + println!("===================================="); + + // Initialize the client + let client = RunAgentClient::new( + "6cf5351f-b228-4648-9a07-20608ef490be", // Agent ID + "simulate_stream", // Entrypoint + false // Remote connection + ).await?; + + println!("✅ Client initialized"); + + println!("🔄 Starting simulation..."); + // Run simulation with parameters (matching Python SDK exactly) + let mut stream = client.run_stream(&[ + ("num_agents", json!("5")), // String values like Python + ("total_days", json!("2")), // String values like Python + ("sessions_per_day", json!("2")), // String values like Python + ("model", json!("gpt-4o-mini")) // String values like Python + ]).await?; + + println!("🚀 Simulation started, streaming updates...\n"); + + // Process the stream - simple output like Python SDK + while let Some(chunk_result) = stream.next().await { + match chunk_result { + Ok(chunk) => { + // Just print the raw data like Python SDK + println!("{}", chunk); + }, + Err(e) => { + println!("❌ Stream Error: {}", e); + break; + } + } + } + + println!("✅ StockAgent test completed successfully!"); + Ok(()) +} \ No newline at end of file diff --git a/examples/Stockagent/secretary.py b/examples/Stockagent/secretary.py new file mode 100644 index 0000000..369ce87 --- /dev/null +++ b/examples/Stockagent/secretary.py @@ -0,0 +1,221 @@ +import json +import os +import openai +from log.custom_logger import log + + +def run_api(model, prompt, temperature: float = 0): + openai.api_key = "" + client = openai.OpenAI(api_key=openai.api_key) + response = client.chat.completions.create( + model=model, + messages=[ + {"role": "user", "content": prompt}, + ], + temperature=temperature, + ) + resp = response.choices[0].message.content + return resp + + +class Secretary: + def __init__(self, model): + self.model = model + + def get_response(self, prompt): + return run_api(self.model, prompt) + + """ + 用json形式返回结果,例如: + {{{{"loan": "yes", "loan_type": 3, "amount": 1000}}}} + 如果不需贷款,则返回: + {{{{"loan" : "no"}}}} + :returns: loan_format_check, fail_response, loan + """ + + def check_loan(self, resp, max_loan) -> (bool, str, dict): + # format check + if isinstance(resp, str) and resp.count('{') == 1 and resp.count('}') == 1: + start_idx = resp.index('{') + end_idx = resp.index('}') + else: + log.logger.debug("Wrong json content in response: {}".format(resp)) + fail_response = "Wrong json format, there is no {} or more than one {} in response." + return False, fail_response, None + + action_json = resp[start_idx: end_idx + 1] + action_json = action_json.replace("\n", "").replace(" ", "") + try: + parsed_json = json.loads(action_json) + except json.JSONDecodeError as e: + print(e) + log.logger.debug("Illegal json content in response: {}".format(resp)) + fail_response = "Illegal json format." + return False, fail_response, None + + # content check + try: + if "loan" not in parsed_json: + log.logger.debug("Wrong json content in response: {}".format(resp)) + fail_response = "Key 'loan' not in response." + return False, fail_response, None + + if parsed_json["loan"].lower() not in ["yes", "no"]: + log.logger.debug("Wrong json content in response: {}".format(resp)) + fail_response = "Value of key 'loan' should be yes or no." + return False, fail_response, None + + if parsed_json["loan"].lower() == "no": + if "loan_type" in parsed_json or "amount" in parsed_json: + log.logger.debug("Wrong json content in response: {}".format(resp)) + fail_response = "Don't include loan_type or amount in response if value of key 'loan' is no." + return False, fail_response, None + else: + return True, "", parsed_json + + if parsed_json["loan"].lower() == "yes": + if "loan_type" not in parsed_json or "amount" not in parsed_json: + log.logger.debug("Wrong json content in response: {}".format(resp)) + fail_response = "Should include loan_type and amount in response if value of key 'loan' is yes." + return False, fail_response, None + if parsed_json["loan_type"] not in [0, 1, 2]: + log.logger.debug("Wrong json content in response: {}".format(resp)) + fail_response = "Value of key 'loan_type' should be 0, 1 or 2." + return False, fail_response, None + if parsed_json["amount"] <= 0 or parsed_json["amount"] > max_loan: + log.logger.debug("Wrong json content in response: {}".format(resp)) + fail_response = f"Value of key 'amount' should be positive and less than {max_loan}" + return False, fail_response, None + return True, "", parsed_json + + log.logger.error("UNSOLVED LOAN JSON RESPONSE:{}".format(parsed_json)) + return False, "", None + except Exception as e: + log.logger.error("UNSOLVED LOAN JSON RESPONSE:{}".format(parsed_json)) + return False, "", None + + def check_action(self, resp, cash, stock_a_amount, + stock_b_amount, stock_a_price, stock_b_price) -> (bool, str, dict): + # format check + if isinstance(resp, str) and resp.count('{') == 1 and resp.count('}') == 1: + start_idx = resp.index('{') + end_idx = resp.index('}') + else: + log.logger.debug("Wrong json content in response: {}".format(resp)) + fail_response = "Wrong json format, there is no {} or more than one {} in response." + return False, fail_response, None + + action_json = resp[start_idx: end_idx + 1] + action_json = action_json.replace("\n", "").replace(" ", "") + try: + parsed_json = json.loads(action_json) + except json.JSONDecodeError as e: + print(e) + log.logger.debug("Illegal json content in response: {}".format(resp)) + fail_response = "Illegal json format." + return False, fail_response, None + + # content check + try: + prices = {"A": stock_a_price, "B": stock_b_price} + holds = {"A": stock_a_amount, "B": stock_b_amount} + if "action_type" not in parsed_json: + log.logger.debug("Wrong json content in response: {}".format(resp)) + fail_response = "Key 'action_type' not in response." + return False, fail_response, None + + if parsed_json["action_type"].lower() not in ["buy", "sell", "no"]: + log.logger.debug("Wrong json content in response: {}".format(resp)) + fail_response = "Value of key 'action_type' should be 'buy', 'sell' or 'no'." + return False, fail_response, None + + if parsed_json["action_type"].lower() == "no": + if "stock" in parsed_json or "amount" in parsed_json: + log.logger.debug("Wrong json content in response: {}".format(resp)) + fail_response = "Don't include stock or amount in response if value of key 'action_type' is no." + return False, fail_response, None + else: + return True, "", parsed_json + else: + if "stock" not in parsed_json or "amount" not in parsed_json or "price" not in parsed_json: + log.logger.debug("Wrong json content in response: {}".format(resp)) + fail_response = "Should include stock, amount and price in response " \ + "if value of key 'action_type' is buy or sell." + return False, fail_response, None + if parsed_json["stock"] not in ['A', 'B']: + log.logger.debug("Wrong json content in response: {}".format(resp)) + fail_response = "Value of key 'stock' should be 'A' or 'B'." + return False, fail_response, None + if parsed_json["price"] <= 0: + log.logger.debug("Wrong json content in response: {}".format(resp)) + fail_response = f"Value of key 'price' should be positive." + return False, fail_response, None + if not isinstance(parsed_json["amount"], int): + log.logger.debug("Wrong json content in response: {}".format(resp)) + fail_response = f"Value of key 'amount' should be integer." + return False, fail_response, None + + # buy more than cash or sell more than hold amount + # price = prices[parsed_json["stock"]] + price = parsed_json["price"] + if parsed_json["action_type"].lower() == "buy": + if parsed_json["amount"] <= 0 or parsed_json["amount"] * price > cash: + log.logger.debug("Buy more than cash: {}".format(resp)) + fail_response = f"The cash you have now is {cash}, " \ + f"the value of 'amount' * 'price' " \ + f"should be positive and not exceed cash." + return False, fail_response, None + + hold_amount = holds[parsed_json["stock"]] + if parsed_json["action_type"].lower() == "sell": + if parsed_json["amount"] <= 0 or parsed_json["amount"] > hold_amount: + log.logger.debug("Sell more than hold: {}".format(resp)) + fail_response = f"The amount of stock you hold is {hold_amount}, " \ + f"the value of 'amount' should be positive and not exceed the " \ + f"amount of stock you hold." + return False, fail_response, None + return True, "", parsed_json + + except Exception as e: + log.logger.error("UNSOLVED ACTION JSON RESPONSE:{}".format(parsed_json)) + return False, "", None + + def check_estimate(self, resp): + # format check + if isinstance(resp, str) and resp.count('{') == 1 and resp.count('}') == 1: + start_idx = resp.index('{') + end_idx = resp.index('}') + else: + log.logger.debug("Wrong json content in response: {}".format(resp)) + fail_response = "Wrong json format, there is no {} or more than one {} in response." + return False, fail_response, None + + action_json = resp[start_idx: end_idx + 1] + action_json = action_json.replace("\n", "").replace(" ", "") + try: + parsed_json = json.loads(action_json) + except json.JSONDecodeError as e: + print(e) + log.logger.debug("Illegal json content in response: {}".format(resp)) + fail_response = "Illegal json format." + return False, fail_response, None + + # content check + try: + if "buy_A" not in parsed_json or "buy_B" not in parsed_json \ + or "sell_A" not in parsed_json or "sell_B" not in parsed_json \ + or "loan" not in parsed_json: + log.logger.debug("Wrong json content in response: {}".format(resp)) + fail_response = "Key 'buy_A', 'buy_B', 'sell_A', 'sell_B' and 'loan' should in response." + return False, fail_response, None + + for key, item in parsed_json.items(): + if item not in ['yes', 'no']: + log.logger.debug("Wrong json content in response: {}".format(resp)) + fail_response = "Value of all keys should be 'yes' or 'no'." + return False, fail_response, None + return True, "", parsed_json + + except Exception as e: + log.logger.error("UNSOLVED ESTIMATE JSON RESPONSE:{}".format(parsed_json)) + return False, "", None diff --git a/examples/Stockagent/stock.py b/examples/Stockagent/stock.py new file mode 100644 index 0000000..964c635 --- /dev/null +++ b/examples/Stockagent/stock.py @@ -0,0 +1,31 @@ +import util + +class Stock: + def __init__(self, name, initial_price, initial_stock, is_new=False): + self.name = name + self.price = initial_price + self.ideal_price = 0 + self.initial_stock = initial_stock + self.history = {} # {date: session_deal} + self.session_deal = [] # [{"price", "amount"}] + + def gen_financial_report(self, index): + if self.name == "A": + return util.FINANCIAL_REPORT_A[index] + elif self.name == "B": + return util.FINANCIAL_REPORT_B[index] + + def add_session_deal(self, price_and_amount): + self.session_deal.append(price_and_amount) + + def update_price(self, date): + if len(self.session_deal) == 0: + return + self.price = self.session_deal[-1]["price"] + self.history[date] = self.session_deal + self.session_deal.clear() + + def get_price(self): + return self.price + + diff --git a/examples/Stockagent/util.py b/examples/Stockagent/util.py new file mode 100644 index 0000000..01d5cf1 --- /dev/null +++ b/examples/Stockagent/util.py @@ -0,0 +1,50 @@ +""" +DONT FORGET TO DELETE!!! +""" +OPENAI_API_KEY = "" +GOOGLE_API_KEY = "" + +# 基础设置 +AGENTS_NUM = 50 # 交易员数量 +TOTAL_DATE = 264 # 模拟时长 +TOTAL_SESSION = 3 # 每日交易次数 + +# 股票初始价格 +STOCK_A_INITIAL_PRICE = 30 +STOCK_B_INITIAL_PRICE = 40 +# STOCK_B_PUBLISH = 100 # 股票B发行数量 + +# agent初始财产 +MAX_INITIAL_PROPERTY = 5000000.0 +MIN_INITIAL_PROPERTY = 100000.0 + + +# 贷款 +LOAN_TYPE = ["one-month", "two-month", "three-month"] +LOAN_TYPE_DATE = [22, 44, 66] # 贷款时长 +LOAN_RATE = [0.027, 0.03, 0.033] # 贷款利率 + +REPAYMENT_DAYS = [22, 44, 66, 88, 110, 132, 154, 176, 198, 220, 242, 264] # 付息日 + +# 财报 +SEASONAL_DAYS = 66 # 一季度的时间 +SEASON_REPORT_DAYS = [12, 78, 144, 210] # 财报发布时间 +FINANCIAL_REPORT_A = ["Last quarter's financial report of Company A. Revenue growth rate (YoY): 9.49%, Revenue million: 4483.99, Gross margin: 41.05%, Income Tax as a percentage of Revenue: 11.31%, Selling Expense Rate:6.83%, Management Expense Rate: 3.83%, Net profit million: 856.6705, Depreciation and Amortization: 0.91%, Capital Expenditures: 2.30%, Changes in working capital: 0.82%, Cash Flow(million): 756.7537", + "Last quarter's financial report of Company A. Revenue growth rate (YoY): 7.38%, Revenue million: 4417.79, Gross margin: 35.68%, Income Tax as a percentage of Revenue: 11.75%, Selling Expense Rate:8.13%, Management Expense Rate: 4.62%, Net profit million: 493.9451, Depreciation and Amortization: 1.34%, Capital Expenditures: 2.68%, Changes in working capital: 0.86%, Cash Flow(million): 396.5329", + "Last quarter's financial report of Company A. Revenue growth rate (YoY): 8.70%, Revenue million: 4041.30, Gross margin: 37.45%, Income Tax as a percentage of Revenue: 9.34%, Selling Expense Rate:6.79%, Management Expense Rate: 3.41%, Net profit million: 724.3648, Depreciation and Amortization: 1.27%, Capital Expenditures: 2.44%, Changes in working capital: 0.94%, Cash Flow(million): 639.5329", + "Last quarter's financial report of Company A. Revenue growth rate (YoY): 7.75%, Revenue million: 5024.04, Gross margin: 42.47%, Income Tax as a percentage of Revenue: 10.67%, Selling Expense Rate:6.56%, Management Expense Rate: 4.72%, Net profit million: 1031.214, Depreciation and Amortization: 1.08%, Capital Expenditures: 2.71%, Changes in working capital: 0.08%, Cash Flow(million): 945.5034"] # 各个季度的财报 +FINANCIAL_REPORT_B = ["Last quarter's financial report of Company B. Revenue growth rate (YoY): 19.96%, Revenue million: 1319.94, Gross margin: 31.21%, Income Tax as a percentage of Revenue: 0.70%, Selling Expense Rate:4.69%, Management Expense Rate: 8.78%, Net profit million: 224.9179, Depreciation and Amortization: 1.13%, Capital Expenditures: 1.77%, Changes in working capital: 0.59%, Cash Flow(million): 208.7266", + "Last quarter's financial report of Company B. Revenue growth rate (YoY): 19.86%, Revenue million: 1096.70, Gross margin: 31.26%, Income Tax as a percentage of Revenue: 0.71%, Selling Expense Rate:3.62%, Management Expense Rate: 9.90%, Net profit million: 186.7678, Depreciation and Amortization: 0.67%, Capital Expenditures: 1.44%, Changes in working capital: -0.31%, Cash Flow(million): 181.6862", + "Last quarter's financial report of Company B. Revenue growth rate (YoY): 18.21%, Revenue million: 1676.70, Gross margin: 31.58%, Income Tax as a percentage of Revenue: 0.92%, Selling Expense Rate:3.78%, Management Expense Rate: 10.27%, Net profit million: 278.3327, Depreciation and Amortization: 0.77%, Capital Expenditures: 1.56%, Changes in working capital: -0.06%, Cash Flow(million): 266.1486", + "Last quarter's financial report of Company B. Revenue growth rate (YoY): 15.98%, Revenue million: 1075.13, Gross margin: 32.41%, Income Tax as a percentage of Revenue: 1.08%, Selling Expense Rate:3.79%, Management Expense Rate: 10.70%, Net profit million: 181.1602, Depreciation and Amortization: 1.09%, Capital Expenditures: 2.28%, Changes in working capital: 0.67%, Cash Flow(million): 161.1985"] + +# 特殊事件 + +EVENT_1_DAY = 78 +EVENT_1_MESSAGE = "The government has announced a reduction in the reserve requirement ratio. " \ + "The lending interest rates have been lowered." +EVENT_1_LOAN_RATE = [0.024, 0.027, 0.030] # 降准后的利率放在这里 + +EVENT_2_DAY = 144 +EVENT_2_MESSAGE = "The government has announced an increase in interest rates." +EVENT_2_LOAN_RATE = [0.0255, 0.0285, 0.0315] \ No newline at end of file diff --git a/examples/ai_lead_generation/agents/README.md b/examples/ai_lead_generation/agents/README.md new file mode 100644 index 0000000..171a104 --- /dev/null +++ b/examples/ai_lead_generation/agents/README.md @@ -0,0 +1,117 @@ +# 🎯 AI Lead Generation Agent - RunAgent + +Automated lead generation from Quora using Firecrawl's Extract endpoint and Google Sheets integration. This RunAgent-powered agent finds and qualifies potential leads, extracting valuable information and organizing it into Google Sheets. + +## Features + +- **Intelligent Search**: Uses Firecrawl's search to find relevant Quora URLs +- **Smart Extraction**: Leverages Firecrawl's Extract endpoint to pull user information +- **Automated Processing**: Formats data into clean, structured format +- **Google Sheets Integration**: Automatically creates and populates sheets with lead data +- **RunAgent Compatible**: Deploy as a cloud API endpoint + +## Setup + +### 1. Install Dependencies + +```bash +pip install -r requirements.txt +``` + +### 2. Configure Composio + +```bash +composio add googlesheets +``` + +Make sure the Google Sheets integration is active in your Composio dashboard. + +### 3. Set Environment Variables + +Update `runagent.config.json` with your API keys: +- `FIRECRAWL_API_KEY`: Get from [Firecrawl](https://www.firecrawl.dev/app/api-keys) +- `COMPOSIO_API_KEY`: Get from [Composio](https://composio.ai) +- `OPENAI_API_KEY`: Get from [OpenAI](https://platform.openai.com/api-keys) + +## Usage + +### Local Testing + +```python +from main import generate_leads + +result = generate_leads( + search_query="AI customer support chatbots", + num_links=3, + firecrawl_api_key="your-key", + composio_api_key="your-key", + openai_api_key="your-key" +) + +print(result) +``` + +### Deploy with RunAgent + +```bash +# Test locally +runagent serve . --local + +# Deploy to cloud +runagent deploy . +``` + +### Use the SDK + +```python +from runagent import RunAgentClient + +client = RunAgentClient( + agent_id="your-deployed-agent-id", + entrypoint_tag="generate_leads", + local=False +) + +result = client.run( + search_query="AI video editing software", + num_links=5 +) + +print(f"Google Sheet: {result['google_sheet_url']}") +print(f"Leads found: {result['leads_count']}") +``` + +## Parameters + +- `search_query` (str): Description of leads to find (e.g., "AI customer support chatbots") +- `num_links` (int): Number of Quora URLs to process (1-10, default: 3) +- `firecrawl_api_key` (str): Your Firecrawl API key +- `composio_api_key` (str): Your Composio API key +- `openai_api_key` (str): Your OpenAI API key + +## Response Format + +```json +{ + "success": true, + "search_query": "AI video editing software", + "urls_processed": 3, + "leads_count": 15, + "google_sheet_url": "https://docs.google.com/spreadsheets/d/...", + "leads_data": [...] +} +``` + +## How It Works + +1. **Search**: Queries Quora for relevant discussions using Firecrawl search +2. **Extract**: Uses Firecrawl Extract to pull user profiles and interactions +3. **Format**: Structures data with username, bio, post type, timestamp, upvotes, and links +4. **Save**: Creates a new Google Sheet with all lead information + +## Support + +For issues or questions, refer to: +- [RunAgent Documentation](https://docs.runagent.dev) +- [Firecrawl Documentation](https://docs.firecrawl.dev) +- [Composio Documentation](https://docs.composio.dev) \ No newline at end of file diff --git a/examples/ai_lead_generation/agents/main.py b/examples/ai_lead_generation/agents/main.py new file mode 100644 index 0000000..a77a3a3 --- /dev/null +++ b/examples/ai_lead_generation/agents/main.py @@ -0,0 +1,419 @@ +#!/usr/bin/env python +""" +RunAgent-compatible entry points for AI Lead Generation Agent +""" +import os +import requests +from firecrawl import FirecrawlApp +from pydantic import BaseModel, Field +from typing import List, Dict, Any +import json +import csv +from datetime import datetime +from dotenv import load_dotenv +from openai import OpenAI + +# Load environment variables +load_dotenv() + + +class QuoraUserInteractionSchema(BaseModel): + username: str = Field(description="The username of the user who posted the question or answer") + bio: str = Field(description="The bio or description of the user") + post_type: str = Field(description="The type of post, either 'question' or 'answer'") + timestamp: str = Field(description="When the question or answer was posted") + upvotes: int = Field(default=0, description="Number of upvotes received") + links: List[str] = Field(default_factory=list, description="Any links included in the post") + + +class QuoraPageSchema(BaseModel): + interactions: List[QuoraUserInteractionSchema] = Field( + description="List of all user interactions (questions and answers) on the page" + ) + + +def search_quora_urls(query: str, firecrawl_api_key: str, num_links: int = 3) -> List[str]: + """Search for relevant Quora URLs based on query""" + url = "https://api.firecrawl.dev/v1/search" + headers = { + "Authorization": f"Bearer {firecrawl_api_key}", + "Content-Type": "application/json" + } + + search_query = f"quora websites where people are looking for {query} services" + print(f" 🔍 Search query: {search_query}") + + payload = { + "query": search_query, + "limit": num_links, + "lang": "en", + "location": "United States", + "timeout": 60000, + } + + try: + response = requests.post(url, json=payload, headers=headers) + print(f" 📡 Search API status: {response.status_code}") + + if response.status_code == 200: + data = response.json() + print(f" 📡 Search success: {data.get('success')}") + + if data.get("success"): + results = data.get("data", []) + urls = [result["url"] for result in results] + print(f" ✅ Found {len(urls)} URLs:") + for i, found_url in enumerate(urls, 1): + print(f" {i}. {found_url}") + return urls + else: + print(f" ❌ Search API error: {response.text}") + + except Exception as e: + print(f" ❌ Error searching URLs: {e}") + + return [] + + +def extract_leads_with_openai(url: str, page_content: str, openai_api_key: str) -> List[dict]: + """Use OpenAI to intelligently extract lead information from page content""" + try: + client = OpenAI(api_key=openai_api_key) + + prompt = f"""Analyze this Quora page content and extract information about users who are asking questions or providing answers related to the topic. + +Page URL: {url} + +Page Content: +{page_content[:8000]} # Limit content to avoid token limits + +Extract the following information for each user you find: +1. Username (if visible) +2. Bio/Description (if available) +3. Whether they asked a question or provided an answer +4. Timestamp (if available) +5. Number of upvotes (if visible) +6. Any relevant links they shared + +Return a JSON array of users. If you can't find specific information, use reasonable defaults like "Unknown" or "Not specified". + +Example format: +[ + {{ + "username": "John Doe", + "bio": "Software Engineer at Tech Corp", + "post_type": "question", + "timestamp": "2 days ago", + "upvotes": 15, + "links": [] + }} +] + +If no users are clearly identifiable, return at least one entry with the page URL and basic information.""" + + response = client.chat.completions.create( + model="gpt-4o-mini", + messages=[ + {"role": "system", "content": "You are a lead generation expert who extracts user information from web pages."}, + {"role": "user", "content": prompt} + ], + response_format={"type": "json_object"}, + temperature=0.3 + ) + + result = json.loads(response.choices[0].message.content) + + # Handle different possible response structures + if isinstance(result, dict) and 'users' in result: + return result['users'] + elif isinstance(result, list): + return result + elif isinstance(result, dict): + # If it's a dict but not with 'users' key, wrap it + return [result] + else: + return [] + + except Exception as e: + print(f" ⚠️ OpenAI extraction failed: {str(e)}") + return [] + + +def extract_leads_from_urls(urls: List[str], firecrawl_api_key: str, openai_api_key: str = None) -> List[dict]: + """Extract user information from Quora URLs using Firecrawl + OpenAI""" + user_info_list = [] + firecrawl_app = FirecrawlApp(api_key=firecrawl_api_key) + use_openai = openai_api_key is not None + + for url in urls: + try: + print(f" 📊 Extracting from: {url}") + + # First, try to scrape the page content + scrape_response = firecrawl_app.scrape_url( + url=url, + params={'formats': ['markdown', 'html']} + ) + + if not scrape_response.get('success'): + print(f" ❌ Failed to scrape {url}") + # Create basic fallback + user_info_list.append({ + "website_url": url, + "user_info": [{ + 'username': 'Quora User', + 'bio': 'Lead from Quora - Manual review needed', + 'post_type': 'discussion', + 'timestamp': 'Recent', + 'upvotes': 0, + 'links': [url] + }] + }) + continue + + page_content = scrape_response.get('markdown', '') or scrape_response.get('html', '') + + if not page_content: + print(f" ⚠️ No content extracted from {url}") + continue + + print(f" ✅ Scraped {len(page_content)} characters") + + # Use OpenAI to intelligently extract user information + if use_openai: + print(f" 🤖 Using OpenAI to extract lead information...") + interactions = extract_leads_with_openai(url, page_content, openai_api_key) + + if interactions and len(interactions) > 0: + print(f" ✅ OpenAI extracted {len(interactions)} leads") + user_info_list.append({ + "website_url": url, + "user_info": interactions + }) + else: + print(f" ⚠️ OpenAI found no leads, creating basic entry") + user_info_list.append({ + "website_url": url, + "user_info": [{ + 'username': 'Quora User', + 'bio': 'Active in discussion', + 'post_type': 'discussion', + 'timestamp': 'Recent', + 'upvotes': 0, + 'links': [url] + }] + }) + else: + # Without OpenAI, create basic entry from URL + print(f" ℹ️ No OpenAI key provided, creating basic entry") + user_info_list.append({ + "website_url": url, + "user_info": [{ + 'username': 'Quora User', + 'bio': 'Lead from Quora discussion', + 'post_type': 'discussion', + 'timestamp': 'Recent', + 'upvotes': 0, + 'links': [url] + }] + }) + + except Exception as e: + print(f" ❌ Error processing {url}: {str(e)}") + # Always create a fallback entry + user_info_list.append({ + "website_url": url, + "user_info": [{ + 'username': 'Quora User', + 'bio': 'Lead from Quora - Manual review needed', + 'post_type': 'discussion', + 'timestamp': 'Recent', + 'upvotes': 0, + 'links': [url] + }] + }) + continue + + print(f"\n📊 Total URLs processed: {len(urls)}") + print(f"📊 URLs with data extracted: {len(user_info_list)}") + + return user_info_list + + +def format_leads_data(user_info_list: List[dict]) -> List[dict]: + """Format extracted data into flattened structure""" + flattened_data = [] + + print(f"\n🔧 Formatting {len(user_info_list)} URL data entries...") + + for info in user_info_list: + website_url = info["website_url"] + user_info = info["user_info"] + + print(f" 📄 Processing {website_url}: {len(user_info)} interactions") + + for interaction in user_info: + # Handle both dict and object types + if isinstance(interaction, dict): + username = interaction.get("username", "Unknown") + bio = interaction.get("bio", "") + post_type = interaction.get("post_type", "") + timestamp = interaction.get("timestamp", "") + upvotes = interaction.get("upvotes", 0) + links = interaction.get("links", []) + else: + # Handle Pydantic model objects + username = getattr(interaction, "username", "Unknown") + bio = getattr(interaction, "bio", "") + post_type = getattr(interaction, "post_type", "") + timestamp = getattr(interaction, "timestamp", "") + upvotes = getattr(interaction, "upvotes", 0) + links = getattr(interaction, "links", []) + + flattened_interaction = { + "Website URL": website_url, + "Username": username, + "Bio": bio, + "Post Type": post_type, + "Timestamp": timestamp, + "Upvotes": upvotes, + "Links": ", ".join(links) if isinstance(links, list) else str(links), + } + flattened_data.append(flattened_interaction) + + print(f" ✅ Formatted {len(flattened_data)} total lead entries\n") + return flattened_data + + +def save_to_csv(data: List[dict], filename: str = None) -> str: + """Save lead data to CSV file""" + if not filename: + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") + filename = f"leads_{timestamp}.csv" + + try: + # Ensure we have data + if not data: + return None + + # Write to CSV + with open(filename, 'w', newline='', encoding='utf-8') as csvfile: + fieldnames = ["Website URL", "Username", "Bio", "Post Type", "Timestamp", "Upvotes", "Links"] + writer = csv.DictWriter(csvfile, fieldnames=fieldnames) + + writer.writeheader() + for row in data: + writer.writerow(row) + + return filename + + except Exception as e: + print(f"Error saving to CSV: {e}") + return None + + +def generate_leads( + search_query: str = "AI customer support chatbots", + num_links: int = 3, + firecrawl_api_key: str = None, + openai_api_key: str = None +) -> Dict[str, Any]: + """ + RunAgent entry point: Generate leads from Quora and save to CSV + + Args: + search_query: What kind of leads to search for + num_links: Number of Quora URLs to process (1-10) + firecrawl_api_key: Firecrawl API key (defaults to env var) + openai_api_key: OpenAI API key for intelligent extraction (optional, defaults to env var) + + Returns: + Dictionary with lead generation results and CSV filename + """ + # Load from environment variables if not provided + firecrawl_api_key = firecrawl_api_key or os.getenv("FIRECRAWL_API_KEY") + openai_api_key = openai_api_key or os.getenv("OPENAI_API_KEY") + + if not firecrawl_api_key: + return { + "success": False, + "error": "Missing FIRECRAWL_API_KEY. Please set it in .env file or pass as parameter." + } + + if openai_api_key: + print("🤖 OpenAI integration enabled for intelligent extraction") + else: + print("ℹ️ OpenAI key not provided - using basic extraction") + + try: + print(f"🎯 Searching for leads: {search_query}") + + # Step 1: Search for relevant Quora URLs + print(f"🔍 Searching for {num_links} relevant Quora URLs...") + urls = search_quora_urls(search_query, firecrawl_api_key, num_links) + + if not urls: + return { + "success": False, + "error": "No relevant URLs found" + } + + print(f"✅ Found {len(urls)} URLs") + + # Step 2: Extract lead information + print("📊 Extracting lead information from URLs...") + user_info_list = extract_leads_from_urls(urls, firecrawl_api_key, openai_api_key) + + if not user_info_list: + return { + "success": False, + "error": "No leads extracted from URLs" + } + + # Step 3: Format the data + print("🔧 Formatting lead data...") + formatted_data = format_leads_data(user_info_list) + + print(f"✅ Extracted {len(formatted_data)} leads") + + # Step 4: Save to CSV + print("📝 Saving to CSV file...") + csv_filename = save_to_csv(formatted_data) + + if not csv_filename: + return { + "success": False, + "error": "Failed to create CSV file", + "leads_count": len(formatted_data), + "leads_data": formatted_data + } + + print(f"🎉 Success! CSV file created: {csv_filename}") + + return { + "success": True, + "search_query": search_query, + "urls_processed": len(urls), + "leads_count": len(formatted_data), + "csv_filename": csv_filename, + "csv_path": os.path.abspath(csv_filename), + "openai_used": openai_api_key is not None, + "leads_data": formatted_data + } + + except Exception as e: + import traceback + return { + "success": False, + "error": str(e), + "traceback": traceback.format_exc() + } + + +if __name__ == "__main__": + # Test the function + result = generate_leads( + search_query="AI video editing software", + num_links=2 + ) + print(json.dumps(result, indent=2)) \ No newline at end of file diff --git a/examples/ai_lead_generation/agents/requirements.txt b/examples/ai_lead_generation/agents/requirements.txt new file mode 100644 index 0000000..1f67e91 --- /dev/null +++ b/examples/ai_lead_generation/agents/requirements.txt @@ -0,0 +1,6 @@ +firecrawl-py +agno +pydantic +requests +python-dotenv +openai \ No newline at end of file diff --git a/examples/ai_lead_generation/agents/runagent.config.json b/examples/ai_lead_generation/agents/runagent.config.json new file mode 100644 index 0000000..c1b9eaa --- /dev/null +++ b/examples/ai_lead_generation/agents/runagent.config.json @@ -0,0 +1,27 @@ +{ + "agent_name": "AI Lead Generation Agent", + "description": "Automated lead generation from Quora using Firecrawl Extract endpoint and Google Sheets integration", + "framework": "agno", + "template": "lead_generation", + "version": "1.0.0", + "created_at": "2025-11-02", + "template_source": { + "repo_url": "https://github.com/runagent-dev/runagent.git", + "path": "examples/ai_lead_generation", + "author": "runagent" + }, + "agent_architecture": { + "entrypoints": [ + { + "file": "main.py", + "module": "generate_leads", + "tag": "generate_leads", + "description": "Generate leads from Quora based on search query and save to Google Sheets" + } + ] + }, + "env_vars": { + "FIRECRAWL_API_KEY": "", + "OPENAI_API_KEY": "" + } + } \ No newline at end of file diff --git a/examples/ai_lead_generation/sdk/python/test.py b/examples/ai_lead_generation/sdk/python/test.py new file mode 100644 index 0000000..93253b5 --- /dev/null +++ b/examples/ai_lead_generation/sdk/python/test.py @@ -0,0 +1,14 @@ +from runagent import RunAgentClient + +client = RunAgentClient( + agent_id="bb9bc13a-ced7-4ac7-a350-a101744109ec", + entrypoint_tag="generate_leads", + local=False +) + +result = client.run( + search_query="Ai powered car sell automation", + num_links=3 +) + +print(result) \ No newline at end of file diff --git a/examples/book_writer/backend/app.py b/examples/book_writer/backend/app.py new file mode 100644 index 0000000..30285ac --- /dev/null +++ b/examples/book_writer/backend/app.py @@ -0,0 +1,189 @@ +from flask import Flask, request, jsonify, send_file, Response +from flask_cors import CORS +from runagent import RunAgentClient +import os +import traceback +from datetime import datetime +import io + +app = Flask(__name__) + +# CORS configuration - allow all origins in development, or specify in production +cors_origins = os.getenv('CORS_ORIGINS', '*').split(',') +if cors_origins == ['*']: + CORS(app, resources={r"/api/*": {"origins": "*"}}) +else: + CORS(app, resources={ + r"/api/*": { + "origins": [origin.strip() for origin in cors_origins] + } + }) + +@app.route('/api/generate-outline', methods=['POST']) +def generate_outline(): + """ + Generate book outline + + Expected JSON body: + { + "agent_id": "your-agent-id", + "title": "Book Title", + "topic": "Book topic", + "goal": "Book goal description" + } + """ + try: + data = request.json + + # Validate required fields + if not data.get('agent_id'): + return jsonify({'error': 'agent_id is required'}), 400 + + if not data.get('topic'): + return jsonify({'error': 'topic is required'}), 400 + + agent_id = data['agent_id'] + title = data.get('title', 'Untitled Book') + topic = data['topic'] + goal = data.get('goal', '') + + # Initialize RunAgent client for outline generation + client = RunAgentClient( + agent_id=agent_id, + entrypoint_tag="generate_outline", + local=False # Set to False when using RunAgent Cloud + ) + + # Generate the outline + result = client.run( + title=title, + topic=topic, + goal=goal + ) + + return jsonify(result), 200 + + except Exception as e: + error_trace = traceback.format_exc() + print(f"Error in generate_outline: {error_trace}") + return jsonify({ + 'error': str(e), + 'traceback': error_trace + }), 500 + + +@app.route('/api/write-book', methods=['POST']) +def write_book(): + """ + Write complete book with all chapters + + Expected JSON body: + { + "agent_id": "your-agent-id", + "title": "Book Title", + "topic": "Book topic", + "goal": "Book goal description", + "num_chapters": 5 + } + """ + try: + data = request.json + + # Validate required fields + if not data.get('agent_id'): + return jsonify({'error': 'agent_id is required'}), 400 + + if not data.get('topic'): + return jsonify({'error': 'topic is required'}), 400 + + agent_id = data['agent_id'] + title = data.get('title', 'Untitled Book') + topic = data['topic'] + goal = data.get('goal', '') + num_chapters = data.get('num_chapters', 5) + + # Initialize RunAgent client + client = RunAgentClient( + agent_id=agent_id, + entrypoint_tag="write_full_book", + local=False + ) + + # Write the complete book + result = client.run( + title=title, + topic=topic, + goal=goal, + num_chapters=num_chapters + ) + + return jsonify(result), 200 + + except Exception as e: + error_trace = traceback.format_exc() + print(f"Error in write_book: {error_trace}") + return jsonify({ + 'error': str(e), + 'traceback': error_trace + }), 500 + + +@app.route('/api/download-book', methods=['POST']) +def download_book(): + """ + Download book as markdown file + + Expected JSON body: + { + "title": "Book Title", + "content": "Book content in markdown" + } + """ + try: + data = request.json + + title = data.get('title', 'book') + content = data.get('content', '') + + # Create filename + filename = f"{title.replace(' ', '_')}.md" + + # Create file in memory using BytesIO + file_stream = io.BytesIO() + file_stream.write(content.encode('utf-8')) + file_stream.seek(0) + + # Create a response with proper headers to avoid _FileProxy__buffer issues + response = Response( + file_stream.getvalue(), + mimetype='text/markdown', + headers={ + 'Content-Disposition': f'attachment; filename="{filename}"', + 'Content-Type': 'text/markdown; charset=utf-8' + } + ) + + return response + + except Exception as e: + error_trace = traceback.format_exc() + print(f"Error in download_book: {error_trace}") + return jsonify({ + 'error': str(e), + 'traceback': error_trace + }), 500 + + +@app.route('/api/health', methods=['GET']) +def health_check(): + """Health check endpoint""" + return jsonify({ + 'status': 'healthy', + 'service': 'book-writer-api', + 'version': '1.0.0' + }), 200 + + +if __name__ == '__main__': + port = int(os.getenv('PORT', 8000)) + app.run(host='0.0.0.0', port=port, debug=True) \ No newline at end of file diff --git a/examples/book_writer/frontend/index.html b/examples/book_writer/frontend/index.html new file mode 100644 index 0000000..91639db --- /dev/null +++ b/examples/book_writer/frontend/index.html @@ -0,0 +1,14 @@ + + + + + + + AI Book Writer + + +
+ + + + diff --git a/examples/book_writer/frontend/package-lock.json b/examples/book_writer/frontend/package-lock.json new file mode 100644 index 0000000..85478e1 --- /dev/null +++ b/examples/book_writer/frontend/package-lock.json @@ -0,0 +1,3361 @@ +{ + "name": "book-writer-saas", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "book-writer-saas", + "version": "1.0.0", + "dependencies": { + "axios": "^1.6.0", + "lucide-react": "^0.263.1", + "react": "^18.2.0", + "react-dom": "^18.2.0" + }, + "devDependencies": { + "@types/react": "^18.2.43", + "@types/react-dom": "^18.2.17", + "@vitejs/plugin-react": "^4.2.1", + "autoprefixer": "^10.4.16", + "postcss": "^8.4.32", + "tailwindcss": "^3.3.0", + "typescript": "^5.2.2", + "vite": "^5.0.8" + } + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.5.tgz", + "integrity": "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz", + "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.5", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-module-transforms": "^7.28.3", + "@babel/helpers": "^7.28.4", + "@babel/parser": "^7.28.5", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.5", + "@babel/types": "^7.28.5", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.5.tgz", + "integrity": "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.5", + "@babel/types": "^7.28.5", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", + "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.27.2", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", + "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.28.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", + "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", + "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.4" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", + "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.5" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.5.tgz", + "integrity": "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.5", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.5", + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.5", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", + "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.52.5.tgz", + "integrity": "sha512-8c1vW4ocv3UOMp9K+gToY5zL2XiiVw3k7f1ksf4yO1FlDFQ1C2u72iACFnSOceJFsWskc2WZNqeRhFRPzv+wtQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.52.5.tgz", + "integrity": "sha512-mQGfsIEFcu21mvqkEKKu2dYmtuSZOBMmAl5CFlPGLY94Vlcm+zWApK7F/eocsNzp8tKmbeBP8yXyAbx0XHsFNA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.52.5.tgz", + "integrity": "sha512-takF3CR71mCAGA+v794QUZ0b6ZSrgJkArC+gUiG6LB6TQty9T0Mqh3m2ImRBOxS2IeYBo4lKWIieSvnEk2OQWA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.52.5.tgz", + "integrity": "sha512-W901Pla8Ya95WpxDn//VF9K9u2JbocwV/v75TE0YIHNTbhqUTv9w4VuQ9MaWlNOkkEfFwkdNhXgcLqPSmHy0fA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.52.5.tgz", + "integrity": "sha512-QofO7i7JycsYOWxe0GFqhLmF6l1TqBswJMvICnRUjqCx8b47MTo46W8AoeQwiokAx3zVryVnxtBMcGcnX12LvA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.52.5.tgz", + "integrity": "sha512-jr21b/99ew8ujZubPo9skbrItHEIE50WdV86cdSoRkKtmWa+DDr6fu2c/xyRT0F/WazZpam6kk7IHBerSL7LDQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.52.5.tgz", + "integrity": "sha512-PsNAbcyv9CcecAUagQefwX8fQn9LQ4nZkpDboBOttmyffnInRy8R8dSg6hxxl2Re5QhHBf6FYIDhIj5v982ATQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.52.5.tgz", + "integrity": "sha512-Fw4tysRutyQc/wwkmcyoqFtJhh0u31K+Q6jYjeicsGJJ7bbEq8LwPWV/w0cnzOqR2m694/Af6hpFayLJZkG2VQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.52.5.tgz", + "integrity": "sha512-a+3wVnAYdQClOTlyapKmyI6BLPAFYs0JM8HRpgYZQO02rMR09ZcV9LbQB+NL6sljzG38869YqThrRnfPMCDtZg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.52.5.tgz", + "integrity": "sha512-AvttBOMwO9Pcuuf7m9PkC1PUIKsfaAJ4AYhy944qeTJgQOqJYJ9oVl2nYgY7Rk0mkbsuOpCAYSs6wLYB2Xiw0Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.52.5.tgz", + "integrity": "sha512-DkDk8pmXQV2wVrF6oq5tONK6UHLz/XcEVow4JTTerdeV1uqPeHxwcg7aFsfnSm9L+OO8WJsWotKM2JJPMWrQtA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.52.5.tgz", + "integrity": "sha512-W/b9ZN/U9+hPQVvlGwjzi+Wy4xdoH2I8EjaCkMvzpI7wJUs8sWJ03Rq96jRnHkSrcHTpQe8h5Tg3ZzUPGauvAw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.52.5.tgz", + "integrity": "sha512-sjQLr9BW7R/ZiXnQiWPkErNfLMkkWIoCz7YMn27HldKsADEKa5WYdobaa1hmN6slu9oWQbB6/jFpJ+P2IkVrmw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.52.5.tgz", + "integrity": "sha512-hq3jU/kGyjXWTvAh2awn8oHroCbrPm8JqM7RUpKjalIRWWXE01CQOf/tUNWNHjmbMHg/hmNCwc/Pz3k1T/j/Lg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.52.5.tgz", + "integrity": "sha512-gn8kHOrku8D4NGHMK1Y7NA7INQTRdVOntt1OCYypZPRt6skGbddska44K8iocdpxHTMMNui5oH4elPH4QOLrFQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.52.5.tgz", + "integrity": "sha512-hXGLYpdhiNElzN770+H2nlx+jRog8TyynpTVzdlc6bndktjKWyZyiCsuDAlpd+j+W+WNqfcyAWz9HxxIGfZm1Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.52.5.tgz", + "integrity": "sha512-arCGIcuNKjBoKAXD+y7XomR9gY6Mw7HnFBv5Rw7wQRvwYLR7gBAgV7Mb2QTyjXfTveBNFAtPt46/36vV9STLNg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.52.5.tgz", + "integrity": "sha512-QoFqB6+/9Rly/RiPjaomPLmR/13cgkIGfA40LHly9zcH1S0bN2HVFYk3a1eAyHQyjs3ZJYlXvIGtcCs5tko9Cw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.52.5.tgz", + "integrity": "sha512-w0cDWVR6MlTstla1cIfOGyl8+qb93FlAVutcor14Gf5Md5ap5ySfQ7R9S/NjNaMLSFdUnKGEasmVnu3lCMqB7w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.52.5.tgz", + "integrity": "sha512-Aufdpzp7DpOTULJCuvzqcItSGDH73pF3ko/f+ckJhxQyHtp67rHw3HMNxoIdDMUITJESNE6a8uh4Lo4SLouOUg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.52.5.tgz", + "integrity": "sha512-UGBUGPFp1vkj6p8wCRraqNhqwX/4kNQPS57BCFc8wYh0g94iVIW33wJtQAx3G7vrjjNtRaxiMUylM0ktp/TRSQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.52.5.tgz", + "integrity": "sha512-TAcgQh2sSkykPRWLrdyy2AiceMckNf5loITqXxFI5VuQjS5tSuw3WlwdN8qv8vzjLAUTvYaH/mVjSFpbkFbpTg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "18.3.26", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.26.tgz", + "integrity": "sha512-RFA/bURkcKzx/X9oumPG9Vp3D3JUgus/d0b67KB0t5S/raciymilkOa66olh78MUI92QLbEJevO7rvqU/kjwKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.0.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.3.7", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", + "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^18.0.0" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.27", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true, + "license": "MIT" + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "dev": true, + "license": "MIT" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/autoprefixer": { + "version": "10.4.21", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.21.tgz", + "integrity": "sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "browserslist": "^4.24.4", + "caniuse-lite": "^1.0.30001702", + "fraction.js": "^4.3.7", + "normalize-range": "^0.1.2", + "picocolors": "^1.1.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/axios": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.0.tgz", + "integrity": "sha512-zt40Pz4zcRXra9CVV31KeyofwiNvAbJ5B6YPz9pMJ+yOSLikvPT4Yi5LjfgjRa9CawVYBaD1JQzIVcIvBejKeA==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.4", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.8.20", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.20.tgz", + "integrity": "sha512-JMWsdF+O8Orq3EMukbUN1QfbLK9mX2CkUmQBcW2T0s8OmdAUL5LLM/6wFwSrqXzlXB13yhyK9gTKS1rIizOduQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.27.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.27.0.tgz", + "integrity": "sha512-AXVQwdhot1eqLihwasPElhX2tAZiBjWdJ9i/Zcj2S6QYIjkx62OKSfnobkriB81C3l4w0rVy3Nt4jaTBltYEpw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.8.19", + "caniuse-lite": "^1.0.30001751", + "electron-to-chromium": "^1.5.238", + "node-releases": "^2.0.26", + "update-browserslist-db": "^1.1.4" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001751", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001751.tgz", + "integrity": "sha512-A0QJhug0Ly64Ii3eIqHu5X51ebln3k4yTUkY1j8drqpWHVreg/VLijN48cZ1bYPiqOQuqpkIKnzr/Ul8V+p6Cw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "dev": true, + "license": "MIT" + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.241", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.241.tgz", + "integrity": "sha512-ILMvKX/ZV5WIJzzdtuHg8xquk2y0BOGlFOxBVwTpbiXqWIH0hamG45ddU4R3PQ0gYu+xgo0vdHXHli9sHIGb4w==", + "dev": true, + "license": "ISC" + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fastq": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/form-data": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", + "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fraction.js": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", + "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "patreon", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jiti": { + "version": "1.21.7", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", + "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "bin/jiti.js" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/lucide-react": { + "version": "0.263.1", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.263.1.tgz", + "integrity": "sha512-keqxAx97PlaEN89PXZ6ki1N8nRjGWtDa4021GFYLNj0RgruM5odbpl8GHTExj0hhPq3sF6Up0gnxt6TSHu+ovw==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-releases": { + "version": "2.0.26", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.26.tgz", + "integrity": "sha512-S2M9YimhSjBSvYnlr5/+umAnPHE++ODwt5e2Ij6FoX45HA/s4vHdkDx1eax2pAPeAOqu4s9b7ppahsyEFdVqQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-range": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-import": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", + "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-js": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz", + "integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" + }, + "peerDependencies": { + "postcss": "^8.4.21" + } + }, + "node_modules/postcss-load-config": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz", + "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "lilconfig": "^3.1.1" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "jiti": ">=1.21.0", + "postcss": ">=8.0.9", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + }, + "postcss": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/postcss-nested": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", + "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.1.1" + }, + "engines": { + "node": ">=12.0" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-refresh": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pify": "^2.3.0" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.11", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.52.5.tgz", + "integrity": "sha512-3GuObel8h7Kqdjt0gxkEzaifHTqLVW56Y/bjN7PSQtkKr0w3V/QYSdt6QWYtd7A1xUtYQigtdUfgj1RvWVtorw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.52.5", + "@rollup/rollup-android-arm64": "4.52.5", + "@rollup/rollup-darwin-arm64": "4.52.5", + "@rollup/rollup-darwin-x64": "4.52.5", + "@rollup/rollup-freebsd-arm64": "4.52.5", + "@rollup/rollup-freebsd-x64": "4.52.5", + "@rollup/rollup-linux-arm-gnueabihf": "4.52.5", + "@rollup/rollup-linux-arm-musleabihf": "4.52.5", + "@rollup/rollup-linux-arm64-gnu": "4.52.5", + "@rollup/rollup-linux-arm64-musl": "4.52.5", + "@rollup/rollup-linux-loong64-gnu": "4.52.5", + "@rollup/rollup-linux-ppc64-gnu": "4.52.5", + "@rollup/rollup-linux-riscv64-gnu": "4.52.5", + "@rollup/rollup-linux-riscv64-musl": "4.52.5", + "@rollup/rollup-linux-s390x-gnu": "4.52.5", + "@rollup/rollup-linux-x64-gnu": "4.52.5", + "@rollup/rollup-linux-x64-musl": "4.52.5", + "@rollup/rollup-openharmony-arm64": "4.52.5", + "@rollup/rollup-win32-arm64-msvc": "4.52.5", + "@rollup/rollup-win32-ia32-msvc": "4.52.5", + "@rollup/rollup-win32-x64-gnu": "4.52.5", + "@rollup/rollup-win32-x64-msvc": "4.52.5", + "fsevents": "~2.3.2" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/sucrase": { + "version": "3.35.0", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz", + "integrity": "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "glob": "^10.3.10", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tailwindcss": { + "version": "3.4.18", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.18.tgz", + "integrity": "sha512-6A2rnmW5xZMdw11LYjhcI5846rt9pbLSabY5XPxo+XWdxwZaFEn47Go4NzFiHu9sNNmr/kXivP1vStfvMaK1GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "arg": "^5.0.2", + "chokidar": "^3.6.0", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.3.2", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "jiti": "^1.21.7", + "lilconfig": "^3.1.3", + "micromatch": "^4.0.8", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.1.1", + "postcss": "^8.4.47", + "postcss-import": "^15.1.0", + "postcss-js": "^4.0.1", + "postcss-load-config": "^4.0.2 || ^5.0 || ^6.0", + "postcss-nested": "^6.2.0", + "postcss-selector-parser": "^6.1.2", + "resolve": "^1.22.8", + "sucrase": "^3.35.0" + }, + "bin": { + "tailwind": "lib/cli.js", + "tailwindcss": "lib/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.4.tgz", + "integrity": "sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + } + } +} diff --git a/examples/book_writer/frontend/package.json b/examples/book_writer/frontend/package.json new file mode 100644 index 0000000..2e38672 --- /dev/null +++ b/examples/book_writer/frontend/package.json @@ -0,0 +1,28 @@ +{ + "name": "book-writer-saas", + "version": "1.0.0", + "type": "module", + "description": "AI Book Writer SaaS Frontend", + "private": true, + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "preview": "vite preview" + }, + "dependencies": { + "react": "^18.2.0", + "react-dom": "^18.2.0", + "axios": "^1.6.0", + "lucide-react": "^0.263.1" + }, + "devDependencies": { + "@types/react": "^18.2.43", + "@types/react-dom": "^18.2.17", + "@vitejs/plugin-react": "^4.2.1", + "typescript": "^5.2.2", + "vite": "^5.0.8", + "tailwindcss": "^3.3.0", + "autoprefixer": "^10.4.16", + "postcss": "^8.4.32" + } + } \ No newline at end of file diff --git a/examples/book_writer/frontend/postcss.config.cjs b/examples/book_writer/frontend/postcss.config.cjs new file mode 100644 index 0000000..33ad091 --- /dev/null +++ b/examples/book_writer/frontend/postcss.config.cjs @@ -0,0 +1,6 @@ +module.exports = { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/examples/book_writer/frontend/public/index.html b/examples/book_writer/frontend/public/index.html new file mode 100644 index 0000000..e69de29 diff --git a/examples/book_writer/frontend/src/App.tsx b/examples/book_writer/frontend/src/App.tsx new file mode 100644 index 0000000..e80ec0c --- /dev/null +++ b/examples/book_writer/frontend/src/App.tsx @@ -0,0 +1,556 @@ +import React, { useState } from 'react'; +import { BookOpen, FileText, Download, Loader2, ArrowRight, ArrowLeft, Sparkles, Check, ChevronDown, ChevronUp, Eye, EyeOff } from 'lucide-react'; + +interface Chapter { + number: number; + title: string; + description?: string; + content?: string; + word_count?: number; +} + +interface BookResult { + success: boolean; + title: string; + topic: string; + goal: string; + book_content: string; + chapters: Chapter[]; + total_chapters: number; + total_words: number; + outline: Chapter[]; + error?: string; +} + +function App() { + const [step, setStep] = useState<'config' | 'processing' | 'results'>('config'); + const [agentId, setAgentId] = useState(''); + const [bookTitle, setBookTitle] = useState(''); + const [topic, setTopic] = useState(''); + const [goal, setGoal] = useState(''); + const [numChapters, setNumChapters] = useState(5); + const [result, setResult] = useState(null); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const [progress, setProgress] = useState(''); + const [expandedChapters, setExpandedChapters] = useState>(new Set()); + const [viewMode, setViewMode] = useState<'overview' | 'full'>('overview'); + + const generateBook = async () => { + setLoading(true); + setError(null); + setStep('processing'); + setProgress('Initializing book generation...'); + + try { + setProgress('Generating book outline...'); + + const response = await fetch('/api/write-book', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + agent_id: agentId, + title: bookTitle, + topic: topic, + goal: goal, + num_chapters: numChapters + }), + }); + + if (!response.ok) { + throw new Error('Failed to generate book'); + } + + const data: BookResult = await response.json(); + + if (data.success) { + setResult(data); + setStep('results'); + } else { + throw new Error(data.error || 'Book generation failed'); + } + } catch (err) { + setError(err instanceof Error ? err.message : 'An error occurred'); + setStep('config'); + } finally { + setLoading(false); + } + }; + + const downloadBook = async () => { + if (!result) return; + + try { + const response = await fetch('/api/download-book', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + title: result.title, + content: result.book_content + }), + }); + + const blob = await response.blob(); + const url = window.URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = `${result.title.replace(/\s+/g, '_')}.md`; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + window.URL.revokeObjectURL(url); + } catch (err) { + console.error('Download error:', err); + alert('Failed to download book'); + } + }; + + const resetForm = () => { + setStep('config'); + setResult(null); + setError(null); + setProgress(''); + setExpandedChapters(new Set()); + setViewMode('overview'); + }; + + const toggleChapter = (chapterNumber: number) => { + const newExpanded = new Set(expandedChapters); + if (newExpanded.has(chapterNumber)) { + newExpanded.delete(chapterNumber); + } else { + newExpanded.add(chapterNumber); + } + setExpandedChapters(newExpanded); + }; + + const formatMarkdown = (text: string) => { + if (!text) return { __html: '' }; + + let formatted = text; + + // Split by lines to process properly + const lines = formatted.split('\n'); + const processed: string[] = []; + let inList = false; + let listItems: string[] = []; + + for (let i = 0; i < lines.length; i++) { + const line = lines[i].trim(); + + // Headers (process first) + if (line.match(/^#{1,6}\s/)) { + // Close any open list + if (inList) { + processed.push(`
    ${listItems.join('')}
`); + listItems = []; + inList = false; + } + + if (line.startsWith('### ')) { + processed.push(`

${line.substring(4)}

`); + } else if (line.startsWith('## ')) { + processed.push(`

${line.substring(3)}

`); + } else if (line.startsWith('# ')) { + processed.push(`

${line.substring(2)}

`); + } + continue; + } + + // List items + if (line.match(/^[-+*]\s/) || line.match(/^\d+\.\s/)) { + if (!inList) { + inList = true; + } + const content = line.replace(/^[-+*]\s/, '').replace(/^\d+\.\s/, ''); + // Process inline formatting + const processedContent = content + .replace(/\*\*(.*?)\*\*/g, '$1') + .replace(/\*(.*?)\*/g, '$1'); + listItems.push(`
  • ${processedContent}
  • `); + continue; + } + + // Close list if we hit a non-list line + if (inList) { + processed.push(`
      ${listItems.join('')}
    `); + listItems = []; + inList = false; + } + + // Empty line = paragraph break + if (line === '') { + processed.push(''); + continue; + } + + // Regular paragraph line + let para = line; + + // Process inline formatting (bold, italic, links) + // Bold first (double asterisks) + para = para.replace(/\*\*(.*?)\*\*/g, '$1'); + // Then italic (single asterisks not part of bold) + para = para.replace(/\*([^*]+?)\*/g, '$1'); + // Links + para = para.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '$1'); + + processed.push(`

    ${para}

    `); + } + + // Close any remaining list + if (inList) { + processed.push(`
      ${listItems.join('')}
    `); + } + + return { __html: processed.join('\n') }; + }; + + return ( +
    +
    + {/* Header */} +
    +
    + +

    + AI Book Writer +

    +
    +

    + Generate complete books with AI-powered research and writing +

    +
    + + Powered by CrewAI & RunAgent +
    +
    + + {/* Configuration Step */} + {step === 'config' && ( +
    +

    + + Book Configuration +

    + +
    + {/* Agent ID */} +
    + + setAgentId(e.target.value)} + placeholder="Enter your deployed agent ID" + className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent transition" + /> +

    + Get this from: runagent serve . +

    +
    + + {/* Book Title */} +
    + + setBookTitle(e.target.value)} + placeholder="e.g., The Future of AI in Healthcare" + className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent transition" + /> +
    + + {/* Topic */} +
    + + setTopic(e.target.value)} + placeholder="e.g., AI applications in medical diagnosis and treatment" + className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent transition" + /> +
    + + {/* Goal */} +
    + + +
    + + +
    +
    + +
    +
    + + + + + + + + + +
    +

    Powered by RunAgent & Agno AI

    +
    +
    + + + + + diff --git a/examples/rag_agent/frontend/package-lock.json b/examples/rag_agent/frontend/package-lock.json new file mode 100644 index 0000000..da73a7e --- /dev/null +++ b/examples/rag_agent/frontend/package-lock.json @@ -0,0 +1,642 @@ +{ + "name": "rag-agent-frontend", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "rag-agent-frontend", + "version": "1.0.0", + "devDependencies": { + "http-server": "^14.1.1" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "dev": true, + "license": "MIT" + }, + "node_modules/basic-auth": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", + "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "5.1.2" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/corser": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/corser/-/corser-2.0.1.tgz", + "integrity": "sha512-utCYNzRSQIZNPIcGZdQc92UVJYAhtGAteCFg0yRaFm8f0P+CPtyGyHXJcGXnffjCybUCEx3FQ2G7U3/o9eIkVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "dev": true, + "license": "MIT" + }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true, + "license": "MIT", + "bin": { + "he": "bin/he" + } + }, + "node_modules/html-encoding-sniffer": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz", + "integrity": "sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-encoding": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/http-proxy": { + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", + "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eventemitter3": "^4.0.0", + "follow-redirects": "^1.0.0", + "requires-port": "^1.0.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/http-server": { + "version": "14.1.1", + "resolved": "https://registry.npmjs.org/http-server/-/http-server-14.1.1.tgz", + "integrity": "sha512-+cbxadF40UXd9T01zUHgA+rlo2Bg1Srer4+B4NwIHdaGxAGGv59nYRnGGDJ9LBk7alpS0US+J+bLLdQOOkJq4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "basic-auth": "^2.0.1", + "chalk": "^4.1.2", + "corser": "^2.0.1", + "he": "^1.2.0", + "html-encoding-sniffer": "^3.0.0", + "http-proxy": "^1.18.1", + "mime": "^1.6.0", + "minimist": "^1.2.6", + "opener": "^1.5.1", + "portfinder": "^1.0.28", + "secure-compare": "3.0.1", + "union": "~0.5.0", + "url-join": "^4.0.1" + }, + "bin": { + "http-server": "bin/http-server" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/opener": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/opener/-/opener-1.5.2.tgz", + "integrity": "sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==", + "dev": true, + "license": "(WTFPL OR MIT)", + "bin": { + "opener": "bin/opener-bin.js" + } + }, + "node_modules/portfinder": { + "version": "1.0.38", + "resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.38.tgz", + "integrity": "sha512-rEwq/ZHlJIKw++XtLAO8PPuOQA/zaPJOZJ37BVuN97nLpMJeuDVLVGRwbFoBgLudgdTMP2hdRJP++H+8QOA3vg==", + "dev": true, + "license": "MIT", + "dependencies": { + "async": "^3.2.6", + "debug": "^4.3.6" + }, + "engines": { + "node": ">= 10.12" + } + }, + "node_modules/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/secure-compare": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/secure-compare/-/secure-compare-3.0.1.tgz", + "integrity": "sha512-AckIIV90rPDcBcglUwXPF3kg0P0qmPsPXAj6BBEENQE1p5yA1xfmDJzfi1Tappj37Pv2mVbKpL3Z1T+Nn7k1Qw==", + "dev": true, + "license": "MIT" + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/union": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/union/-/union-0.5.0.tgz", + "integrity": "sha512-N6uOhuW6zO95P3Mel2I2zMsbsanvvtgn6jVqJv4vbVcz/JN0OkL9suomjQGmWtxJQXOCqUJvquc1sMeNz/IwlA==", + "dev": true, + "dependencies": { + "qs": "^6.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/url-join": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/url-join/-/url-join-4.0.1.tgz", + "integrity": "sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==", + "dev": true, + "license": "MIT" + }, + "node_modules/whatwg-encoding": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz", + "integrity": "sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=12" + } + } + } +} diff --git a/examples/rag_agent/frontend/package.json b/examples/rag_agent/frontend/package.json new file mode 100644 index 0000000..a5d3e19 --- /dev/null +++ b/examples/rag_agent/frontend/package.json @@ -0,0 +1,12 @@ +{ + "name": "rag-agent-frontend", + "version": "1.0.0", + "description": "RAG Agent Frontend - Intelligent Document Assistant", + "scripts": { + "dev": "http-server -p 3000 -o" + }, + "devDependencies": { + "http-server": "^14.1.1" + } +} + diff --git a/examples/rag_agent/frontend/style.css b/examples/rag_agent/frontend/style.css new file mode 100644 index 0000000..65fad5a --- /dev/null +++ b/examples/rag_agent/frontend/style.css @@ -0,0 +1,751 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; + background: linear-gradient(135deg, #667eea 0%, #764ba2 50%, #f093fb 100%); + background-attachment: fixed; + min-height: 100vh; + padding: 20px; + position: relative; + overflow-x: hidden; +} + +body::before { + content: ''; + position: fixed; + top: -50%; + left: -50%; + width: 200%; + height: 200%; + background: radial-gradient(circle at 30% 20%, rgba(255,255,255,0.1) 0%, transparent 50%), + radial-gradient(circle at 70% 80%, rgba(255,255,255,0.1) 0%, transparent 50%); + animation: float 20s ease-in-out infinite; + pointer-events: none; + z-index: 0; +} + +.container { + max-width: 1000px; + margin: 0 auto; + position: relative; + z-index: 1; +} + +/* Header */ +header { + text-align: center; + color: white; + margin-bottom: 40px; + animation: fadeInDown 0.8s ease; +} + +header h1 { + font-size: 3.5em; + margin-bottom: 10px; + text-shadow: 2px 2px 20px rgba(0,0,0,0.3); + background: linear-gradient(135deg, #fff 0%, #f0f0f0 100%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; + font-weight: 700; + letter-spacing: -1px; +} + +.tagline { + font-size: 1.2em; + opacity: 0.95; + text-shadow: 1px 1px 10px rgba(0,0,0,0.2); + font-weight: 300; +} + +/* Main Content */ +main { + background: rgba(255, 255, 255, 0.98); + backdrop-filter: blur(10px); + border-radius: 24px; + padding: 50px; + box-shadow: 0 25px 80px rgba(0,0,0,0.25), + 0 0 0 1px rgba(255,255,255,0.5); + animation: fadeInUp 0.8s ease; + border: 1px solid rgba(255, 255, 255, 0.2); +} + +/* Tabs */ +.tabs { + display: flex; + gap: 10px; + margin-bottom: 30px; + border-bottom: 2px solid #e9ecef; + padding-bottom: 10px; +} + +.tab-btn { + padding: 12px 24px; + border: none; + background: transparent; + color: #666; + font-size: 1em; + font-weight: 600; + cursor: pointer; + border-bottom: 3px solid transparent; + transition: all 0.3s ease; + position: relative; + top: 2px; +} + +.tab-btn:hover { + color: #667eea; +} + +.tab-btn.active { + color: #667eea; + border-bottom-color: #667eea; +} + +.tab-content { + display: none; +} + +.tab-content.active { + display: block; +} + +/* Examples Section */ +.examples-section { + margin-bottom: 40px; +} + +.examples-section h2 { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; + margin-bottom: 24px; + font-size: 1.6em; + font-weight: 700; +} + +.examples-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); + gap: 15px; + margin-bottom: 30px; +} + +.example-card { + background: linear-gradient(135deg, #f8f9fa 0%, #ffffff 100%); + border: 2px solid #e9ecef; + border-radius: 16px; + padding: 20px; + cursor: pointer; + transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1); + position: relative; + overflow: hidden; +} + +.example-card::before { + content: ''; + position: absolute; + top: 0; + left: -100%; + width: 100%; + height: 100%; + background: linear-gradient(90deg, transparent, rgba(255,255,255,0.4), transparent); + transition: left 0.5s ease; +} + +.example-card:hover::before { + left: 100%; +} + +.example-card:hover { + border-color: #667eea; + background: #fff; + transform: translateY(-4px); + box-shadow: 0 6px 20px rgba(102, 126, 234, 0.25); +} + +.example-card h3 { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; + font-size: 1.05em; + margin-bottom: 10px; + font-weight: 600; + position: relative; + z-index: 1; +} + +.example-card p { + font-size: 0.9em; + color: #666; + margin: 6px 0; + position: relative; + z-index: 1; +} + +/* Form Section */ +.form-section h2 { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; + margin-bottom: 30px; + font-size: 1.6em; + font-weight: 700; +} + +.form-group { + margin-bottom: 20px; +} + +.form-group label { + display: block; + font-weight: 600; + color: #555; + margin-bottom: 8px; + font-size: 1em; +} + +.form-group input, +.form-group textarea, +.form-group select { + width: 100%; + padding: 14px 16px; + border: 2px solid #e9ecef; + border-radius: 12px; + font-size: 1em; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + font-family: inherit; + background: white; + cursor: pointer; + appearance: none; + -webkit-appearance: none; + -moz-appearance: none; + background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='6 9 12 15 18 9'%3e%3c/polyline%3e%3c/svg%3e"); + background-repeat: no-repeat; + background-position: right 12px center; + background-size: 16px; + padding-right: 40px; +} + +.form-group select:disabled { + opacity: 0.6; + cursor: not-allowed; + background-color: #f5f5f5; +} + +.form-group input[type="file"] { + padding: 10px; +} + +.form-group input:focus, +.form-group textarea:focus, +.form-group select:focus { + outline: none; + border-color: #667eea; + box-shadow: 0 0 0 4px rgba(102, 126, 234, 0.1), + inset 0 0 0 1px #667eea; + transform: translateY(-1px); +} + +.form-group textarea { + resize: vertical; +} + +.form-hint { + display: block; + margin-top: 6px; + font-size: 0.85em; + color: #888; +} + +/* Buttons */ +.form-actions { + display: flex; + gap: 15px; + margin-top: 30px; +} + +.btn { + flex: 1; + padding: 15px 30px; + border: none; + border-radius: 8px; + font-size: 1em; + font-weight: 600; + cursor: pointer; + transition: all 0.3s ease; +} + +.btn-primary { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; + position: relative; + overflow: hidden; +} + +.btn-primary::before { + content: ''; + position: absolute; + top: 50%; + left: 50%; + width: 0; + height: 0; + border-radius: 50%; + background: rgba(255, 255, 255, 0.2); + transform: translate(-50%, -50%); + transition: width 0.6s, height 0.6s; +} + +.btn-primary:hover::before { + width: 300px; + height: 300px; +} + +.btn-primary:hover { + transform: translateY(-3px); + box-shadow: 0 8px 25px rgba(102, 126, 234, 0.5); +} + +.btn-secondary { + background: linear-gradient(135deg, #6c757d 0%, #5a6268 100%); + color: white; +} + +.btn-secondary:hover { + background: linear-gradient(135deg, #5a6268 0%, #495057 100%); + transform: translateY(-3px); + box-shadow: 0 8px 25px rgba(108, 117, 125, 0.4); +} + +.btn-clear { + background: #dc3545; + color: white; + padding: 8px 16px; + font-size: 0.9em; + flex: 0; +} + +.btn-clear:hover { + background: #c82333; +} + +.btn:disabled { + opacity: 0.6; + cursor: not-allowed; + transform: none !important; +} + +/* Results Section */ +.metadata-section { + background: linear-gradient(135deg, #e3f2fd 0%, #f3e5f5 100%); + border: 1px solid #bbdefb; + border-radius: 12px; + padding: 15px 20px; + margin-bottom: 20px; +} + +.metadata-item { + margin: 8px 0; + font-size: 0.95em; + color: #555; +} + +.metadata-item strong { + color: #667eea; +} + +.answer-content h3 { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; + margin-top: 24px; + margin-bottom: 12px; + font-weight: 700; +} + +.answer-content h3:first-child { + margin-top: 0; +} + +.answer-content ul, +.answer-content ol { + margin-left: 25px; + margin-bottom: 15px; +} + +.answer-content li { + margin-bottom: 8px; +} + +.answer-content p { + margin-bottom: 15px; +} + +.answer-content strong { + color: #555; +} + +/* Chat Section */ +.chat-section { + margin-top: 20px; +} + +.chat-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 12px; +} + +.chat-header h2 { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; + font-size: 1.6em; + font-weight: 700; +} + +.chat-container { + background: linear-gradient(135deg, #f8f9fa 0%, #ffffff 100%); + border: 1px solid #e9ecef; + border-radius: 16px; + height: 520px; + overflow: hidden; + display: flex; + flex-direction: column; + box-shadow: inset 0 2px 8px rgba(0,0,0,0.05); +} + +.messages { + flex: 1; + overflow-y: auto; + padding: 20px; + display: flex; + flex-direction: column; + gap: 12px; +} + +.message { + display: flex; + gap: 12px; + align-items: flex-start; + animation: fadeIn 0.2s ease; +} + +.avatar { + width: 32px; + height: 32px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-size: 16px; +} + +.avatar.user { background: #e8f0fe; color: #1a73e8; } +.avatar.assistant { background: #e8f5e9; color: #2e7d32; } + +.bubble { + max-width: 80%; + padding: 12px 14px; + border-radius: 14px; + border: 1px solid #e9ecef; + background: #fff; + line-height: 1.7; + color: #333; +} + +.message.user .bubble { + background: #eef2ff; + border-color: #c7d2fe; +} + +.message.assistant .bubble { + background: #ffffff; +} + +.bubble .content { + font-size: 0.98em; +} + +.bubble .content h3 { margin-top: 8px; margin-bottom: 8px; } +.bubble .content ul { margin-left: 20px; margin-bottom: 10px; } +.bubble .content li { margin-bottom: 6px; } +.bubble .content p { margin-bottom: 10px; } + +.chat-input-bar { + margin-top: 12px; +} + +.chat-input-wrapper { + display: flex; + gap: 10px; + align-items: flex-end; +} + +.chat-input { + flex: 1; + padding: 12px 14px; + border: 2px solid #e9ecef; + border-radius: 12px; + font-family: inherit; + font-size: 1em; + max-height: 160px; + min-height: 44px; + overflow-y: auto; + resize: none; +} + +.chat-input:focus { + outline: none; + border-color: #667eea; + box-shadow: 0 0 0 4px rgba(102, 126, 234, 0.1), inset 0 0 0 1px #667eea; +} + +.chat-actions { + display: flex; + gap: 8px; +} + +/* Upload Results */ +.upload-results { + margin-top: 30px; + padding: 20px; + border-radius: 12px; + border: 2px solid #e9ecef; +} + +.upload-success { + background: linear-gradient(135deg, #d4edda 0%, #c3e6cb 100%); + padding: 20px; + border-radius: 12px; + color: #155724; +} + +.upload-success h3 { + margin-bottom: 10px; +} + +.upload-success p { + margin: 8px 0; +} + +.upload-error { + background: linear-gradient(135deg, #f8d7da 0%, #f5c6cb 100%); + padding: 20px; + border-radius: 12px; + color: #721c24; +} + +.upload-error h3 { + margin-bottom: 10px; +} + +/* Stats Section */ +.stats-section { + margin-bottom: 30px; +} + +.stats-section h2 { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; + margin-bottom: 24px; + font-size: 1.6em; + font-weight: 700; +} + +.stats-section .btn { + margin-bottom: 20px; +} + +.stats-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); + gap: 20px; +} + +.stat-card { + background: linear-gradient(135deg, #f8f9fa 0%, #ffffff 100%); + border: 2px solid #e9ecef; + border-radius: 16px; + padding: 24px; + transition: all 0.3s ease; +} + +.stat-card:hover { + border-color: #667eea; + transform: translateY(-4px); + box-shadow: 0 6px 20px rgba(102, 126, 234, 0.25); +} + +.stat-card h3 { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; + font-size: 1.2em; + margin-bottom: 10px; + font-weight: 700; +} + +.stat-description { + color: #666; + font-size: 0.9em; + margin-bottom: 15px; +} + +.stat-value { + font-size: 1.1em; + color: #333; + margin-bottom: 10px; +} + +.stat-value strong { + color: #667eea; +} + +.stat-collection { + color: #888; + font-size: 0.85em; + margin-top: 10px; +} + +.stat-error { + margin-top: 10px; + color: #dc3545; + font-size: 0.85em; +} + +.error-message { + padding: 20px; + background: #f8d7da; + border-radius: 12px; + color: #721c24; + text-align: center; +} + +/* Loading Indicator */ +.loading { + text-align: center; + padding: 40px; +} + +.spinner { + width: 60px; + height: 60px; + margin: 0 auto 20px; + border: 5px solid rgba(102, 126, 234, 0.1); + border-top: 5px solid #667eea; + border-right: 5px solid #764ba2; + border-radius: 50%; + animation: spin 1s linear infinite; +} + +@keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} + +.loading p { + color: #666; + font-size: 1.1em; +} + +/* Footer */ +footer { + text-align: center; + color: white; + margin-top: 30px; + padding: 20px; + opacity: 0.8; +} + +/* Animations */ +@keyframes fadeInDown { + from { + opacity: 0; + transform: translateY(-30px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +@keyframes fadeInUp { + from { + opacity: 0; + transform: translateY(30px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +@keyframes fadeIn { + from { + opacity: 0; + } + to { + opacity: 1; + } +} + +@keyframes float { + 0%, 100% { + transform: translateY(0) translateX(0); + } + 33% { + transform: translateY(-20px) translateX(10px); + } + 66% { + transform: translateY(20px) translateX(-10px); + } +} + +/* Responsive Design */ +@media (max-width: 768px) { + body { + padding: 10px; + } + + main { + padding: 25px; + } + + header h1 { + font-size: 2em; + } + + .form-actions { + flex-direction: column; + } + + .examples-grid, + .stats-grid { + grid-template-columns: 1fr; + } + + .tabs { + flex-direction: column; + } + + .tab-btn { + text-align: left; + } + + .bubble { max-width: 100%; } +} + diff --git a/examples/rag_agent/manage_documents.py b/examples/rag_agent/manage_documents.py new file mode 100644 index 0000000..ec3419d --- /dev/null +++ b/examples/rag_agent/manage_documents.py @@ -0,0 +1,249 @@ +#!/usr/bin/env python3 +""" +Document Management Script for RAG Router Agent + +This script is used to add documents to the Qdrant databases. +Run this OUTSIDE of RunAgent to populate your databases. + +Usage: + python manage_documents.py add /path/to/document.pdf products + python manage_documents.py list + python manage_documents.py stats +""" + +import os +import sys +from typing import Dict, Any +from dotenv import load_dotenv +from langchain_text_splitters import RecursiveCharacterTextSplitter +from langchain_community.document_loaders import PyPDFLoader +from langchain_community.vectorstores import Qdrant +from langchain_openai import OpenAIEmbeddings +from qdrant_client import QdrantClient +from qdrant_client.models import Distance, VectorParams + +# Load environment variables +load_dotenv() + +# Collection configurations +COLLECTIONS = { + "products": { + "name": "Product Information", + "collection_name": "products_collection", + "description": "Product details, specifications, and features" + }, + "support": { + "name": "Customer Support & FAQ", + "collection_name": "support_collection", + "description": "Customer support information, FAQs, and guides" + }, + "finance": { + "name": "Financial Information", + "collection_name": "finance_collection", + "description": "Financial data, revenue, costs, and liabilities" + } +} + + +class DocumentManager: + """Manage documents in Qdrant databases""" + + def __init__(self): + self.openai_api_key = os.getenv("OPENAI_API_KEY") + self.qdrant_url = os.getenv("QDRANT_URL") + self.qdrant_api_key = os.getenv("QDRANT_API_KEY") + + if not all([self.openai_api_key, self.qdrant_url, self.qdrant_api_key]): + raise ValueError("Missing required environment variables: OPENAI_API_KEY, QDRANT_URL, QDRANT_API_KEY") + + self.embeddings = OpenAIEmbeddings( + model="text-embedding-3-small", + api_key=self.openai_api_key + ) + + self.client = QdrantClient( + url=self.qdrant_url, + api_key=self.qdrant_api_key + ) + + self._initialize_collections() + + def _initialize_collections(self): + """Initialize Qdrant collections if they don't exist""" + vector_size = 1536 # OpenAI embedding size + + for db_type, config in COLLECTIONS.items(): + try: + self.client.get_collection(config["collection_name"]) + print(f"✅ Collection '{config['collection_name']}' already exists") + except Exception: + # Create collection if it doesn't exist + self.client.create_collection( + collection_name=config["collection_name"], + vectors_config=VectorParams(size=vector_size, distance=Distance.COSINE) + ) + print(f"✨ Created collection '{config['collection_name']}'") + + def add_document(self, file_path: str, db_type: str) -> Dict[str, Any]: + """Add a PDF document to specified database""" + + if db_type not in COLLECTIONS: + return { + "success": False, + "message": f"Invalid database type. Must be one of: {list(COLLECTIONS.keys())}" + } + + if not os.path.exists(file_path): + return { + "success": False, + "message": f"File not found: {file_path}" + } + + if not file_path.lower().endswith('.pdf'): + return { + "success": False, + "message": "Only PDF files are supported" + } + + try: + print(f"\n📄 Processing document: {file_path}") + print(f"📊 Target database: {COLLECTIONS[db_type]['name']}") + + # Load PDF + loader = PyPDFLoader(file_path) + documents = loader.load() + print(f"✅ Loaded {len(documents)} pages") + + # Split into chunks + text_splitter = RecursiveCharacterTextSplitter( + chunk_size=1000, + chunk_overlap=200 + ) + texts = text_splitter.split_documents(documents) + print(f"✅ Split into {len(texts)} chunks") + + if texts: + # Create Qdrant vectorstore + db = Qdrant( + client=self.client, + collection_name=COLLECTIONS[db_type]["collection_name"], + embeddings=self.embeddings + ) + + # Add documents + print(f"⏳ Adding to Qdrant...") + db.add_documents(texts) + + return { + "success": True, + "message": f"Successfully added {len(texts)} chunks to {COLLECTIONS[db_type]['name']}", + "chunks_added": len(texts), + "database": COLLECTIONS[db_type]["name"] + } + else: + return { + "success": False, + "message": "No text extracted from document" + } + + except Exception as e: + return { + "success": False, + "message": f"Error processing document: {str(e)}" + } + + def list_collections(self): + """List all collections and their info""" + print("\n📚 Qdrant Collections:") + print("=" * 60) + + collections = self.client.get_collections() + + for collection in collections.collections: + info = self.client.get_collection(collection.name) + print(f"\n📦 {collection.name}") + print(f" Vectors: {info.vectors_count}") + print(f" Status: {info.status}") + + def get_stats(self): + """Get statistics for all databases""" + print("\n📊 Database Statistics:") + print("=" * 60) + + for db_type, config in COLLECTIONS.items(): + try: + info = self.client.get_collection(config["collection_name"]) + print(f"\n{config['name']}") + print(f" Collection: {config['collection_name']}") + print(f" Documents: {info.vectors_count}") + print(f" Description: {config['description']}") + except Exception as e: + print(f"\n{config['name']}") + print(f" Status: ❌ Not found or error: {str(e)}") + + +def main(): + """Main CLI interface""" + if len(sys.argv) < 2: + print(""" +RAG Router Agent - Document Manager + +Usage: + python manage_documents.py add + python manage_documents.py list + python manage_documents.py stats + +Database Types: + - products: Product information, specifications, features + - support: Customer support, FAQs, troubleshooting + - finance: Financial data, pricing, revenue reports + +Examples: + python manage_documents.py add product_manual.pdf products + python manage_documents.py add faq_guide.pdf support + python manage_documents.py add financial_report.pdf finance + python manage_documents.py list + python manage_documents.py stats + """) + sys.exit(1) + + try: + manager = DocumentManager() + + command = sys.argv[1].lower() + + if command == "add": + if len(sys.argv) != 4: + print("❌ Usage: python manage_documents.py add ") + sys.exit(1) + + file_path = sys.argv[2] + db_type = sys.argv[3].lower() + + result = manager.add_document(file_path, db_type) + + if result["success"]: + print(f"\n✅ {result['message']}") + print(f"📊 Chunks added: {result['chunks_added']}") + else: + print(f"\n❌ {result['message']}") + sys.exit(1) + + elif command == "list": + manager.list_collections() + + elif command == "stats": + manager.get_stats() + + else: + print(f"❌ Unknown command: {command}") + print("Available commands: add, list, stats") + sys.exit(1) + + except Exception as e: + print(f"\n❌ Error: {str(e)}") + sys.exit(1) + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/examples/rag_agent/nvidia_fin.pdf b/examples/rag_agent/nvidia_fin.pdf new file mode 100644 index 0000000..e50c57a Binary files /dev/null and b/examples/rag_agent/nvidia_fin.pdf differ diff --git a/examples/rag_agent/sdk/python/test.py b/examples/rag_agent/sdk/python/test.py new file mode 100644 index 0000000..c25df05 --- /dev/null +++ b/examples/rag_agent/sdk/python/test.py @@ -0,0 +1,47 @@ +from runagent import RunAgentClient + +# # Non-streaming client +# client = RunAgentClient( +# agent_id="5d072242-bb9c-4567-bbdb-432811697060", +# entrypoint_tag="query", +# local=True +# ) + +# result = client.run( +# question="i am sad. Tell me a joke." +# ) + +# print("=== Non-Streaming Result ===") +# print(result["answer"]) +# print(f"Source: {result['source']}") +# print(f"Database: {result['database_used']}") + +# Streaming client +print("\n=== Streaming Result ===") +streaming_client = RunAgentClient( + agent_id="5d072242-bb9c-4567-bbdb-432811697060", + entrypoint_tag="query_stream", + local=True +) + +# Stream the response +stream = streaming_client.run_stream( + question="What is Nvidia's price-to-sales (P/S) ratio according to the document? And also the cash flow analysis in the document?" +) + +metadata = None +full_answer = "" +for chunk in stream: + if chunk.get("type") == "metadata": + metadata = chunk + print(f"Source: {chunk.get('source')}") + print(f"Database: {chunk.get('database_used')}") + print("Answer (streaming): ", end="", flush=True) + elif chunk.get("type") == "content": + content = chunk.get("content", "") + print(content, end="", flush=True) + full_answer += content + elif chunk.get("type") == "complete": + print(f"\n\nStreaming complete! Total length: {chunk.get('total_length')} characters") + elif chunk.get("type") == "error": + print(f"\nError: {chunk.get('error')}") \ No newline at end of file diff --git a/examples/rag_agent/test_connection.py b/examples/rag_agent/test_connection.py new file mode 100644 index 0000000..5561f02 --- /dev/null +++ b/examples/rag_agent/test_connection.py @@ -0,0 +1,54 @@ +#!/usr/bin/env python3 +""" +Test script to check if backend is accessible +""" +import requests +import sys + +def test_backend(url): + """Test if backend is accessible""" + print(f"Testing backend at: {url}") + print("=" * 60) + + try: + # Test health endpoint + response = requests.get(f"{url}/health", timeout=5) + print(f"✅ SUCCESS! Backend is accessible") + print(f" Status Code: {response.status_code}") + print(f" Response: {response.json()}") + return True + except requests.exceptions.ConnectionError: + print(f"❌ FAILED: Cannot connect to backend") + print(f" Error: Connection refused") + print(f" Possible causes:") + print(f" 1. Backend server is not running") + print(f" 2. Port 5000 is blocked by firewall") + print(f" 3. Backend is not listening on the correct interface") + return False + except requests.exceptions.Timeout: + print(f"❌ FAILED: Connection timed out") + print(f" The backend is not responding") + return False + except Exception as e: + print(f"❌ FAILED: {type(e).__name__}: {str(e)}") + return False + +if __name__ == '__main__': + # Test localhost + print("\n1. Testing localhost connection:") + test_backend("http://localhost:5000") + + # Test internal IP + print("\n2. Testing internal IP (10.1.0.5):") + test_backend("http://10.1.0.5:5000") + + # Test public IP (if accessible) + print("\n3. Testing public IP (20.84.81.110):") + test_backend("http://20.84.81.110:5000") + + print("\n" + "=" * 60) + print("If localhost works but public IP doesn't, check:") + print("1. Firewall/security group allows port 5000") + print("2. Backend is running with host='0.0.0.0'") + print("3. Network configuration allows external access") + diff --git a/examples/recipe_creator/agents/recipe_agent.py b/examples/recipe_creator/agents/recipe_agent.py new file mode 100644 index 0000000..b66660a --- /dev/null +++ b/examples/recipe_creator/agents/recipe_agent.py @@ -0,0 +1,74 @@ +from agno.agent import Agent +from agno.models.openai import OpenAIChat +from agno.tools.exa import ExaTools +from textwrap import dedent + +recipe_agent = Agent( + name="ChefGenius", + tools=[ExaTools()], + model=OpenAIChat(id="gpt-4o"), + description=dedent("""\ + You are ChefGenius, a passionate culinary expert who creates personalized recipes! 🍳 + + You help users create delicious meals based on their ingredients, dietary needs, + and time constraints."""), + instructions=dedent("""\ + When creating recipes: + + 1. Analyze the user's ingredients and constraints + 2. Search for relevant recipes using Exa + 3. Provide a complete recipe with: + - Recipe title + - Prep & cook time + - Ingredients with measurements + - Step-by-step instructions + - Nutritional info (if available) + - Tips and variations + + Format your response clearly with: + - Use markdown formatting + - Add emojis: 🌱 Vegetarian, 🌿 Vegan, 🌾 Gluten-free, ⏱️ Quick + - Include substitution suggestions + - Note any allergen warnings"""), + markdown=True, +) + + +def create_recipe(ingredients: str, dietary_restrictions: str = "", time_limit: str = ""): + """ + Create a recipe based on ingredients and preferences + + Args: + ingredients: Available ingredients (e.g., "chicken, rice, broccoli") + dietary_restrictions: Any dietary needs (e.g., "vegetarian", "gluten-free") + time_limit: Maximum cooking time (e.g., "30 minutes", "1 hour") + """ + prompt = f"Create a recipe using these ingredients: {ingredients}." + + if dietary_restrictions: + prompt += f" Dietary restrictions: {dietary_restrictions}." + + if time_limit: + prompt += f" Must be ready in: {time_limit}." + + response = recipe_agent.run(prompt) + return { + "recipe": response.content, + "success": True + } + + +def create_recipe_stream(ingredients: str, dietary_restrictions: str = "", time_limit: str = ""): + """Streaming version of recipe creation""" + prompt = f"Create a recipe using these ingredients: {ingredients}." + + if dietary_restrictions: + prompt += f" Dietary restrictions: {dietary_restrictions}." + + if time_limit: + prompt += f" Must be ready in: {time_limit}." + + for chunk in recipe_agent.run(prompt, stream=True): + yield {"content": chunk if hasattr(chunk, 'content') else str(chunk)} + + \ No newline at end of file diff --git a/examples/recipe_creator/agents/requirements.txt b/examples/recipe_creator/agents/requirements.txt new file mode 100644 index 0000000..6ed9cd0 --- /dev/null +++ b/examples/recipe_creator/agents/requirements.txt @@ -0,0 +1,3 @@ +agno +openai +exa-py \ No newline at end of file diff --git a/examples/recipe_creator/agents/runagent.config.json b/examples/recipe_creator/agents/runagent.config.json new file mode 100644 index 0000000..68bb1a3 --- /dev/null +++ b/examples/recipe_creator/agents/runagent.config.json @@ -0,0 +1,27 @@ +{ + "agent_name": "Recipe Creator Agent", + "description": "AI-powered recipe creation assistant", + "framework": "agno", + "template": "custom", + "version": "1.0.0", + "created_at": "2025-10-27 00:00:00", + "template_source": { + "repo_url": "https://github.com/runagent-dev/runagent.git", + "path": "templates/agno/custom", + "author": "custom" + }, + "agent_architecture": { + "entrypoints": [ + { + "file": "recipe_agent.py", + "module": "create_recipe", + "tag": "recipe_create" + }, + { + "file": "recipe_agent.py", + "module": "create_recipe_stream", + "tag": "recipe_stream" + } + ] + } + } \ No newline at end of file diff --git a/examples/recipe_creator/backend/app.py b/examples/recipe_creator/backend/app.py new file mode 100644 index 0000000..9a8b600 --- /dev/null +++ b/examples/recipe_creator/backend/app.py @@ -0,0 +1,118 @@ +from flask import Flask, request, jsonify, Response +from flask_cors import CORS +from runagent import RunAgentClient +import os +import json + +app = Flask(__name__) +CORS(app) + +# Initialize RunAgent client +recipe_client = RunAgentClient( + agent_id="6f15f51e-0b6e-479a-ae8c-8174db2712fd", + entrypoint_tag="recipe_create", + local=False +) + +stream_client = RunAgentClient( + agent_id="6f15f51e-0b6e-479a-ae8c-8174db2712fd", + entrypoint_tag="recipe_stream", + local=False +) + + +@app.route('/health', methods=['GET']) +def health(): + """Health check endpoint""" + return jsonify({ + "status": "healthy", + "agent_id": "00efd47c-2c3d-492b-97ec-c6b79eeb93c4", + "mode": "local" + }) + + +@app.route('/api/recipe', methods=['POST']) +def create_recipe(): + """Create a recipe (non-streaming)""" + try: + data = request.json + ingredients = data.get('ingredients', '') + dietary_restrictions = data.get('dietary_restrictions', '') + time_limit = data.get('time_limit', '') + + if not ingredients: + return jsonify({"error": "Ingredients are required"}), 400 + + # Call the agent + result = recipe_client.run( + ingredients=ingredients, + dietary_restrictions=dietary_restrictions, + time_limit=time_limit + ) + + return jsonify(result) + + except Exception as e: + return jsonify({"error": str(e)}), 500 + + +@app.route('/api/recipe/stream', methods=['POST']) +def create_recipe_stream(): + """Create a recipe (streaming)""" + try: + data = request.json + ingredients = data.get('ingredients', '') + dietary_restrictions = data.get('dietary_restrictions', '') + time_limit = data.get('time_limit', '') + + if not ingredients: + return jsonify({"error": "Ingredients are required"}), 400 + + def generate(): + for chunk in stream_client.run( + ingredients=ingredients, + dietary_restrictions=dietary_restrictions, + time_limit=time_limit + ): + yield f"data: {json.dumps(chunk)}\n\n" + + return Response(generate(), mimetype='text/event-stream') + + except Exception as e: + return jsonify({"error": str(e)}), 500 + + +@app.route('/api/examples', methods=['GET']) +def get_examples(): + """Get example recipe queries""" + examples = [ + { + "name": "Quick Chicken Dinner", + "ingredients": "chicken breast, broccoli, rice, garlic", + "dietary_restrictions": "", + "time_limit": "30 minutes" + }, + { + "name": "Vegetarian Pasta", + "ingredients": "pasta, mushrooms, spinach, cream, parmesan", + "dietary_restrictions": "vegetarian", + "time_limit": "25 minutes" + }, + { + "name": "Healthy Breakfast", + "ingredients": "oats, banana, honey, almonds, milk", + "dietary_restrictions": "", + "time_limit": "15 minutes" + }, + { + "name": "Vegan Buddha Bowl", + "ingredients": "quinoa, chickpeas, sweet potato, kale, tahini", + "dietary_restrictions": "vegan", + "time_limit": "45 minutes" + } + ] + return jsonify(examples) + + +if __name__ == '__main__': + app.run(debug=True, port=5000) \ No newline at end of file diff --git a/examples/recipe_creator/frontend/README.md b/examples/recipe_creator/frontend/README.md new file mode 100644 index 0000000..3a7a7c6 --- /dev/null +++ b/examples/recipe_creator/frontend/README.md @@ -0,0 +1,28 @@ +# ChefGenius Frontend + +## Setup + +1. Install dependencies: +```bash +npm install +``` + +2. Run the development server: +```bash +npm run dev +``` + +This will start a live server on `http://localhost:3000` and automatically open your browser. + +## Features + +- 🎨 Beautiful gradient design +- ✨ Smooth animations +- 📱 Fully responsive +- 🔄 Real-time recipe streaming +- 🎯 Example recipes to try + +## Development + +The dev server will automatically reload when you make changes to HTML, CSS, or JavaScript files. + diff --git a/examples/recipe_creator/frontend/app.js b/examples/recipe_creator/frontend/app.js new file mode 100644 index 0000000..637ea74 --- /dev/null +++ b/examples/recipe_creator/frontend/app.js @@ -0,0 +1,230 @@ +// Configuration +const API_BASE_URL = 'http://localhost:5000'; + +// DOM Elements +const recipeForm = document.getElementById('recipeForm'); +const submitBtn = document.getElementById('submitBtn'); +const streamBtn = document.getElementById('streamBtn'); +const clearBtn = document.getElementById('clearBtn'); +const resultsSection = document.getElementById('resultsSection'); +const recipeContent = document.getElementById('recipeContent'); +const loadingIndicator = document.getElementById('loadingIndicator'); +const examplesContainer = document.getElementById('examplesContainer'); + +// Elements +const ingredientsInput = document.getElementById('ingredients'); +const dietaryInput = document.getElementById('dietary'); +const timeLimitInput = document.getElementById('timeLimit'); + +// Markdown to HTML converter (simple version) +function markdownToHtml(markdown) { + let html = markdown; + + // Headers + html = html.replace(/### (.*$)/gim, '

    $1

    '); + html = html.replace(/## (.*$)/gim, '

    $1

    '); + html = html.replace(/# (.*$)/gim, '

    $1

    '); + + // Bold + html = html.replace(/\*\*(.*?)\*\*/gim, '$1'); + + // Lists + html = html.replace(/^\* (.*$)/gim, '
  • $1
  • '); + html = html.replace(/^\d+\. (.*$)/gim, '
  • $1
  • '); + + // Wrap consecutive
  • in
      + html = html.replace(/(
    • .*<\/li>\n?)+/gim, '
        $&
      '); + + // Paragraphs + html = html.split('\n\n').map(para => { + if (!para.startsWith('<') && para.trim()) { + return `

      ${para}

      `; + } + return para; + }).join('\n'); + + return html; +} + +// Load examples +async function loadExamples() { + try { + const response = await fetch(`${API_BASE_URL}/api/examples`); + const examples = await response.json(); + + examplesContainer.innerHTML = examples.map(example => ` +
      +

      ${example.name}

      +

      Ingredients: ${example.ingredients.substring(0, 40)}...

      + ${example.dietary_restrictions ? `

      Diet: ${example.dietary_restrictions}

      ` : ''} + ${example.time_limit ? `

      Time: ${example.time_limit}

      ` : ''} +
      + `).join(''); + } catch (error) { + console.error('Failed to load examples:', error); + } +} + +// Fill form with example +window.fillExample = function(example) { + ingredientsInput.value = example.ingredients; + dietaryInput.value = example.dietary_restrictions; + timeLimitInput.value = example.time_limit; + + // Scroll to form + recipeForm.scrollIntoView({ behavior: 'smooth' }); +} + +// Show loading +function showLoading() { + resultsSection.style.display = 'block'; + loadingIndicator.style.display = 'block'; + recipeContent.style.display = 'none'; + submitBtn.disabled = true; + streamBtn.disabled = true; + + // Scroll to results + resultsSection.scrollIntoView({ behavior: 'smooth' }); +} + +// Hide loading +function hideLoading() { + loadingIndicator.style.display = 'none'; + recipeContent.style.display = 'block'; + submitBtn.disabled = false; + streamBtn.disabled = false; +} + +// Show error +function showError(message) { + recipeContent.innerHTML = ` +
      +

      ❌ Error

      +

      ${message}

      +
      + `; + hideLoading(); +} + +// Handle regular submission +recipeForm.addEventListener('submit', async (e) => { + e.preventDefault(); + + const data = { + ingredients: ingredientsInput.value.trim(), + dietary_restrictions: dietaryInput.value.trim(), + time_limit: timeLimitInput.value.trim() + }; + + showLoading(); + + try { + const response = await fetch(`${API_BASE_URL}/api/recipe`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(data) + }); + + const result = await response.json(); + + if (response.ok && result.success) { + recipeContent.innerHTML = markdownToHtml(result.recipe); + } else { + showError(result.error || 'Failed to create recipe'); + } + } catch (error) { + showError(`Network error: ${error.message}`); + } finally { + hideLoading(); + } +}); + +// Handle streaming submission +streamBtn.addEventListener('click', async (e) => { + e.preventDefault(); + + const data = { + ingredients: ingredientsInput.value.trim(), + dietary_restrictions: dietaryInput.value.trim(), + time_limit: timeLimitInput.value.trim() + }; + + if (!data.ingredients) { + alert('Please enter ingredients'); + return; + } + + showLoading(); + recipeContent.innerHTML = ''; + recipeContent.style.display = 'block'; + loadingIndicator.style.display = 'none'; + submitBtn.disabled = true; + streamBtn.disabled = true; + + try { + const response = await fetch(`${API_BASE_URL}/api/recipe/stream`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(data) + }); + + const reader = response.body.getReader(); + const decoder = new TextDecoder(); + let fullText = ''; + + while (true) { + const { value, done } = await reader.read(); + if (done) break; + + const chunk = decoder.decode(value); + const lines = chunk.split('\n'); + + for (const line of lines) { + if (line.startsWith('data: ')) { + const data = JSON.parse(line.substring(6)); + if (data.content) { + fullText += data.content; + recipeContent.innerHTML = markdownToHtml(fullText); + + // Auto-scroll to bottom + recipeContent.scrollTop = recipeContent.scrollHeight; + } + } + } + } + + } catch (error) { + showError(`Streaming error: ${error.message}`); + } finally { + submitBtn.disabled = false; + streamBtn.disabled = false; + } +}); + +// Clear results +clearBtn.addEventListener('click', () => { + resultsSection.style.display = 'none'; + recipeContent.innerHTML = ''; +}); + +// Check backend health on load +async function checkHealth() { + try { + const response = await fetch(`${API_BASE_URL}/health`); + const health = await response.json(); + console.log('Backend health:', health); + } catch (error) { + console.error('Backend not reachable:', error); + alert('Warning: Backend server is not running. Please start the Flask server.'); + } +} + +// Initialize +document.addEventListener('DOMContentLoaded', () => { + loadExamples(); + checkHealth(); +}); \ No newline at end of file diff --git a/examples/recipe_creator/frontend/index.html b/examples/recipe_creator/frontend/index.html new file mode 100644 index 0000000..46cb393 --- /dev/null +++ b/examples/recipe_creator/frontend/index.html @@ -0,0 +1,92 @@ + + + + + + ChefGenius - AI Recipe Creator + + + +
      + +
      +

      🍳 ChefGenius

      +

      Your AI-Powered Recipe Assistant

      +
      + + +
      + +
      +

      ✨ Try These Examples

      +
      + +
      +
      + + +
      +

      Create Your Recipe

      +
      +
      + + +
      + +
      + + +
      + +
      + + +
      + +
      + + +
      +
      +
      + + + +
      + + +
      +

      Powered by RunAgent & Agno AI

      +
      +
      + + + + \ No newline at end of file diff --git a/examples/recipe_creator/frontend/package-lock.json b/examples/recipe_creator/frontend/package-lock.json new file mode 100644 index 0000000..3dc0e22 --- /dev/null +++ b/examples/recipe_creator/frontend/package-lock.json @@ -0,0 +1,642 @@ +{ + "name": "chefgenius-frontend", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "chefgenius-frontend", + "version": "1.0.0", + "devDependencies": { + "http-server": "^14.1.1" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "dev": true, + "license": "MIT" + }, + "node_modules/basic-auth": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", + "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "5.1.2" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/corser": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/corser/-/corser-2.0.1.tgz", + "integrity": "sha512-utCYNzRSQIZNPIcGZdQc92UVJYAhtGAteCFg0yRaFm8f0P+CPtyGyHXJcGXnffjCybUCEx3FQ2G7U3/o9eIkVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "dev": true, + "license": "MIT" + }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true, + "license": "MIT", + "bin": { + "he": "bin/he" + } + }, + "node_modules/html-encoding-sniffer": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz", + "integrity": "sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-encoding": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/http-proxy": { + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", + "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eventemitter3": "^4.0.0", + "follow-redirects": "^1.0.0", + "requires-port": "^1.0.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/http-server": { + "version": "14.1.1", + "resolved": "https://registry.npmjs.org/http-server/-/http-server-14.1.1.tgz", + "integrity": "sha512-+cbxadF40UXd9T01zUHgA+rlo2Bg1Srer4+B4NwIHdaGxAGGv59nYRnGGDJ9LBk7alpS0US+J+bLLdQOOkJq4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "basic-auth": "^2.0.1", + "chalk": "^4.1.2", + "corser": "^2.0.1", + "he": "^1.2.0", + "html-encoding-sniffer": "^3.0.0", + "http-proxy": "^1.18.1", + "mime": "^1.6.0", + "minimist": "^1.2.6", + "opener": "^1.5.1", + "portfinder": "^1.0.28", + "secure-compare": "3.0.1", + "union": "~0.5.0", + "url-join": "^4.0.1" + }, + "bin": { + "http-server": "bin/http-server" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/opener": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/opener/-/opener-1.5.2.tgz", + "integrity": "sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==", + "dev": true, + "license": "(WTFPL OR MIT)", + "bin": { + "opener": "bin/opener-bin.js" + } + }, + "node_modules/portfinder": { + "version": "1.0.38", + "resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.38.tgz", + "integrity": "sha512-rEwq/ZHlJIKw++XtLAO8PPuOQA/zaPJOZJ37BVuN97nLpMJeuDVLVGRwbFoBgLudgdTMP2hdRJP++H+8QOA3vg==", + "dev": true, + "license": "MIT", + "dependencies": { + "async": "^3.2.6", + "debug": "^4.3.6" + }, + "engines": { + "node": ">= 10.12" + } + }, + "node_modules/portfinder/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/portfinder/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/secure-compare": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/secure-compare/-/secure-compare-3.0.1.tgz", + "integrity": "sha512-AckIIV90rPDcBcglUwXPF3kg0P0qmPsPXAj6BBEENQE1p5yA1xfmDJzfi1Tappj37Pv2mVbKpL3Z1T+Nn7k1Qw==", + "dev": true, + "license": "MIT" + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/union": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/union/-/union-0.5.0.tgz", + "integrity": "sha512-N6uOhuW6zO95P3Mel2I2zMsbsanvvtgn6jVqJv4vbVcz/JN0OkL9suomjQGmWtxJQXOCqUJvquc1sMeNz/IwlA==", + "dev": true, + "dependencies": { + "qs": "^6.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/url-join": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/url-join/-/url-join-4.0.1.tgz", + "integrity": "sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==", + "dev": true, + "license": "MIT" + }, + "node_modules/whatwg-encoding": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz", + "integrity": "sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=12" + } + } + } +} diff --git a/examples/recipe_creator/frontend/package.json b/examples/recipe_creator/frontend/package.json new file mode 100644 index 0000000..314e79f --- /dev/null +++ b/examples/recipe_creator/frontend/package.json @@ -0,0 +1,12 @@ +{ + "name": "chefgenius-frontend", + "version": "1.0.0", + "description": "AI-Powered Recipe Creator Frontend", + "scripts": { + "dev": "http-server -p 3000 -o" + }, + "devDependencies": { + "http-server": "^14.1.1" + } +} + diff --git a/examples/recipe_creator/frontend/style.css b/examples/recipe_creator/frontend/style.css new file mode 100644 index 0000000..b8324c8 --- /dev/null +++ b/examples/recipe_creator/frontend/style.css @@ -0,0 +1,443 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; + background: linear-gradient(135deg, #667eea 0%, #764ba2 50%, #f093fb 100%); + background-attachment: fixed; + min-height: 100vh; + padding: 20px; + position: relative; + overflow-x: hidden; +} + +body::before { + content: ''; + position: fixed; + top: -50%; + left: -50%; + width: 200%; + height: 200%; + background: radial-gradient(circle at 30% 20%, rgba(255,255,255,0.1) 0%, transparent 50%), + radial-gradient(circle at 70% 80%, rgba(255,255,255,0.1) 0%, transparent 50%); + animation: float 20s ease-in-out infinite; + pointer-events: none; + z-index: 0; +} + +.container { + max-width: 900px; + margin: 0 auto; + position: relative; + z-index: 1; +} + +/* Header */ +header { + text-align: center; + color: white; + margin-bottom: 40px; + animation: fadeInDown 0.8s ease; +} + +header h1 { + font-size: 3.5em; + margin-bottom: 10px; + text-shadow: 2px 2px 20px rgba(0,0,0,0.3); + background: linear-gradient(135deg, #fff 0%, #f0f0f0 100%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; + font-weight: 700; + letter-spacing: -1px; +} + +.tagline { + font-size: 1.2em; + opacity: 0.95; + text-shadow: 1px 1px 10px rgba(0,0,0,0.2); + font-weight: 300; +} + +/* Main Content */ +main { + background: rgba(255, 255, 255, 0.98); + backdrop-filter: blur(10px); + border-radius: 24px; + padding: 50px; + box-shadow: 0 25px 80px rgba(0,0,0,0.25), + 0 0 0 1px rgba(255,255,255,0.5); + animation: fadeInUp 0.8s ease; + border: 1px solid rgba(255, 255, 255, 0.2); +} + +/* Examples Section */ +.examples-section { + margin-bottom: 40px; +} + +.examples-section h2 { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; + margin-bottom: 24px; + font-size: 1.6em; + font-weight: 700; +} + +.examples-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: 15px; + margin-bottom: 30px; +} + +.example-card { + background: linear-gradient(135deg, #f8f9fa 0%, #ffffff 100%); + border: 2px solid #e9ecef; + border-radius: 16px; + padding: 20px; + cursor: pointer; + transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1); + position: relative; + overflow: hidden; +} + +.example-card::before { + content: ''; + position: absolute; + top: 0; + left: -100%; + width: 100%; + height: 100%; + background: linear-gradient(90deg, transparent, rgba(255,255,255,0.4), transparent); + transition: left 0.5s ease; +} + +.example-card:hover::before { + left: 100%; +} + +.example-card:hover { + border-color: #667eea; + background: #fff; + transform: translateY(-4px); + box-shadow: 0 6px 20px rgba(102, 126, 234, 0.25); +} + +.example-card h3 { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; + font-size: 1.05em; + margin-bottom: 10px; + font-weight: 600; + position: relative; + z-index: 1; +} + +.example-card p { + font-size: 0.9em; + color: #666; + margin: 6px 0; + position: relative; + z-index: 1; +} + +/* Form Section */ +.form-section h2 { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; + margin-bottom: 30px; + font-size: 1.6em; + font-weight: 700; +} + +.form-group { + margin-bottom: 20px; +} + +.form-group label { + display: block; + font-weight: 600; + color: #555; + margin-bottom: 8px; + font-size: 1em; +} + +.form-group input, +.form-group textarea { + width: 100%; + padding: 14px 16px; + border: 2px solid #e9ecef; + border-radius: 12px; + font-size: 1em; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + font-family: inherit; + background: white; +} + +.form-group input:focus, +.form-group textarea:focus { + outline: none; + border-color: #667eea; + box-shadow: 0 0 0 4px rgba(102, 126, 234, 0.1), + inset 0 0 0 1px #667eea; + transform: translateY(-1px); +} + +.form-group textarea { + resize: vertical; +} + +/* Buttons */ +.form-actions { + display: flex; + gap: 15px; + margin-top: 30px; +} + +.btn { + flex: 1; + padding: 15px 30px; + border: none; + border-radius: 8px; + font-size: 1em; + font-weight: 600; + cursor: pointer; + transition: all 0.3s ease; +} + +.btn-primary { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; + position: relative; + overflow: hidden; +} + +.btn-primary::before { + content: ''; + position: absolute; + top: 50%; + left: 50%; + width: 0; + height: 0; + border-radius: 50%; + background: rgba(255, 255, 255, 0.2); + transform: translate(-50%, -50%); + transition: width 0.6s, height 0.6s; +} + +.btn-primary:hover::before { + width: 300px; + height: 300px; +} + +.btn-primary:hover { + transform: translateY(-3px); + box-shadow: 0 8px 25px rgba(102, 126, 234, 0.5); +} + +.btn-secondary { + background: linear-gradient(135deg, #6c757d 0%, #5a6268 100%); + color: white; +} + +.btn-secondary:hover { + background: linear-gradient(135deg, #5a6268 0%, #495057 100%); + transform: translateY(-3px); + box-shadow: 0 8px 25px rgba(108, 117, 125, 0.4); +} + +.btn-clear { + background: #dc3545; + color: white; + padding: 8px 16px; + font-size: 0.9em; +} + +.btn-clear:hover { + background: #c82333; +} + +.btn:disabled { + opacity: 0.6; + cursor: not-allowed; + transform: none !important; +} + +/* Results Section */ +.results-section { + margin-top: 40px; + padding-top: 40px; + border-top: 2px solid #e9ecef; +} + +.results-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 20px; +} + +.results-header h2 { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; + font-size: 1.6em; + font-weight: 700; +} + +.recipe-content { + background: linear-gradient(135deg, #f8f9fa 0%, #ffffff 100%); + border: 1px solid #e9ecef; + border-radius: 16px; + padding: 30px; + line-height: 1.9; + color: #333; + animation: fadeIn 0.5s ease; + box-shadow: inset 0 2px 8px rgba(0,0,0,0.05); +} + +.recipe-content h3 { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; + margin-top: 24px; + margin-bottom: 12px; + font-weight: 700; +} + +.recipe-content h3:first-child { + margin-top: 0; +} + +.recipe-content ul, +.recipe-content ol { + margin-left: 25px; + margin-bottom: 15px; +} + +.recipe-content li { + margin-bottom: 8px; +} + +.recipe-content p { + margin-bottom: 15px; +} + +.recipe-content strong { + color: #555; +} + +/* Loading Indicator */ +.loading { + text-align: center; + padding: 40px; +} + +.spinner { + width: 60px; + height: 60px; + margin: 0 auto 20px; + border: 5px solid rgba(102, 126, 234, 0.1); + border-top: 5px solid #667eea; + border-right: 5px solid #764ba2; + border-radius: 50%; + animation: spin 1s linear infinite; +} + +@keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} + +.loading p { + color: #666; + font-size: 1.1em; +} + +/* Footer */ +footer { + text-align: center; + color: white; + margin-top: 30px; + padding: 20px; + opacity: 0.8; +} + +/* Animations */ +@keyframes fadeInDown { + from { + opacity: 0; + transform: translateY(-30px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +@keyframes fadeInUp { + from { + opacity: 0; + transform: translateY(30px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +@keyframes fadeIn { + from { + opacity: 0; + } + to { + opacity: 1; + } +} + +@keyframes float { + 0%, 100% { + transform: translateY(0) translateX(0); + } + 33% { + transform: translateY(-20px) translateX(10px); + } + 66% { + transform: translateY(20px) translateX(-10px); + } +} + +/* Responsive Design */ +@media (max-width: 768px) { + body { + padding: 10px; + } + + main { + padding: 25px; + } + + header h1 { + font-size: 2em; + } + + .form-actions { + flex-direction: column; + } + + .examples-grid { + grid-template-columns: 1fr; + } +} \ No newline at end of file diff --git a/examples/recipe_creator/sdk/python/test.py b/examples/recipe_creator/sdk/python/test.py new file mode 100644 index 0000000..f9ce06e --- /dev/null +++ b/examples/recipe_creator/sdk/python/test.py @@ -0,0 +1,156 @@ +""" +Simple test script for ChefGenius Recipe Agent using RunAgent Python SDK + +Usage: + python test_agent.py +""" + +from runagent import RunAgentClient +from dotenv import load_dotenv +import os + + + + +print("🧪 Testing ChefGenius Recipe Agent") +print("=" * 50) + +# Test 1: Non-streaming recipe creation +def test_non_streaming(): + print("\n1️⃣ Test: Non-Streaming Recipe Creation") + print("-" * 50) + + client = RunAgentClient( + agent_id="ea9267bf-ce87-4717-a896-923539b77dab", + entrypoint_tag="recipe_create", + local=False + ) + + result = client.run( + ingredients="chicken breast, broccoli, rice, garlic", + dietary_restrictions="", + time_limit="30 minutes" + ) + + print("✅ Result:") + if result.get("success"): + print(result["recipe"]) + else: + print("❌ Error:", result) + + return result + + +# Test 2: Streaming recipe creation +def test_streaming(): + print("\n2️⃣ Test: Streaming Recipe Creation") + print("-" * 50) + + client = RunAgentClient( + agent_id="ea9267bf-ce87-4717-a896-923539b77dab", + entrypoint_tag="recipe_stream", + local=False + ) + + print("✅ Streaming output:\n") + + for chunk in client.run( + ingredients="pasta, mushrooms, spinach, cream", + dietary_restrictions="vegetarian", + time_limit="25 minutes" + ): + print(chunk) + + print("\n\n✅ Stream complete!") + + +# Test 3: Quick test with minimal params +def test_quick(): + print("\n3️⃣ Test: Quick Recipe (minimal params)") + print("-" * 50) + + client = RunAgentClient( + agent_id=AGENT_ID, + entrypoint_tag="recipe_create", + local=LOCAL_MODE + ) + + result = client.run( + ingredients="eggs, cheese, tomatoes" + ) + + print("✅ Result:") + if result.get("success"): + print(result["recipe"][:500] + "...") # Show first 500 chars + else: + print("❌ Error:", result) + + +# Test 4: Vegan recipe +def test_vegan(): + print("\n4️⃣ Test: Vegan Recipe") + print("-" * 50) + + client = RunAgentClient( + agent_id=AGENT_ID, + entrypoint_tag="recipe_create", + local=LOCAL_MODE + ) + + result = client.run( + ingredients="quinoa, chickpeas, sweet potato, kale", + dietary_restrictions="vegan", + time_limit="40 minutes" + ) + + print("✅ Result:") + if result.get("success"): + print(result["recipe"][:500] + "...") + else: + print("❌ Error:", result) + + +# Main test runner +def run_all_tests(): + try: + # Validate agent ID + if AGENT_ID == "your-agent-id-here": + print("❌ ERROR: Please set AGENT_ID in .env file") + print(" Run 'runagent serve .' in the agent directory first") + return + + print(f"Agent ID: {AGENT_ID}") + print(f"Local Mode: {LOCAL_MODE}") + + # Run tests + test_non_streaming() + print("\n" + "=" * 50) + + test_streaming() + print("\n" + "=" * 50) + + test_quick() + print("\n" + "=" * 50) + + test_vegan() + print("\n" + "=" * 50) + + print("\n🎉 All tests completed!") + + except Exception as e: + print(f"\n❌ Test failed with error: {e}") + import traceback + traceback.print_exc() + + +if __name__ == "__main__": + # You can run individual tests or all tests + + # Run all tests + # run_all_tests() + + # Or run individual tests: + # test_non_streaming() + test_streaming() + # test_quick() + # test_vegan() \ No newline at end of file diff --git a/examples/screenplay_writer/README.md b/examples/screenplay_writer/README.md new file mode 100644 index 0000000..e479585 --- /dev/null +++ b/examples/screenplay_writer/README.md @@ -0,0 +1,141 @@ +# AI Crew for screenwriting +## Introduction +Example script to automatically write a screenplay from a newsgroup post using agents with [Crew.ai] (https://github.com/joaomdmoura/crewAI) . +CrewAI orchestrates autonomous AI agents, enabling them to collaborate and execute complex tasks efficiently. +You can also try it out with a personal email with many replies back and forth and see it turn into a movie script. +Demonstrates: +- multiple API endpoints (offical Mistral, Together.ai, Anyscale) +- running single tasks: spam detection and scoring +- running a crew to create a screenplay from a newsgroup post by first analyzing the text, creating a dialogue and ultimately formatting it + +By [Toon Beerten](toon@neontreebot.be) + +## Example output + +Input: + +``` +From: keith@cco.caltech.edu (Keith Allan Schneider) +Subject: Re: >I think that about 70% (or so) people approve of the +>>death penalty, even realizing all of its shortcomings. Doesn't this make +>>it reasonable? Or are *you* the sole judge of reasonability? +>Aside from revenge, what merits do you find in capital punishment? + +Are we talking about me, or the majority of the people that support it? +Anyway, I think that "revenge" or "fairness" is why most people are in +favor of the punishment. If a murderer is going to be punished, people +that think that he should "get what he deserves." Most people wouldn't +think it would be fair for the murderer to live, while his victim died. + +>Revenge? Petty and pathetic. + +Perhaps you think that it is petty and pathetic, but your views are in the +minority. + +>We have a local televised hot topic talk show that very recently +>did a segment on capital punishment. Each and every advocate of +>the use of this portion of our system of "jurisprudence" cited the +>main reason for supporting it: "That bastard deserved it". True +>human compassion, forgiveness, and sympathy. + +Where are we required to have compassion, forgiveness, and sympathy? If +someone wrongs me, I will take great lengths to make sure that his advantage +is removed, or a similar situation is forced upon him. If someone kills +another, then we can apply the golden rule and kill this person in turn. +Is not our entire moral system based on such a concept? + +Or, are you stating that human life is sacred, somehow, and that it should +never be violated? This would sound like some sort of religious view. + +>>I mean, how reasonable is imprisonment, really, when you think about it? +>>Sure, the person could be released if found innocent, but you still +>>can't undo the imiprisonment that was served. Perhaps we shouldn't +>>imprision people if we could watch them closely instead. The cost would +>>probably be similar, especially if we just implanted some sort of +>>electronic device. +>Would you rather be alive in prison or dead in the chair? + +Once a criminal has committed a murder, his desires are irrelevant. + +And, you still have not answered my question. If you are concerned about +the death penalty due to the possibility of the execution of an innocent, +then why isn't this same concern shared with imprisonment. Shouldn't we, +by your logic, administer as minimum as punishment as possible, to avoid +violating the liberty or happiness of an innocent person? + +keith +``` + +End result: + +``` +## Keith: + Robert, I don't understand. You're opposed to the death penalty because of some misplaced sense of compassion for criminals? + +## Robert: + No, Keith. It's about fairness and justice. We can't just take a life because someone has taken another. + +## Keith: +But what about the families of the victims? Don't they deserve justice? + +## Robert: +Of course they do, but the death penalty doesn't bring them back. It just perpetuates a cycle of violence. + +## Keith: + I don't see it that way. If someone takes an innocent life, then their own life should be forfeit. It's only fair. + +## Robert: + But what if we make a mistake? What if we execute an innocent person? + +## Keith: + That's a rare occurrence. And besides, we have a justice system in place to prevent that. + +## Robert: + And what if that system fails? What then, Keith? + +## Keith: + Well, we have to trust that it won't. + +## Robert: + And what about the cost-effectiveness of imprisonment versus the death penalty? + +## Keith: + That's a valid point, but the cost shouldn't be the only factor we consider. + +## Robert: + Agreed. But what about the potential violation of an innocent person's liberty or happiness by keeping a guilty one in prison for life? + +## Keith: + That's a complex issue. But I believe that the state has a responsibility to protect its citizens, even if it means depriving an individual of their freedom. + +## Robert: + And what about the possibility of using electronic surveillance devices to monitor prisoners and ensure their rehabilitation? + +## Keith: + I'll leave that to the experts. But I still believe in the death penalty as a means of justice and fairness. + +## Robert: + I respect your opinion, Keith. But I'll continue to advocate for a more compassionate and reasonable approach. +``` + +## Running the Script +Can be run in a new python env and installing crewai + +## Possible (non-local) endpoints +Easily select in the script which API endpoint to use: +- Official Mistral: benefit of having access to mistral-medium +- Together.ai: lots of models to choose from +- Anyscale: cheapest at the time of writing + +## Disclaimer +This is provided as is. The motivation is that i learn best from actual samples and i hope you do too. Please understand i can't give support on this. + +## License +MIT License. diff --git a/examples/screenplay_writer/agents/requirements.txt b/examples/screenplay_writer/agents/requirements.txt new file mode 100644 index 0000000..0585407 --- /dev/null +++ b/examples/screenplay_writer/agents/requirements.txt @@ -0,0 +1,5 @@ +crewai==1.2.0 +pyyaml +python-dotenv +openai + diff --git a/examples/screenplay_writer/agents/runagent.config.json b/examples/screenplay_writer/agents/runagent.config.json new file mode 100644 index 0000000..e88ff6e --- /dev/null +++ b/examples/screenplay_writer/agents/runagent.config.json @@ -0,0 +1,28 @@ +{ + "agent_name": "Screenplay Writer Crew", + "description": "CrewAI-based pipeline turning discussion text into a formatted screenplay and score.", + "framework": "crewai", + "template": "custom", + "version": "1.0.0", + "created_at": "2025-10-30 00:00:00", + "template_source": { + "repo_url": "https://github.com/runagent-dev/runagent.git", + "path": "templates/crewai/custom", + "author": "custom" + }, + "agent_architecture": { + "entrypoints": [ + { + "file": "screenplay_agent.py", + "module": "generate_screenplay", + "tag": "screenplay_generate" + }, + { + "file": "screenplay_agent.py", + "module": "generate_screenplay_stream", + "tag": "screenplay_stream" + } + ] + } +} + diff --git a/examples/screenplay_writer/agents/screenplay_agent.py b/examples/screenplay_writer/agents/screenplay_agent.py new file mode 100644 index 0000000..1a76d8a --- /dev/null +++ b/examples/screenplay_writer/agents/screenplay_agent.py @@ -0,0 +1,106 @@ +import re +from pathlib import Path +from typing import Dict, Generator, Any + +import yaml +from crewai import Agent, Task, Crew, Process +from dotenv import load_dotenv + + +def _load_configs(base_dir: Path) -> Dict[str, Any]: + load_dotenv() + agents_config_path = base_dir / "config" / "agents.yaml" + tasks_config_path = base_dir / "config" / "tasks.yaml" + + with open(agents_config_path, "r") as file: + agents_config = yaml.safe_load(file) + + with open(tasks_config_path, "r") as file: + tasks_config = yaml.safe_load(file) + + return {"agents": agents_config, "tasks": tasks_config} + + +def _build_agents(agents_cfg: Dict[str, Any]): + spamfilter = Agent(config=agents_cfg["spamfilter"], allow_delegation=False, verbose=True) + analyst = Agent(config=agents_cfg["analyst"], allow_delegation=False, verbose=True) + scriptwriter = Agent(config=agents_cfg["scriptwriter"], allow_delegation=False, verbose=True) + formatter = Agent(config=agents_cfg["formatter"], allow_delegation=False, verbose=True) + scorer = Agent(config=agents_cfg["scorer"], allow_delegation=False, verbose=True) + return spamfilter, analyst, scriptwriter, formatter, scorer + + +def generate_screenplay(discussion: str) -> Dict[str, Any]: + """ + Run the screenplay pipeline and return the formatted script and score. + """ + base_dir = Path(__file__).resolve().parents[1] + cfg = _load_configs(base_dir) + agents_cfg, tasks_cfg = cfg["agents"], cfg["tasks"] + + spamfilter, analyst, scriptwriter, formatter, scorer = _build_agents(agents_cfg) + + # Inject discussion into templated task descriptions + t0_desc = str(tasks_cfg["task0"]["description"]).replace("{{discussion}}", discussion) + t1_desc = str(tasks_cfg["task1"]["description"]).replace("{{discussion}}", discussion) + + task0 = Task( + description=t0_desc, + expected_output=tasks_cfg["task0"]["expected_output"], + agent=spamfilter, + ) + + result0 = task0.execute() + if isinstance(result0, str) and "STOP" in result0: + return {"filtered": True, "reason": "Spam or vulgar content detected", "success": True} + + task1 = Task( + description=t1_desc, + expected_output=tasks_cfg["task1"]["expected_output"], + agent=analyst, + ) + + task2 = Task( + description=tasks_cfg["task2"]["description"], + expected_output=tasks_cfg["task2"]["expected_output"], + agent=scriptwriter, + ) + + task3 = Task( + description=tasks_cfg["task3"]["description"], + expected_output=tasks_cfg["task3"]["expected_output"], + agent=formatter, + ) + + crew = Crew( + agents=[analyst, scriptwriter, formatter], + tasks=[task1, task2, task3], + verbose=2, + process=Process.sequential, + ) + + result = crew.kickoff() + + # Remove bracketed directions + cleaned = re.sub(r"\(.*?\)", "", result) + + # Score + task4 = Task( + description=str(tasks_cfg["task4"]["description"]).replace("{{script}}", cleaned), + expected_output=tasks_cfg["task4"]["expected_output"], + agent=scorer, + ) + + score_raw = task4.execute() + score_line = score_raw.split("\n")[0] if isinstance(score_raw, str) else str(score_raw) + + return {"script": cleaned, "score": score_line, "success": True} + + +def generate_screenplay_stream(discussion: str) -> Generator[Dict[str, Any], None, None]: + """Simple streaming: first yield script, then yield score.""" + result = generate_screenplay(discussion) + yield {"content": result.get("script", "")} + yield {"content": f"Score: {result.get('score', '')}"} + + diff --git a/examples/screenplay_writer/config/agents.yaml b/examples/screenplay_writer/config/agents.yaml new file mode 100644 index 0000000..7679235 --- /dev/null +++ b/examples/screenplay_writer/config/agents.yaml @@ -0,0 +1,55 @@ +spamfilter: + role: > + spamfilter + goal: > + Decide whether a text is spam or not. + backstory: > + You are an expert spam filter with years of experience. You DETEST advertisements, newsletters and vulgar language. + +analyst: + role: > + analyse + goal: > + You will distill all arguments from all discussion members. Identify who said what. You can reword what they said as long as the main discussion points remain. + backstory: > + You are an expert discussion analyst. + +scriptwriter: + role: > + scriptwriter + goal: > + Turn a conversation into a movie script. Only write the dialogue parts. Do not start the sentence with an action. Do not specify situational descriptions. Do not write parentheticals. + backstory: > + You are an expert on writing natural sounding movie script dialogues. You only focus on the text part and you HATE directional notes. + +formatter: + role: > + formatter + goal: > + Format the text as asked. Leave out actions from discussion members that happen between brackets, eg (smiling). + backstory: > + You are an expert text formatter. + +scorer: + role: > + scorer + goal: > + You score a dialogue assessing various aspects of the exchange between the participants using a 1-10 scale, where 1 is the lowest performance and 10 is the highest: + Scale: + 1-3: Poor - The dialogue has significant issues that prevent effective communication. + 4-6: Average - The dialogue has some good points but also has notable weaknesses. + 7-9: Good - The dialogue is mostly effective with minor issues. + 10: Excellent - The dialogue is exemplary in achieving its purpose with no apparent issues. + Factors to Consider: + Clarity: How clear is the exchange? Are the statements and responses easy to understand? + Relevance: Do the responses stay on topic and contribute to the conversation's purpose? + Conciseness: Is the dialogue free of unnecessary information or redundancy? + Politeness: Are the participants respectful and considerate in their interaction? + Engagement: Do the participants seem interested and actively involved in the dialogue? + Flow: Is there a natural progression of ideas and responses? Are there awkward pauses or interruptions? + Coherence: Does the dialogue make logical sense as a whole? + Responsiveness: Do the participants address each other's points adequately? + Language Use: Is the grammar, vocabulary, and syntax appropriate for the context of the dialogue? + Emotional Intelligence: Are the participants aware of and sensitive to the emotional tone of the dialogue? + backstory: > + You are an expert at scoring conversations on a scale of 1 to 10. You have a keen eye for detail and can identify the strengths and weaknesses of any dialogue. diff --git a/examples/screenplay_writer/config/tasks.yaml b/examples/screenplay_writer/config/tasks.yaml new file mode 100644 index 0000000..50babe8 --- /dev/null +++ b/examples/screenplay_writer/config/tasks.yaml @@ -0,0 +1,46 @@ +task0: + description: > + Read the following newsgroup post. If this contains vulgar language reply with STOP . If this is spam reply with STOP. + ### NEWGROUP POST: + {{discussion}} + expected_output: > + Either "STOP" if the post contains vulgar language or is spam, or no response if it does not. + +task1: + description: > + Analyse in much detail the following discussion: + ### DISCUSSION: + {{discussion}} + expected_output: > + A detailed analysis of the discussion, identifying who said what and rewording if necessary while maintaining the main discussion points. + +task2: + description: > + Create a dialogue heavy screenplay from the discussion, between two persons. Do NOT write parentheticals. Leave out wrylies. You MUST SKIP directional notes. + expected_output: > + A screenplay dialogue consisting only of the dialogue parts between two persons, without parentheticals, wrylies, or directional notes. + +task3: + description: > + Format the script exactly like this: + ## (person 1): + (first text line from person 1) + + ## (person 2): + (first text line from person 2) + + ## (person 1): + (second text line from person 1) + + ## (person 2): + (second text line from person 2) + expected_output: > + A formatted script with the specified structure, ensuring each line is formatted according to the provided template. + +task4: + description: > + Score the following script: + ### SCRIPT: + {{script}} + expected_output: > + A score from 1 to 10, indicating how well the script is. diff --git a/examples/screenplay_writer/screenplay_writer.py b/examples/screenplay_writer/screenplay_writer.py new file mode 100644 index 0000000..ee9c2df --- /dev/null +++ b/examples/screenplay_writer/screenplay_writer.py @@ -0,0 +1,152 @@ +import re +import yaml +from pathlib import Path +from crewai import Agent, Task, Crew, Process +from dotenv import load_dotenv + +load_dotenv() + +# Use Path for file locations +current_dir = Path.cwd() +agents_config_path = current_dir / "config" / "agents.yaml" +tasks_config_path = current_dir / "config" / "tasks.yaml" + +# Load YAML configuration files +with open(agents_config_path, "r") as file: + agents_config = yaml.safe_load(file) + +with open(tasks_config_path, "r") as file: + tasks_config = yaml.safe_load(file) + +## Define Agents +spamfilter = Agent( + config=agents_config["spamfilter"], allow_delegation=False, verbose=True +) + +analyst = Agent(config=agents_config["analyst"], allow_delegation=False, verbose=True) + +scriptwriter = Agent( + config=agents_config["scriptwriter"], allow_delegation=False, verbose=True +) + +formatter = Agent( + config=agents_config["formatter"], allow_delegation=False, verbose=True +) + + +scorer = Agent(config=agents_config["scorer"], allow_delegation=False, verbose=True) + + +# this is one example of a public post in the newsgroup alt.atheism +# try it out yourself by replacing this with your own email thread or text or ... +discussion = """From: keith@cco.caltech.edu (Keith Allan Schneider) +Subject: Re: >I think that about 70% (or so) people approve of the +>>death penalty, even realizing all of its shortcomings. Doesn't this make +>>it reasonable? Or are *you* the sole judge of reasonability? +>Aside from revenge, what merits do you find in capital punishment? + +Are we talking about me, or the majority of the people that support it? +Anyway, I think that "revenge" or "fairness" is why most people are in +favor of the punishment. If a murderer is going to be punished, people +that think that he should "get what he deserves." Most people wouldn't +think it would be fair for the murderer to live, while his victim died. + +>Revenge? Petty and pathetic. + +Perhaps you think that it is petty and pathetic, but your views are in the +minority. + +>We have a local televised hot topic talk show that very recently +>did a segment on capital punishment. Each and every advocate of +>the use of this portion of our system of "jurisprudence" cited the +>main reason for supporting it: "That bastard deserved it". True +>human compassion, forgiveness, and sympathy. + +Where are we required to have compassion, forgiveness, and sympathy? If +someone wrongs me, I will take great lengths to make sure that his advantage +is removed, or a similar situation is forced upon him. If someone kills +another, then we can apply the golden rule and kill this person in turn. +Is not our entire moral system based on such a concept? + +Or, are you stating that human life is sacred, somehow, and that it should +never be violated? This would sound like some sort of religious view. + +>>I mean, how reasonable is imprisonment, really, when you think about it? +>>Sure, the person could be released if found innocent, but you still +>>can't undo the imiprisonment that was served. Perhaps we shouldn't +>>imprision people if we could watch them closely instead. The cost would +>>probably be similar, especially if we just implanted some sort of +>>electronic device. +>Would you rather be alive in prison or dead in the chair? + +Once a criminal has committed a murder, his desires are irrelevant. + +And, you still have not answered my question. If you are concerned about +the death penalty due to the possibility of the execution of an innocent, +then why isn't this same concern shared with imprisonment. Shouldn't we, +by your logic, administer as minimum as punishment as possible, to avoid +violating the liberty or happiness of an innocent person? + +keith +""" + +# Filter out spam and vulgar posts +task0 = Task( + description=tasks_config["task0"]["description"], + expected_output=tasks_config["task0"]["expected_output"], + agent=spamfilter, +) +result = task0.execute() +if "STOP" in result: + # stop here and proceed to next post + print("This spam message will be filtered out") + +# process post with a crew of agents, ultimately delivering a well formatted dialogue +task1 = Task( + description=tasks_config["task1"]["description"], + expected_output=tasks_config["task1"]["expected_output"], + agent=analyst, +) + +task2 = Task( + description=tasks_config["task2"]["description"], + expected_output=tasks_config["task2"]["expected_output"], + agent=scriptwriter, +) + +task3 = Task( + description=tasks_config["task3"]["description"], + expected_output=tasks_config["task3"]["expected_output"], + agent=formatter, +) +crew = Crew( + agents=[analyst, scriptwriter, formatter], + tasks=[task1, task2, task3], + verbose=2, # Crew verbose more will let you know what tasks are being worked on, you can set it to 1 or 2 to different logging levels + process=Process.sequential, # Sequential process will have tasks executed one after the other and the outcome of the previous one is passed as extra content into this next. +) + +result = crew.kickoff() + +# get rid of directions and actions between brackets, eg: (smiling) +result = re.sub(r"\(.*?\)", "", result) + +print("===================== end result from crew ===================================") +print(result) +print("===================== score ==================================================") +task4 = Task( + description=tasks_config["task4"]["description"], + expected_output=tasks_config["task4"]["expected_output"], + agent=scorer, +) + +score = task4.execute() +score = score.split("\n")[0] # sometimes an explanation comes after score, ignore +print(f"Scoring the dialogue as: {score}/10") diff --git a/examples/screenplay_writer/sdk/python/test_sdk.py b/examples/screenplay_writer/sdk/python/test_sdk.py new file mode 100644 index 0000000..119dcc9 --- /dev/null +++ b/examples/screenplay_writer/sdk/python/test_sdk.py @@ -0,0 +1,22 @@ +from runagent import RunAgentClient + + +client = RunAgentClient( + agent_id="18aa1012-acc2-4416-9c7f-6242f874374a", + entrypoint_tag="screenplay_generate", + local=False +) + + +discussion = ( + "Two friends debate whether pineapple belongs on pizza while waiting at a coffee bar.\n" + "They trade points back and forth without insults or stage directions." +) + +result = client.run( + discussion=discussion +) + +print(result) + + diff --git a/examples/trip_planner/README.md b/examples/trip_planner/README.md new file mode 100644 index 0000000..f647fb1 --- /dev/null +++ b/examples/trip_planner/README.md @@ -0,0 +1,223 @@ +# TripGenius - AI Trip Planner SaaS + +A complete SaaS application for AI-powered trip planning using RunAgent, AG2, and FalkorDB GraphRAG. + +## Architecture + +``` +travel-planner-saas/ +├── agent/ # RunAgent-compatible trip planner agent +│ ├── trip_agent.py +│ ├── requirements.txt +│ └── runagent.config.json +├── backend/ # Flask REST API +│ ├── app.py +│ └── requirements.txt +└── frontend/ # Web UI + ├── index.html + ├── style.css + ├── app.js + └── package.json +``` + +## Prerequisites + +1. **Python 3.8+** +2. **Node.js 14+** (for frontend dev server) +3. **OpenAI API Key** - Get from https://platform.openai.com/api-keys +4. **Google Maps API Key** - Get from https://console.cloud.google.com/ (enable Directions API) +5. **RunAgent CLI** installed (`pip install runagent`) + +## Setup Instructions + +### Step 1: Setup and Deploy the Agent + +```bash +cd agent + +# Install dependencies +pip install -r requirements.txt + +# Set environment variables +export OPENAI_API_KEY="your-openai-api-key" +export GOOGLE_MAP_API_KEY="your-google-maps-api-key" + +# Deploy the agent locally with RunAgent +runagent serve . +``` + +**Important**: Copy the Agent ID from the output. It will look like: +``` +Agent ID: abc12345-6789-0def-1234-567890abcdef +``` + +### Step 2: Setup Backend + +Open a new terminal: + +```bash +cd backend + +# Install dependencies +pip install -r requirements.txt + +# Set environment variables with your Agent ID from Step 2 +export AGENT_ID="abc12345-6789-0def-1234-567890abcdef" # Replace with your actual Agent ID +export LOCAL_MODE="true" +export OPENAI_API_KEY="your-openai-api-key" + +# Start the Flask server +python app.py +``` + +The backend will run on `http://localhost:5000` + +### Step 4: Setup Frontend + +Open a new terminal: + +```bash +cd frontend + +# Install dependencies +npm install + +# Start the development server +npm run dev +``` + +The frontend will open automatically at `http://localhost:3000` + +## Usage + +1. Open `http://localhost:3000` in your browser +2. Browse example trips or create your own +3. Fill in: + - **Destination**: City name (e.g., "Rome") + - **Number of Days**: 1-14 days + - **Preferences**: What you're interested in (food, culture, history, etc.) +4. Click "Create Itinerary" or "Stream Itinerary" +5. View your AI-generated trip plan! + +## Features + +- ✅ AI-powered trip planning with AG2 multi-agent system +- ✅ **Works for ANY city worldwide** (powered by GPT-4o knowledge) +- ✅ **Google Maps integration for real travel times and distances** +- ✅ Structured itinerary output with daily events +- ✅ Beautiful, responsive web interface +- ✅ Streaming and non-streaming modes +- ✅ Example trip templates +- ✅ RunAgent deployment compatible +- ✅ No database required - fully dynamic! + +## API Endpoints + +### Backend API + +- `GET /health` - Health check +- `POST /api/trip` - Create trip itinerary (non-streaming) +- `POST /api/trip/stream` - Create trip itinerary (streaming) +- `GET /api/examples` - Get example trip queries +- `GET /api/destinations` - Get popular destinations + +### Request Format + +```json +{ + "destination": "Rome", + "num_days": 3, + "preferences": "historical sites, Italian cuisine" +} +``` + +### Response Format + +```json +{ + "success": true, + "itinerary": { + "days": [ + { + "events": [ + { + "type": "Attraction", + "location": "Colosseum", + "city": "Rome", + "description": "Ancient amphitheater..." + }, + { + "type": "Restaurant", + "location": "Trattoria da Enzo", + "city": "Rome", + "description": "Traditional Roman dishes..." + } + ] + } + ] + } +} +``` + +## Configuration + +### Agent Configuration (runagent.config.json) + +The agent exposes two entrypoints: +- `trip_create` - Non-streaming trip planning +- `trip_stream` - Streaming trip planning + +### Environment Variables + +**Agent:** +- `OPENAI_API_KEY` - OpenAI API key (required) +- `GOOGLE_MAP_API_KEY` - Google Maps API key (required for travel times) + +**Backend:** +- `AGENT_ID` - RunAgent agent ID (required) +- `LOCAL_MODE` - "true" for local, "false" for remote (default: "true") + +## Troubleshooting + +### Agent not found +- Ensure `runagent serve .` is running in the agent directory +- Copy the correct Agent ID to the backend + +### FalkorDB connection error +- Ensure Docker container is running on port 6379 +- Check `FALKORDB_HOST` and `FALKORDB_PORT` environment variables + +### Backend not connecting to agent +- Verify `AGENT_ID` in backend matches the agent ID from `runagent serve` +- Ensure `LOCAL_MODE=true` when running locally + +### Frontend not loading +- Check that backend is running on port 5000 +- Check browser console for CORS errors + +## Production Deployment + +For production deployment with RunAgent Cloud: + +1. Deploy agent to RunAgent Cloud: + ```bash + runagent deploy . + ``` + +2. Update backend environment: + ```bash + export AGENT_ID="your-cloud-agent-id" + export LOCAL_MODE="false" + ``` + +3. Deploy backend and frontend to your hosting platform + +## License + +Apache License 2.0 + +## Support + +For issues or questions, please refer to: +- RunAgent Documentation: https://docs.run-agent.ai +- AG2 Documentation: https://docs.ag2.ai \ No newline at end of file diff --git a/examples/trip_planner/agent/requirements.txt b/examples/trip_planner/agent/requirements.txt new file mode 100644 index 0000000..ea5b57c --- /dev/null +++ b/examples/trip_planner/agent/requirements.txt @@ -0,0 +1,4 @@ +ag2 +pydantic +requests +openai \ No newline at end of file diff --git a/examples/trip_planner/agent/runagent.config.json b/examples/trip_planner/agent/runagent.config.json new file mode 100644 index 0000000..54bc53e --- /dev/null +++ b/examples/trip_planner/agent/runagent.config.json @@ -0,0 +1,27 @@ +{ + "agent_name": "Trip Planner Agent", + "description": "AI-powered trip planning assistant with GraphRAG", + "framework": "ag2", + "template": "custom", + "version": "1.0.0", + "created_at": "2025-10-29 00:00:00", + "template_source": { + "repo_url": "https://github.com/runagent-dev/runagent.git", + "path": "templates/ag2/custom", + "author": "custom" + }, + "agent_architecture": { + "entrypoints": [ + { + "file": "trip_agent.py", + "module": "create_trip_planner", + "tag": "trip_create" + }, + { + "file": "trip_agent.py", + "module": "create_trip_planner_stream", + "tag": "trip_stream" + } + ] + } + } \ No newline at end of file diff --git a/examples/trip_planner/agent/trip_agent.py b/examples/trip_planner/agent/trip_agent.py new file mode 100644 index 0000000..9e8e8e8 --- /dev/null +++ b/examples/trip_planner/agent/trip_agent.py @@ -0,0 +1,458 @@ +""" +Trip Planner Agent - Compatible with AG2 >= 0.7.4 + +Changes from older versions: +- Uses AssistantAgent instead of SwarmAgent (recommended) +- Uses register_hand_off() standalone function +- Uses OnCondition and AfterWork (new names) +""" + +import os +import json +import requests +import copy +from typing import Any, Dict +from pydantic import BaseModel + +from autogen import AssistantAgent, UserProxyAgent +from autogen.agentchat.contrib.swarm_agent import ( + AfterWork, + OnCondition, + SwarmResult, + initiate_swarm_chat, + register_hand_off, +) + + +# Pydantic models for structured output +class Event(BaseModel): + type: str # Attraction, Restaurant, Travel + location: str + city: str + description: str + + +class Day(BaseModel): + events: list[Event] + + +class Itinerary(BaseModel): + days: list[Day] + + +# Initialize LLM Configuration +config_list = [ + { + "model": "gpt-4o", + "api_key": os.environ.get("OPENAI_API_KEY"), + } +] + +llm_config = {"config_list": config_list, "timeout": 120} + + +def _fetch_travel_time(origin: str, destination: str) -> dict: + """Retrieves route information using Google Maps Directions API.""" + endpoint = "https://maps.googleapis.com/maps/api/directions/json" + params = { + "origin": origin, + "destination": destination, + "mode": "walking", + "key": os.environ.get("GOOGLE_MAP_API_KEY"), + } + + try: + response = requests.get(endpoint, params=params) + if response.status_code == 200: + return response.json() + else: + return { + "error": "Failed to retrieve route information", + "status_code": response.status_code, + } + except Exception as e: + return {"error": str(e)} + + +def add_travel_times_to_itinerary(itinerary_data: dict) -> dict: + """Add travel times between events in the itinerary using Google Maps API.""" + if not isinstance(itinerary_data, dict) or "days" not in itinerary_data: + return itinerary_data + + google_api_key = os.environ.get("GOOGLE_MAP_API_KEY") + if not google_api_key: + print("Warning: GOOGLE_MAP_API_KEY not set. Skipping travel time calculations.") + return itinerary_data + + for day in itinerary_data["days"]: + if "events" not in day: + continue + + events = day["events"] + new_events = [] + + for i, event in enumerate(events): + new_events.append(event) + + # Add travel event between this and next event + if i < len(events) - 1: + current_event = event + next_event = events[i + 1] + + origin = f"{current_event['location']}, {current_event['city']}" + destination = f"{next_event['location']}, {next_event['city']}" + + maps_response = _fetch_travel_time(origin, destination) + + if "routes" in maps_response and len(maps_response["routes"]) > 0: + try: + leg = maps_response["routes"][0]["legs"][0] + travel_time = f"{leg['duration']['text']} ({leg['distance']['text']})" + + new_events.append({ + "type": "Travel", + "location": f"Walking from {current_event['location']} to {next_event['location']}", + "city": current_event["city"], + "description": travel_time + }) + except Exception as e: + print(f"Error adding travel time: {e}") + + day["events"] = new_events + + return itinerary_data + + +def create_trip_planner(destination: str, num_days: int, preferences: str): + """ + Create a trip itinerary based on user inputs using AI agents + + Args: + destination: City to visit (e.g., "Rome", "Paris", "Tokyo") + num_days: Number of days for the trip + preferences: User preferences (e.g., "historical sites, Italian food") + """ + + # Context for the swarm + trip_context = { + "itinerary_confirmed": False, + "itinerary": "", + "structured_itinerary": None, + "destination": destination, + "num_days": num_days, + "preferences": preferences, + } + + def mark_itinerary_as_complete( + final_itinerary: str, context_variables: Any + ) -> SwarmResult: + """Store and mark our itinerary as accepted.""" + # Normalize framework-provided context into a plain dict + try: + if not isinstance(context_variables, dict): + if hasattr(context_variables, "data"): + context_variables = dict(context_variables.data) + elif hasattr(context_variables, "dict"): + context_variables = dict(context_variables.dict()) + else: + context_variables = {} + except Exception: + context_variables = {} + + context_variables["itinerary_confirmed"] = True + context_variables["itinerary"] = final_itinerary + + return SwarmResult( + agent="structured_output_agent", + context_variables=context_variables, + values="Itinerary recorded and confirmed.", + ) + + def create_structured_itinerary( + context_variables: Any, structured_itinerary: Any + ) -> SwarmResult: + """Once a structured itinerary is created, store it.""" + # Normalize context to plain dict + try: + if not isinstance(context_variables, dict): + if hasattr(context_variables, "data"): + context_variables = dict(context_variables.data) + elif hasattr(context_variables, "dict"): + context_variables = dict(context_variables.dict()) + else: + context_variables = {} + except Exception: + context_variables = {} + + # Proceed even if not explicitly confirmed; planner may have failed to call the tool + # Normalize structured itinerary value if it's a pydantic model or JSON string + try: + if isinstance(structured_itinerary, BaseModel): + structured_itinerary = structured_itinerary.model_dump() + elif isinstance(structured_itinerary, str): + structured_itinerary = json.loads(structured_itinerary) + except Exception: + pass + + context_variables["structured_itinerary"] = structured_itinerary + + return SwarmResult( + context_variables=context_variables, + values="Structured itinerary stored and ready.", + ) + + # Create Planner Agent using AssistantAgent (recommended for AG2 0.7.4+) + planner_agent = AssistantAgent( + name="planner_agent", + system_message=f"""You are an expert trip planner with deep knowledge of destinations worldwide. + +Create a detailed {num_days}-day itinerary for {destination} based on these preferences: {preferences}. + +For each day, provide: +1. Morning attraction/activity +2. Lunch restaurant (local cuisine preferred) +3. Afternoon attraction/activity +4. Dinner restaurant + +Each event MUST have: +- type: 'Attraction' or 'Restaurant' +- location: Exact name of the place +- city: {destination} +- description: Brief description (1-2 sentences) + +Focus on: +- Popular and well-rated places +- Logical geographic clustering (places close together) +- Mix of famous landmarks and local experiences +- Variety in cuisine types +- Appropriate pacing (not too rushed) + +After creating the complete itinerary, mark it as complete. + +IMPORTANT: When the itinerary is complete, you MUST call the tool `mark_itinerary_as_complete` with the entire final itinerary as plain text in the `final_itinerary` argument. Do not just print the itinerary; always call the tool to persist it.""", + llm_config=llm_config, + ) + + # Register function for planner agent + planner_agent.register_for_llm( + name="mark_itinerary_as_complete", + description="Call this when the itinerary is complete and ready to be formatted." + )(mark_itinerary_as_complete) + + # Create Structured Output Agent with response format + structured_config_list = copy.deepcopy(config_list) + for config in structured_config_list: + config["response_format"] = Itinerary + + structured_output_agent = AssistantAgent( + name="structured_output_agent", + system_message=( + "You are a data formatting agent. Format the provided itinerary into the required structured JSON " + "format with days and events. When you have produced the structured itinerary, you MUST call the " + "tool `create_structured_itinerary` and pass the full JSON object via the `structured_itinerary` argument. " + "Do not only print the JSON; always call the tool to persist it." + ), + llm_config={"config_list": structured_config_list, "timeout": 120}, + ) + + # Register function for structured output agent + structured_output_agent.register_for_llm( + name="create_structured_itinerary", + description="Call this to store the structured itinerary." + )(create_structured_itinerary) + + # Register hand-offs using the new standalone function + register_hand_off( + agent=planner_agent, + hand_to=[ + OnCondition( + target=structured_output_agent, + condition="Itinerary is complete and ready to format", + ), + # Ensure structured agent gets a turn even if tool call fails + AfterWork(agent=structured_output_agent), + ] + ) + + register_hand_off( + agent=structured_output_agent, + hand_to=[AfterWork(agent="TERMINATE")] + ) + + # Create user proxy (auto-approves) + user_proxy = UserProxyAgent( + name="user_proxy", + code_execution_config=False, + human_input_mode="NEVER", + ) + + # Register functions for execution + user_proxy.register_for_execution(name="mark_itinerary_as_complete")(mark_itinerary_as_complete) + user_proxy.register_for_execution(name="create_structured_itinerary")(create_structured_itinerary) + + # Start the conversation + initial_message = f"Create a complete {num_days}-day trip itinerary for {destination}. Preferences: {preferences}. Include specific restaurant and attraction names with descriptions." + + chat_result, context_variables, last_agent = initiate_swarm_chat( + initial_agent=planner_agent, + agents=[planner_agent, structured_output_agent], + user_agent=user_proxy, + context_variables=trip_context, + messages=initial_message, + max_rounds=30, + ) + + # Extract the structured itinerary + if context_variables.get("structured_itinerary"): + itinerary = context_variables["structured_itinerary"] + + # Normalize structured itinerary into a dict + try: + if isinstance(itinerary, BaseModel): + itinerary = itinerary.model_dump() + elif isinstance(itinerary, str): + itinerary = json.loads(itinerary) + except Exception as e: + print(f"Warning: Could not normalize structured itinerary: {e}") + + # Add travel times using Google Maps API if available + if os.environ.get("GOOGLE_MAP_API_KEY") and isinstance(itinerary, dict): + try: + itinerary = add_travel_times_to_itinerary(itinerary) + except Exception as e: + print(f"Warning: Could not add travel times: {e}") + + return { + "success": True, + "itinerary": itinerary, + "message": "Trip itinerary created successfully!" + } + else: + # Fallback: try to recover JSON produced by structured agent or any text from chat + recovered_structured: Dict[str, Any] | None = None + fallback_text = context_variables.get("itinerary") + + def try_parse_json_from_text(text: str) -> Dict[str, Any] | None: + if not isinstance(text, str): + return None + s = text.strip() + # Heuristic: extract largest JSON object + try: + if s.startswith("{") and s.endswith("}"): + return json.loads(s) + first = s.find("{") + last = s.rfind("}") + if first != -1 and last != -1 and last > first: + return json.loads(s[first:last + 1]) + except Exception: + return None + return None + + # 1) Inspect chat_result common shapes + try: + # dict-like + if isinstance(chat_result, dict): + # messages array + msgs = chat_result.get("messages") + if isinstance(msgs, list) and msgs: + last_msg = msgs[-1] + content = last_msg.get("content") if isinstance(last_msg, dict) else None + recovered_structured = try_parse_json_from_text(content) + # top-level text fields + if recovered_structured is None: + for key in ["values", "content", "message", "text", "response"]: + val = chat_result.get(key) + recovered_structured = try_parse_json_from_text(val) + if recovered_structured is not None: + break + # object-like with .messages + if recovered_structured is None and hasattr(chat_result, "messages"): + msgs = getattr(chat_result, "messages", None) + if isinstance(msgs, list) and msgs: + last_msg = msgs[-1] + if isinstance(last_msg, dict): + recovered_structured = try_parse_json_from_text(last_msg.get("content")) + # string-like + if recovered_structured is None and isinstance(chat_result, str): + recovered_structured = try_parse_json_from_text(chat_result) + except Exception as e: + print(f"Warning: Could not recover itinerary from chat_result: {e}") + + # 2) Inspect last_agent chat buffer (autogen often stores messages on the agent) + if recovered_structured is None: + try: + if last_agent is not None: + # Common internal buffers in autogen + for attr in [ + "chat_messages", + "_oai_messages", + "_chat_messages", + "history", + ]: + buf = getattr(last_agent, attr, None) + if isinstance(buf, list) and buf: + last_msg = buf[-1] + if isinstance(last_msg, dict): + recovered_structured = try_parse_json_from_text(last_msg.get("content")) + if recovered_structured is not None: + break + elif isinstance(buf, dict): + # Some buffers are dict[role] -> list[messages] + for v in buf.values(): + if isinstance(v, list) and v: + last_msg = v[-1] + if isinstance(last_msg, dict): + recovered_structured = try_parse_json_from_text(last_msg.get("content")) + if recovered_structured is not None: + break + if recovered_structured is not None: + break + except Exception as e: + print(f"Warning: Could not recover itinerary from last_agent: {e}") + + # If we recovered a structured JSON, return it + if isinstance(recovered_structured, dict) and recovered_structured.get("days"): + return { + "success": True, + "itinerary": recovered_structured, + "message": "Trip itinerary created successfully!" + } + + # Otherwise, fallback to any text we can find from chat_result + if not fallback_text: + try: + if isinstance(chat_result, dict): + for key in ["values", "content", "message", "text", "response"]: + val = chat_result.get(key) + if isinstance(val, str) and val.strip(): + fallback_text = val + break + if not fallback_text and isinstance(chat_result.get("messages"), list): + msgs = chat_result["messages"] + if msgs: + last = msgs[-1] + if isinstance(last, dict) and isinstance(last.get("content"), str): + fallback_text = last["content"] + elif isinstance(chat_result, str): + fallback_text = chat_result + except Exception as e: + print(f"Warning: Could not derive text itinerary from chat_result: {e}") + + if not fallback_text: + fallback_text = "Unable to create itinerary" + + return { + "success": True, + "itinerary": fallback_text, + "message": "Trip itinerary created successfully!" + } + + +def create_trip_planner_stream(destination: str, num_days: int, preferences: str): + """Streaming version of trip planner""" + try: + result = create_trip_planner(destination, num_days, preferences) + # Simulate streaming by yielding the result + yield {"content": json.dumps(result, indent=2)} + except Exception as e: + yield {"content": json.dumps({"error": str(e)})} \ No newline at end of file diff --git a/examples/trip_planner/backend/app.py b/examples/trip_planner/backend/app.py new file mode 100644 index 0000000..f3b6359 --- /dev/null +++ b/examples/trip_planner/backend/app.py @@ -0,0 +1,178 @@ +from flask import Flask, request, jsonify, Response +from flask_cors import CORS +from runagent import RunAgentClient +import os +import json + +app = Flask(__name__) +# Explicit CORS to avoid preflight failures from different hosts/ports +CORS( + app, + resources={r"/*": {"origins": "*"}}, + supports_credentials=False, + allow_headers=["Content-Type", "Authorization"], + methods=["GET", "POST", "OPTIONS"] +) + +# Initialize RunAgent client - UPDATE THESE WITH YOUR ACTUAL IDs +LOCAL_MODE="false" +AGENT_ID="be1eef6e-2700-4980-b808-e94b3394e747" +# Initialize RunAgent clients +trip_client = RunAgentClient( + agent_id="be1eef6e-2700-4980-b808-e94b3394e747", + entrypoint_tag="trip_create", + local=False +) + +stream_client = RunAgentClient( + agent_id="be1eef6e-2700-4980-b808-e94b3394e747", + entrypoint_tag="trip_stream", + local=False +) + + +@app.route('/health', methods=['GET']) +def health(): + """Health check endpoint""" + try: + mode = "remote" + try: + mode = "local" if getattr(trip_client, 'local', False) else "remote" + except Exception: + pass + return jsonify({ + "status": "healthy", + "agent_id": AGENT_ID, + "mode": mode + }) + except Exception as e: + return jsonify({"status": "error", "error": str(e)}), 500 + + +@app.route('/api/trip', methods=['POST']) +def create_trip(): + """Create a trip itinerary (non-streaming)""" + try: + data = request.json + destination = data.get('destination', '') + num_days = data.get('num_days', 2) + preferences = data.get('preferences', '') + + if not destination: + return jsonify({"error": "Destination is required"}), 400 + + # Call the agent + result = trip_client.run( + destination=destination, + num_days=num_days, + preferences=preferences + ) + + return jsonify(result) + + except Exception as e: + return jsonify({"error": str(e)}), 500 + + +@app.route('/api/trip/stream', methods=['POST']) +def create_trip_stream(): + """Create a trip itinerary (streaming)""" + try: + data = request.json + destination = data.get('destination', '') + num_days = data.get('num_days', 2) + preferences = data.get('preferences', '') + + if not destination: + return jsonify({"error": "Destination is required"}), 400 + + def generate(): + for chunk in stream_client.run( + destination=destination, + num_days=num_days, + preferences=preferences + ): + yield f"data: {json.dumps(chunk)}\n\n" + + return Response(generate(), mimetype='text/event-stream') + + except Exception as e: + return jsonify({"error": str(e)}), 500 + + +@app.route('/api/destinations', methods=['GET']) +def get_destinations(): + """Get example destinations""" + destinations = [ + { + "city": "Rome", + "country": "Italy", + "description": "Ancient city with historic landmarks", + "popular_for": "History, Art, Food" + }, + { + "city": "Paris", + "country": "France", + "description": "City of lights and romance", + "popular_for": "Art, Culture, Cuisine" + }, + { + "city": "Tokyo", + "country": "Japan", + "description": "Modern metropolis meets tradition", + "popular_for": "Technology, Culture, Food" + }, + { + "city": "Barcelona", + "country": "Spain", + "description": "Mediterranean beauty and Gaudi architecture", + "popular_for": "Architecture, Beaches, Culture" + } + ] + return jsonify(destinations) + + +@app.route('/api/examples', methods=['GET']) +def get_examples(): + """Get example trip queries""" + examples = [ + { + "name": "Rome Weekend Getaway", + "destination": "Rome", + "num_days": 2, + "preferences": "historical sites, Italian cuisine, must-see landmarks" + }, + { + "name": "Rome Art & Culture", + "destination": "Rome", + "num_days": 3, + "preferences": "museums, art galleries, Vatican, local restaurants" + }, + { + "name": "Rome Food Tour", + "destination": "Rome", + "num_days": 2, + "preferences": "traditional Roman food, pasta, local trattorias" + }, + { + "name": "Rome Historic Journey", + "destination": "Rome", + "num_days": 4, + "preferences": "ancient ruins, Colosseum, Roman Forum, historical churches" + } + ] + return jsonify(examples) + + +if __name__ == '__main__': + # Check if agent ID is set + if AGENT_ID == "be1eef6e-2700-4980-b808-e94b3394e747": + print("⚠️ WARNING: Please set AGENT_ID environment variable") + print(" Run 'runagent serve .' in the agent directory first") + print(" Then set AGENT_ID to the ID shown in the output\n") + + print(f"🚀 Starting Trip Planner Backend") + print(f" Agent ID: {AGENT_ID}") + print(f" Server: http://localhost:5000\n") + + app.run(debug=True, host='0.0.0.0', port=5000) \ No newline at end of file diff --git a/examples/trip_planner/frontend/app.js b/examples/trip_planner/frontend/app.js new file mode 100644 index 0000000..db3bf60 --- /dev/null +++ b/examples/trip_planner/frontend/app.js @@ -0,0 +1,262 @@ +// Configuration: hardcode backend URL (adjust if needed) +const API_BASE_URL = 'http://20.84.81.110:5000'; + +// DOM Elements +const tripForm = document.getElementById('tripForm'); +const submitBtn = document.getElementById('submitBtn'); +const streamBtn = document.getElementById('streamBtn'); +const clearBtn = document.getElementById('clearBtn'); +const resultsSection = document.getElementById('resultsSection'); +const itineraryContent = document.getElementById('itineraryContent'); +const loadingIndicator = document.getElementById('loadingIndicator'); +const examplesContainer = document.getElementById('examplesContainer'); + +// Form inputs +const destinationInput = document.getElementById('destination'); +const numDaysInput = document.getElementById('numDays'); +const preferencesInput = document.getElementById('preferences'); + +// Load examples +async function loadExamples() { + try { + const response = await fetch(`${API_BASE_URL}/api/examples`); + const examples = await response.json(); + + examplesContainer.innerHTML = examples.map(example => ` +
      +

      ${example.name}

      +

      📍 Destination: ${example.destination}

      +

      📅 Duration: ${example.num_days} days

      +

      💡 Focus: ${example.preferences.substring(0, 50)}...

      +
      + `).join(''); + } catch (error) { + console.error('Failed to load examples:', error); + } +} + +// Fill form with example +window.fillExample = function(example) { + destinationInput.value = example.destination; + numDaysInput.value = example.num_days; + preferencesInput.value = example.preferences; + + // Scroll to form + tripForm.scrollIntoView({ behavior: 'smooth' }); +} + +// Show loading +function showLoading() { + resultsSection.style.display = 'block'; + loadingIndicator.style.display = 'block'; + itineraryContent.style.display = 'none'; + submitBtn.disabled = true; + streamBtn.disabled = true; + + // Scroll to results + resultsSection.scrollIntoView({ behavior: 'smooth' }); +} + +// Hide loading +function hideLoading() { + loadingIndicator.style.display = 'none'; + itineraryContent.style.display = 'block'; + submitBtn.disabled = false; + streamBtn.disabled = false; +} + +// Show error +function showError(message) { + itineraryContent.innerHTML = ` +
      +

      ❌ Error

      +

      ${message}

      +
      + `; + hideLoading(); +} + +// Format itinerary data +function formatItinerary(data) { + if (typeof data === 'string') { + try { + data = JSON.parse(data); + } catch (e) { + return `
      ${data}
      `; + } + } + + // Check if it's the success response format + if (data.success && data.itinerary) { + data = data.itinerary; + } + + // Check if it's structured itinerary + if (data.days && Array.isArray(data.days)) { + let html = ''; + + data.days.forEach((day, index) => { + html += ` +
      +
      Day ${index + 1}
      +
      + `; + + if (day.events && Array.isArray(day.events)) { + day.events.forEach(event => { + const icon = event.type === 'Restaurant' ? '🍽️' : + event.type === 'Travel' ? '🚶' : '🏛️'; + const citySuffix = event.city ? `, ${event.city}` : ''; + const mapsQuery = encodeURIComponent(`${event.location}${citySuffix}`); + const mapsUrl = `https://www.google.com/maps/search/?api=1&query=${mapsQuery}`; + + html += ` +
      +
      +
      ${icon} ${event.type}
      + Open in Maps ↗ +
      +
      ${event.location}${citySuffix}
      +
      ${event.description}
      +
      + `; + }); + } + + html += ` +
      +
      + `; + }); + + return html; + } + + // Fallback to JSON display + return `
      ${JSON.stringify(data, null, 2)}
      `; +} + +// Handle regular submission +tripForm.addEventListener('submit', async (e) => { + e.preventDefault(); + + const data = { + destination: destinationInput.value.trim(), + num_days: parseInt(numDaysInput.value), + preferences: preferencesInput.value.trim(), + remote: true, + agent_id: 'be1eef6e-2700-4980-b808-e94b3394e747' + }; + + if (!data.destination) { + alert('Please enter a destination'); + return; + } + + showLoading(); + + try { + const response = await fetch(`${API_BASE_URL}/api/trip`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(data) + }); + + const result = await response.json(); + + if (response.ok) { + itineraryContent.innerHTML = formatItinerary(result); + } else { + showError(result.error || 'Failed to create itinerary'); + } + } catch (error) { + showError(`Network error: ${error.message}`); + } finally { + hideLoading(); + } +}); + +// Handle streaming submission +streamBtn.addEventListener('click', async (e) => { + e.preventDefault(); + + const data = { + destination: destinationInput.value.trim(), + num_days: parseInt(numDaysInput.value), + preferences: preferencesInput.value.trim(), + remote: true, + agent_id: 'be1eef6e-2700-4980-b808-e94b3394e747' + }; + + if (!data.destination) { + alert('Please enter a destination'); + return; + } + + showLoading(); + itineraryContent.innerHTML = ''; + itineraryContent.style.display = 'block'; + loadingIndicator.style.display = 'none'; + submitBtn.disabled = true; + streamBtn.disabled = true; + + try { + const response = await fetch(`${API_BASE_URL}/api/trip/stream`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(data) + }); + + const reader = response.body.getReader(); + const decoder = new TextDecoder(); + let accumulatedData = ''; + + while (true) { + const { value, done } = await reader.read(); + if (done) break; + + const chunk = decoder.decode(value); + const lines = chunk.split('\n'); + + for (const line of lines) { + if (line.startsWith('data: ')) { + try { + const data = JSON.parse(line.substring(6)); + if (data.content) { + accumulatedData += data.content; + itineraryContent.innerHTML = formatItinerary(accumulatedData); + + // Auto-scroll to bottom + itineraryContent.scrollTop = itineraryContent.scrollHeight; + } + } catch (e) { + console.error('Error parsing stream data:', e); + } + } + } + } + + } catch (error) { + showError(`Streaming error: ${error.message}`); + } finally { + submitBtn.disabled = false; + streamBtn.disabled = false; + } +}); + +// Clear results +clearBtn.addEventListener('click', () => { + resultsSection.style.display = 'none'; + itineraryContent.innerHTML = ''; +}); + +// Removed health check to avoid blocking UI if backend URL differs + +// Initialize +document.addEventListener('DOMContentLoaded', () => { + loadExamples(); +}); \ No newline at end of file diff --git a/examples/trip_planner/frontend/index.html b/examples/trip_planner/frontend/index.html new file mode 100644 index 0000000..eddd4c4 --- /dev/null +++ b/examples/trip_planner/frontend/index.html @@ -0,0 +1,97 @@ + + + + + + TripGenius - AI Trip Planner + + + +
      + +
      +

      ✈️ TripGenius

      +

      Your AI-Powered Trip Planning Assistant

      +
      + + +
      + +
      +

      ✨ Popular Trip Ideas

      +
      + +
      +
      + + +
      +

      Plan Your Trip

      +
      +
      + + +
      + +
      + + +
      + +
      + + + Tell us what you're interested in (food, culture, history, adventure, etc.) +
      + +
      + + +
      +
      +
      + + + +
      + + +
      +

      Powered by RunAgent & AG2 AI

      +
      +
      + + + + \ No newline at end of file diff --git a/examples/trip_planner/frontend/package-lock.json b/examples/trip_planner/frontend/package-lock.json new file mode 100644 index 0000000..fa26495 --- /dev/null +++ b/examples/trip_planner/frontend/package-lock.json @@ -0,0 +1,642 @@ +{ + "name": "tripgenius-frontend", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "tripgenius-frontend", + "version": "1.0.0", + "devDependencies": { + "http-server": "^14.1.1" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "dev": true, + "license": "MIT" + }, + "node_modules/basic-auth": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", + "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "5.1.2" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/corser": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/corser/-/corser-2.0.1.tgz", + "integrity": "sha512-utCYNzRSQIZNPIcGZdQc92UVJYAhtGAteCFg0yRaFm8f0P+CPtyGyHXJcGXnffjCybUCEx3FQ2G7U3/o9eIkVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "dev": true, + "license": "MIT" + }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true, + "license": "MIT", + "bin": { + "he": "bin/he" + } + }, + "node_modules/html-encoding-sniffer": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz", + "integrity": "sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-encoding": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/http-proxy": { + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", + "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eventemitter3": "^4.0.0", + "follow-redirects": "^1.0.0", + "requires-port": "^1.0.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/http-server": { + "version": "14.1.1", + "resolved": "https://registry.npmjs.org/http-server/-/http-server-14.1.1.tgz", + "integrity": "sha512-+cbxadF40UXd9T01zUHgA+rlo2Bg1Srer4+B4NwIHdaGxAGGv59nYRnGGDJ9LBk7alpS0US+J+bLLdQOOkJq4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "basic-auth": "^2.0.1", + "chalk": "^4.1.2", + "corser": "^2.0.1", + "he": "^1.2.0", + "html-encoding-sniffer": "^3.0.0", + "http-proxy": "^1.18.1", + "mime": "^1.6.0", + "minimist": "^1.2.6", + "opener": "^1.5.1", + "portfinder": "^1.0.28", + "secure-compare": "3.0.1", + "union": "~0.5.0", + "url-join": "^4.0.1" + }, + "bin": { + "http-server": "bin/http-server" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/opener": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/opener/-/opener-1.5.2.tgz", + "integrity": "sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==", + "dev": true, + "license": "(WTFPL OR MIT)", + "bin": { + "opener": "bin/opener-bin.js" + } + }, + "node_modules/portfinder": { + "version": "1.0.38", + "resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.38.tgz", + "integrity": "sha512-rEwq/ZHlJIKw++XtLAO8PPuOQA/zaPJOZJ37BVuN97nLpMJeuDVLVGRwbFoBgLudgdTMP2hdRJP++H+8QOA3vg==", + "dev": true, + "license": "MIT", + "dependencies": { + "async": "^3.2.6", + "debug": "^4.3.6" + }, + "engines": { + "node": ">= 10.12" + } + }, + "node_modules/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/secure-compare": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/secure-compare/-/secure-compare-3.0.1.tgz", + "integrity": "sha512-AckIIV90rPDcBcglUwXPF3kg0P0qmPsPXAj6BBEENQE1p5yA1xfmDJzfi1Tappj37Pv2mVbKpL3Z1T+Nn7k1Qw==", + "dev": true, + "license": "MIT" + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/union": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/union/-/union-0.5.0.tgz", + "integrity": "sha512-N6uOhuW6zO95P3Mel2I2zMsbsanvvtgn6jVqJv4vbVcz/JN0OkL9suomjQGmWtxJQXOCqUJvquc1sMeNz/IwlA==", + "dev": true, + "dependencies": { + "qs": "^6.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/url-join": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/url-join/-/url-join-4.0.1.tgz", + "integrity": "sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==", + "dev": true, + "license": "MIT" + }, + "node_modules/whatwg-encoding": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz", + "integrity": "sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=12" + } + } + } +} diff --git a/examples/trip_planner/frontend/package.json b/examples/trip_planner/frontend/package.json new file mode 100644 index 0000000..13df131 --- /dev/null +++ b/examples/trip_planner/frontend/package.json @@ -0,0 +1,11 @@ +{ + "name": "tripgenius-frontend", + "version": "1.0.0", + "description": "AI-Powered Trip Planner Frontend", + "scripts": { + "dev": "http-server -p 3001 -o" + }, + "devDependencies": { + "http-server": "^14.1.1" + } + } \ No newline at end of file diff --git a/examples/trip_planner/frontend/style.css b/examples/trip_planner/frontend/style.css new file mode 100644 index 0000000..e57f11f --- /dev/null +++ b/examples/trip_planner/frontend/style.css @@ -0,0 +1,495 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; + background: linear-gradient(135deg, #4facfe 0%, #00f2fe 50%, #43e97b 100%); + background-attachment: fixed; + min-height: 100vh; + padding: 20px; + position: relative; + overflow-x: hidden; +} + +body::before { + content: ''; + position: fixed; + top: -50%; + left: -50%; + width: 200%; + height: 200%; + background: radial-gradient(circle at 30% 20%, rgba(255,255,255,0.15) 0%, transparent 50%), + radial-gradient(circle at 70% 80%, rgba(255,255,255,0.15) 0%, transparent 50%); + animation: float 20s ease-in-out infinite; + pointer-events: none; + z-index: 0; +} + +.container { + max-width: 1000px; + margin: 0 auto; + position: relative; + z-index: 1; +} + +/* Header */ +header { + text-align: center; + color: white; + margin-bottom: 40px; + animation: fadeInDown 0.8s ease; +} + +header h1 { + font-size: 3.5em; + margin-bottom: 10px; + text-shadow: 2px 2px 20px rgba(0,0,0,0.3); + background: linear-gradient(135deg, #fff 0%, #f0f0f0 100%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; + font-weight: 700; + letter-spacing: -1px; +} + +.tagline { + font-size: 1.2em; + opacity: 0.95; + text-shadow: 1px 1px 10px rgba(0,0,0,0.2); + font-weight: 300; +} + +/* Main Content */ +main { + background: rgba(255, 255, 255, 0.98); + backdrop-filter: blur(10px); + border-radius: 24px; + padding: 50px; + box-shadow: 0 25px 80px rgba(0,0,0,0.25), + 0 0 0 1px rgba(255,255,255,0.5); + animation: fadeInUp 0.8s ease; + border: 1px solid rgba(255, 255, 255, 0.2); +} + +/* Examples Section */ +.examples-section { + margin-bottom: 40px; +} + +.examples-section h2 { + background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; + margin-bottom: 24px; + font-size: 1.6em; + font-weight: 700; +} + +.examples-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); + gap: 15px; + margin-bottom: 30px; +} + +.example-card { + background: linear-gradient(135deg, #f8f9fa 0%, #ffffff 100%); + border: 2px solid #e9ecef; + border-radius: 16px; + padding: 20px; + cursor: pointer; + transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1); + position: relative; + overflow: hidden; +} + +.example-card::before { + content: ''; + position: absolute; + top: 0; + left: -100%; + width: 100%; + height: 100%; + background: linear-gradient(90deg, transparent, rgba(255,255,255,0.4), transparent); + transition: left 0.5s ease; +} + +.example-card:hover::before { + left: 100%; +} + +.example-card:hover { + border-color: #4facfe; + background: #fff; + transform: translateY(-4px); + box-shadow: 0 6px 20px rgba(79, 172, 254, 0.25); +} + +.example-card h3 { + background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; + font-size: 1.05em; + margin-bottom: 10px; + font-weight: 600; + position: relative; + z-index: 1; +} + +.example-card p { + font-size: 0.9em; + color: #666; + margin: 6px 0; + position: relative; + z-index: 1; +} + +.example-card .days { + font-weight: 600; + color: #4facfe; +} + +/* Form Section */ +.form-section h2 { + background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; + margin-bottom: 30px; + font-size: 1.6em; + font-weight: 700; +} + +.form-group { + margin-bottom: 20px; +} + +.form-group label { + display: block; + font-weight: 600; + color: #555; + margin-bottom: 8px; + font-size: 1em; +} + +.form-group input, +.form-group textarea { + width: 100%; + padding: 14px 16px; + border: 2px solid #e9ecef; + border-radius: 12px; + font-size: 1em; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + font-family: inherit; + background: white; +} + +.form-group input:focus, +.form-group textarea:focus { + outline: none; + border-color: #4facfe; + box-shadow: 0 0 0 4px rgba(79, 172, 254, 0.1), + inset 0 0 0 1px #4facfe; + transform: translateY(-1px); +} + +.form-group textarea { + resize: vertical; +} + +.form-group small { + display: block; + color: #999; + font-size: 0.85em; + margin-top: 5px; +} + +/* Buttons */ +.form-actions { + display: flex; + gap: 15px; + margin-top: 30px; +} + +.btn { + flex: 1; + padding: 15px 30px; + border: none; + border-radius: 8px; + font-size: 1em; + font-weight: 600; + cursor: pointer; + transition: all 0.3s ease; +} + +.btn-primary { + background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%); + color: white; + position: relative; + overflow: hidden; +} + +.btn-primary::before { + content: ''; + position: absolute; + top: 50%; + left: 50%; + width: 0; + height: 0; + border-radius: 50%; + background: rgba(255, 255, 255, 0.2); + transform: translate(-50%, -50%); + transition: width 0.6s, height 0.6s; +} + +.btn-primary:hover::before { + width: 300px; + height: 300px; +} + +.btn-primary:hover { + transform: translateY(-3px); + box-shadow: 0 8px 25px rgba(79, 172, 254, 0.5); +} + +.btn-secondary { + background: linear-gradient(135deg, #6c757d 0%, #5a6268 100%); + color: white; +} + +.btn-secondary:hover { + background: linear-gradient(135deg, #5a6268 0%, #495057 100%); + transform: translateY(-3px); + box-shadow: 0 8px 25px rgba(108, 117, 125, 0.4); +} + +.btn-clear { + background: #dc3545; + color: white; + padding: 8px 16px; + font-size: 0.9em; +} + +.btn-clear:hover { + background: #c82333; +} + +.btn:disabled { + opacity: 0.6; + cursor: not-allowed; + transform: none !important; +} + +/* Results Section */ +.results-section { + margin-top: 40px; + padding-top: 40px; + border-top: 2px solid #e9ecef; +} + +.results-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 20px; +} + +.results-header h2 { + background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; + font-size: 1.6em; + font-weight: 700; +} + +.itinerary-content { + background: linear-gradient(135deg, #f8f9fa 0%, #ffffff 100%); + border: 1px solid #e9ecef; + border-radius: 16px; + padding: 30px; + line-height: 1.9; + color: #333; + animation: fadeIn 0.5s ease; + box-shadow: inset 0 2px 8px rgba(0,0,0,0.05); +} + +.itinerary-content .day { + margin-bottom: 30px; + padding-bottom: 30px; + border-bottom: 2px solid #e9ecef; +} + +.itinerary-content .day:last-child { + border-bottom: none; + margin-bottom: 0; + padding-bottom: 0; +} + +.itinerary-content .day-header { + background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; + font-size: 1.4em; + font-weight: 700; + margin-bottom: 20px; +} + +.events-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); + gap: 16px; +} + +.itinerary-content .event { + margin-bottom: 0; + padding: 16px; + background: white; + border-radius: 12px; + border: 1px solid #e9ecef; + box-shadow: 0 4px 12px rgba(0,0,0,0.06); +} + +.event-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 8px; +} + +.itinerary-content .event-type { + font-weight: 700; + color: #4facfe; + letter-spacing: 0.2px; +} + +.itinerary-content .event-location { + font-size: 1.1em; + font-weight: 600; + color: #333; + margin-bottom: 8px; +} + +.itinerary-content .event-description { + color: #666; + line-height: 1.6; +} + +.maps-link { + font-size: 0.85em; + color: #0d6efd; + text-decoration: none; +} + +.maps-link:hover { + text-decoration: underline; +} + +/* Loading Indicator */ +.loading { + text-align: center; + padding: 40px; +} + +.spinner { + width: 60px; + height: 60px; + margin: 0 auto 20px; + border: 5px solid rgba(79, 172, 254, 0.1); + border-top: 5px solid #4facfe; + border-right: 5px solid #00f2fe; + border-radius: 50%; + animation: spin 1s linear infinite; +} + +@keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} + +.loading p { + color: #666; + font-size: 1.1em; +} + +/* Footer */ +footer { + text-align: center; + color: white; + margin-top: 30px; + padding: 20px; + opacity: 0.8; +} + +/* Animations */ +@keyframes fadeInDown { + from { + opacity: 0; + transform: translateY(-30px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +@keyframes fadeInUp { + from { + opacity: 0; + transform: translateY(30px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +@keyframes fadeIn { + from { + opacity: 0; + } + to { + opacity: 1; + } +} + +@keyframes float { + 0%, 100% { + transform: translateY(0) translateX(0); + } + 33% { + transform: translateY(-20px) translateX(10px); + } + 66% { + transform: translateY(20px) translateX(-10px); + } +} + +/* Responsive Design */ +@media (max-width: 768px) { + body { + padding: 10px; + } + + main { + padding: 25px; + } + + header h1 { + font-size: 2em; + } + + .form-actions { + flex-direction: column; + } + + .examples-grid { + grid-template-columns: 1fr; + } +} \ No newline at end of file diff --git a/examples/trip_planner/sdk/python/test.py b/examples/trip_planner/sdk/python/test.py new file mode 100644 index 0000000..ad26871 --- /dev/null +++ b/examples/trip_planner/sdk/python/test.py @@ -0,0 +1,58 @@ +""" +Minimal RunAgent SDK Test for TripGenius + +Update AGENT_ID and run! +""" + +from runagent import RunAgentClient + +# UPDATE THIS! +AGENT_ID = "be1eef6e-2700-4980-b808-e94b3394e747" + + +# ============================================ +# NON-STREAMING TEST +# ============================================ + +print("\n🧪 Test 1: Non-Streaming") +print("-" * 50) + +client = RunAgentClient( + agent_id=AGENT_ID, + entrypoint_tag="trip_create", + local=False +) + +result = client.run( + destination="Kanazawa", + num_days=2, + preferences="Cuisine, Landscape" +) + +print(f"\n✅ Result:") +print(result) + + +# ============================================ +# STREAMING TEST +# ============================================ + +# print("\n\n🧪 Test 2: Streaming") +# print("-" * 50) + +# stream_client = RunAgentClient( +# agent_id=AGENT_ID, +# entrypoint_tag="trip_stream", +# local=True +# ) + +# print("\n📡 Streaming response:\n") + +# for chunk in stream_client.run( +# destination="Paris", +# num_days=2, +# preferences="art museums, French cuisine" +# ): +# print(chunk, end="", flush=True) + +# print("\n\n✅ Done!") \ No newline at end of file diff --git a/leads_20251102_115848.csv b/leads_20251102_115848.csv new file mode 100644 index 0000000..090aecb --- /dev/null +++ b/leads_20251102_115848.csv @@ -0,0 +1,4 @@ +Website URL,Username,Bio,Post Type,Timestamp,Upvotes,Links +https://www.quora.com/Where-can-I-hire-someone-to-create-chatbot-customer-service,Quora User,Lead from Quora - Manual review needed,discussion,Recent,0,https://www.quora.com/Where-can-I-hire-someone-to-create-chatbot-customer-service +https://www.quora.com/What-is-the-most-realistic-AI-based-chatbot-website,Quora User,Lead from Quora - Manual review needed,discussion,Recent,0,https://www.quora.com/What-is-the-most-realistic-AI-based-chatbot-website +https://www.quora.com/Which-is-the-best-chatbot-provider-for-customer-service,Quora User,Lead from Quora - Manual review needed,discussion,Recent,0,https://www.quora.com/Which-is-the-best-chatbot-provider-for-customer-service diff --git a/leads_20251102_120327.csv b/leads_20251102_120327.csv new file mode 100644 index 0000000..fe20b58 --- /dev/null +++ b/leads_20251102_120327.csv @@ -0,0 +1,4 @@ +Website URL,Username,Bio,Post Type,Timestamp,Upvotes,Links +https://www.quora.com/What-is-the-most-realistic-AI-based-chatbot-website,Quora User,Lead from Quora - Manual review needed,discussion,Recent,0,https://www.quora.com/What-is-the-most-realistic-AI-based-chatbot-website +https://www.quora.com/Where-can-I-hire-someone-to-create-chatbot-customer-service,Quora User,Lead from Quora - Manual review needed,discussion,Recent,0,https://www.quora.com/Where-can-I-hire-someone-to-create-chatbot-customer-service +https://www.quora.com/Where-can-I-find-an-AI-charbot-service,Quora User,Lead from Quora - Manual review needed,discussion,Recent,0,https://www.quora.com/Where-can-I-find-an-AI-charbot-service diff --git a/parlant-data/cache_embeddings.json b/parlant-data/cache_embeddings.json deleted file mode 100644 index 9e26dfe..0000000 --- a/parlant-data/cache_embeddings.json +++ /dev/null @@ -1 +0,0 @@ -{} \ No newline at end of file diff --git a/parlant-data/evaluation_cache.json b/parlant-data/evaluation_cache.json deleted file mode 100644 index 9e26dfe..0000000 --- a/parlant-data/evaluation_cache.json +++ /dev/null @@ -1 +0,0 @@ -{} \ No newline at end of file diff --git a/runagent-rust/API_AUTHENTICATION.md b/runagent-rust/API_AUTHENTICATION.md new file mode 100644 index 0000000..bb283f6 --- /dev/null +++ b/runagent-rust/API_AUTHENTICATION.md @@ -0,0 +1,92 @@ +# How RunAgent Rust SDK Handles API Authentication + +## Overview + +The Rust SDK is **independent** and doesn't validate tokens itself. Instead, it sends your API token to the RunAgent server, which validates it. Here's how it works: + +## 1. API Token Loading (Priority Order) + +The SDK loads your API token from these sources in order of priority: + +1. **Environment Variable** (Highest Priority) + ```bash + export RUNAGENT_API_KEY="your-new-api-key-here" + ``` + +2. **Config File** (`~/.runagent/user_data.json`) + ```json + { + "api_key": "your-api-key", + "base_url": "http://20.84.81.110:8335/" + } + ``` + +3. **Default** (None - will fail on protected endpoints) + +## 2. How Tokens Are Sent to Server + +When the SDK makes requests, it sends your API token in **TWO ways** for maximum compatibility: + +### Method 1: Authorization Header +```rust +Authorization: Bearer +``` + +### Method 2: Query Parameter (for WebSocket compatibility) +``` +?token= +``` + +See `src/client/rest_client.rs` lines 127-149: +- Line 128-130: Adds token as query parameter +- Line 148-149: Adds Authorization header with Bearer token + +## 3. Server-Side Validation + +The SDK **does NOT validate** tokens locally. When you make a request: + +1. SDK sends your API token to the server +2. Server validates the token +3. Server checks if token has permission for requested resource +4. Server returns: + - ✅ Success (200) if token is valid and authorized + - ❌ 401 Unauthorized if token is invalid + - ❌ 403 Forbidden if token is valid but lacks permission + +## 4. Updating Your API Key + +If you created a new API key, update it using one of these methods: + +### Option A: Update Config File +```bash +cat > ~/.runagent/user_data.json << EOF +{ + "api_key": "your-new-api-key-here", + "base_url": "http://20.84.81.110:8335/" +} +EOF +``` + +### Option B: Set Environment Variable +```bash +export RUNAGENT_API_KEY="your-new-api-key-here" +export RUNAGENT_BASE_URL="http://20.84.81.110:8335/" +``` + +### Option C: Use Config API (if implemented) +```rust +use runagent::utils::Config; + +let mut config = Config::load()?; +config.api_key = Some("your-new-api-key".to_string()); +config.save()?; +``` + +## 5. Current Status + +Your current config file (`~/.runagent/user_data.json`) has: +- **Old API Key**: `rau_af476c074eb5045bfb5dee677140f8a4d3da6215056c26497da48e85390e4b43` +- **Base URL**: `http://20.84.81.110:8335/` ✅ (correct) + +To use your new API key, update the config file with the new token! + diff --git a/runagent-rust/runagent/Cargo.toml b/runagent-rust/runagent/Cargo.toml index 7cf50f2..192a29b 100644 --- a/runagent-rust/runagent/Cargo.toml +++ b/runagent-rust/runagent/Cargo.toml @@ -2,7 +2,7 @@ name = "runagent" version = "0.1.23" edition = "2021" -description = "RunAgent SDK for Rust - Deploy and manage AI agents easily" +description = "RunAgent SDK for Rust - Client SDK for interacting with deployed AI agents" license = "MIT" repository = "https://github.com/runagent-dev/runagent" homepage = "https://runagent.ai" @@ -18,14 +18,9 @@ authors = ["RunAgent "] tokio = { workspace = true } tokio-tungstenite = { workspace = true } reqwest = { workspace = true } -axum = { workspace = true } -tower = { workspace = true } -tower-http = { workspace = true, features = ["trace"] } -hyper = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } serde_yaml = { workspace = true } -sqlx = { workspace = true } uuid = { workspace = true } anyhow = { workspace = true } thiserror = { workspace = true } @@ -37,22 +32,20 @@ tracing = { workspace = true } tracing-subscriber = { workspace = true } config = { workspace = true } dotenv = { workspace = true } -which = { workspace = true } -tempfile = { workspace = true } async-stream = { workspace = true } +sqlx = { workspace = true } # Additional dependencies url = "2.5" bytes = "1.5" mime = "0.3" percent-encoding = "2.3" -dirs = "5.0" # Add this line +dirs = "5.0" [dev-dependencies] tokio-test = "0.4" +tempfile = "3.8" -# Add the features section [features] -default = ["db", "server"] -server = [] +default = ["db"] db = [] diff --git a/runagent-rust/runagent/src/client/rest_client.rs b/runagent-rust/runagent/src/client/rest_client.rs index a21b51a..aa21409 100644 --- a/runagent-rust/runagent/src/client/rest_client.rs +++ b/runagent-rust/runagent/src/client/rest_client.rs @@ -23,8 +23,9 @@ impl RestClient { api_key: Option, api_prefix: Option<&str>, ) -> RunAgentResult { + // Increase timeout to 10 minutes (600 seconds) to match agent execution timeout let client = Client::builder() - .timeout(Duration::from_secs(30)) + .timeout(Duration::from_secs(600)) .user_agent("RunAgent-Rust-SDK/0.1.0") .build()?; @@ -68,7 +69,20 @@ impl RestClient { } else { // Try to parse as JSON to get error details if let Ok(json) = serde_json::from_str::(&error_text) { - if let Some(detail) = json.get("detail").and_then(|d| d.as_str()) { + // Try to extract nested error message + if let Some(error_obj) = json.get("error") { + if let Some(message) = error_obj.get("message").and_then(|m| m.as_str()) { + message.to_string() + } else if let Some(detail) = json.get("detail").and_then(|d| d.as_str()) { + detail.to_string() + } else if let Some(message) = json.get("message").and_then(|m| m.as_str()) { + message.to_string() + } else if let Some(error) = json.get("error").and_then(|e| e.as_str()) { + error.to_string() + } else { + error_text + } + } else if let Some(detail) = json.get("detail").and_then(|d| d.as_str()) { detail.to_string() } else if let Some(message) = json.get("message").and_then(|m| m.as_str()) { message.to_string() @@ -82,9 +96,18 @@ impl RestClient { } }; + // Check if error message contains permission/403 info even if status is 500 + if error_msg.contains("permission") || error_msg.contains("403") || error_msg.contains("do not have permission") { + return Err(RunAgentError::authentication(format!( + "Access denied: {}. This usually means:\n - The agent doesn't belong to your account\n - Your API key doesn't have permission to access this agent\n - The agent ID is incorrect", error_msg + ))); + } + match status.as_u16() { 401 => Err(RunAgentError::authentication(error_msg)), - 403 => Err(RunAgentError::authentication(format!("Access denied: {}", error_msg))), + 403 => Err(RunAgentError::authentication(format!( + "Access denied: {}. This usually means:\n - The agent doesn't belong to your account\n - Your API key doesn't have permission to access this agent\n - The agent ID is incorrect", error_msg + ))), 400 | 422 => Err(RunAgentError::validation(error_msg)), 404 => Err(RunAgentError::validation(format!("Not found: {}", error_msg))), 500..=599 => Err(RunAgentError::server(format!("Server error: {}", error_msg))), @@ -100,14 +123,15 @@ impl RestClient { data: Option<&Value>, params: Option<&HashMap>, ) -> RunAgentResult { - let url = self.get_url(path)?; + let mut url = self.get_url(path)?; - let mut request_builder = self.client.request(method, url); - - // Add authorization header if API key is available + // Add API key as token query parameter if available (matching WebSocket behavior) if let Some(ref api_key) = self.api_key { - request_builder = request_builder.header("Authorization", format!("Bearer {}", api_key)); + url.query_pairs_mut() + .append_pair("token", api_key); } + + let mut request_builder = self.client.request(method, url); // Add query parameters if let Some(params) = params { @@ -121,6 +145,11 @@ impl RestClient { .json(data); } + // Add Authorization header if API key is available + if let Some(ref api_key) = self.api_key { + request_builder = request_builder.header("Authorization", format!("Bearer {}", api_key)); + } + let response = request_builder.send().await?; self.handle_response(response).await } @@ -163,20 +192,45 @@ impl RestClient { input_kwargs: &HashMap, ) -> RunAgentResult { let data = serde_json::json!({ - "input_data": { - "input_args": input_args, - "input_kwargs": input_kwargs - } + "id": "run_start", + "entrypoint_tag": entrypoint_tag, + "input_args": input_args, + "input_kwargs": input_kwargs, + "timeout_seconds": 600, + "async_execution": false }); - let path = format!("agents/{}/execute/{}", agent_id, entrypoint_tag); + let path = format!("agents/{}/run", agent_id); + let url = self.get_url(&path)?; + tracing::debug!("Running agent {} with entrypoint {} at {}", agent_id, entrypoint_tag, url); + self.post(&path, &data).await + .map_err(|e| { + if e.category() == "validation" && e.to_string().contains("Not found") { + RunAgentError::validation(format!( + "Agent {} not found on server at {}. Check that:\n - The agent exists and is deployed\n - The agent ID is correct\n - The base URL ({}) is correct\n - Your API key is valid (if required)", + agent_id, url, self.base_url + )) + } else { + e + } + }) } /// Get agent architecture information pub async fn get_agent_architecture(&self, agent_id: &str) -> RunAgentResult { let path = format!("agents/{}/architecture", agent_id); self.get(&path).await + .map_err(|e| { + if e.category() == "validation" && e.to_string().contains("Not found") { + RunAgentError::validation(format!( + "Agent {} not found on server. Check that the agent exists and is deployed. Error: {}", + agent_id, e + )) + } else { + e + } + }) } /// Health check diff --git a/runagent-rust/runagent/src/client/runagent_client.rs b/runagent-rust/runagent/src/client/runagent_client.rs index d80e77c..6fc89e9 100644 --- a/runagent-rust/runagent/src/client/runagent_client.rs +++ b/runagent-rust/runagent/src/client/runagent_client.rs @@ -27,7 +27,10 @@ pub struct RunAgentClient { } impl RunAgentClient { - /// Create a new RunAgent client with database lookup + /// Create a new RunAgent client + /// + /// For local agents, tries to lookup host/port from database if available. + /// For remote agents, configuration is loaded from environment variables. pub async fn new( agent_id: &str, entrypoint_tag: &str, @@ -135,9 +138,20 @@ impl RunAgentClient { db_service, }; - // Get agent architecture - client.agent_architecture = Some(client.get_agent_architecture_internal().await?); - client.validate_entrypoint()?; + // Get agent architecture (skip validation to match Python SDK behavior) + match client.get_agent_architecture_internal().await { + Ok(architecture) => { + client.agent_architecture = Some(architecture); + tracing::debug!("Agent architecture loaded, skipping client-side validation"); + } + Err(e) => { + tracing::debug!("Failed to get agent architecture, skipping validation: {}", e); + // Set a minimal architecture to avoid validation errors + client.agent_architecture = Some(serde_json::json!({ + "entrypoints": [{"tag": "simulate_stream", "file": "main.py", "module": "simulate_stream"}] + })); + } + } Ok(client) } @@ -146,7 +160,7 @@ impl RunAgentClient { match self.rest_client.get_agent_architecture(&self.agent_id).await { Ok(architecture) => Ok(architecture), Err(_) => { - // Fallback: provide default architecture + // Fallback: provide default architecture with common entrypoints Ok(serde_json::json!({ "entrypoints": [ { @@ -158,6 +172,16 @@ impl RunAgentClient { "tag": "generic_stream", "file": "main.py", "module": "run_stream" + }, + { + "tag": "simulate_stream", + "file": "main.py", + "module": "simulate_stream" + }, + { + "tag": "run", + "file": "main.py", + "module": "run" } ] })) @@ -219,17 +243,52 @@ impl RunAgentClient { .await?; if response.get("success").and_then(|s| s.as_bool()).unwrap_or(false) { + // Handle new response format with nested data (matching Python SDK) + if let Some(data) = response.get("data") { + if let Some(result_data) = data.get("result_data") { + if let Some(output_data) = result_data.get("data") { + // Check if the output contains a generator object string + if let Some(content_str) = output_data.as_str() { + if content_str.contains("generator object") { + tracing::warn!("Agent returned generator object instead of content. Consider using streaming endpoint for this agent."); + // Return the raw string for now + return Ok(output_data.clone()); + } + } + return self.serializer.deserialize_object(output_data.clone()); + } + } + } + // Fallback to old format for backward compatibility if let Some(output_data) = response.get("output_data") { - self.serializer.deserialize_object(output_data.clone()) - } else { - Ok(Value::Null) + // Check if the output contains a generator object string + if let Some(content_str) = output_data.as_str() { + if content_str.contains("generator object") { + tracing::warn!("Agent returned generator object instead of content. Consider using streaming endpoint for this agent."); + // Return the raw string for now + return Ok(output_data.clone()); + } + } + return self.serializer.deserialize_object(output_data.clone()); } + Ok(Value::Null) } else { - let error_msg = response - .get("error") - .and_then(|e| e.as_str()) - .unwrap_or("Unknown error"); - Err(RunAgentError::server(error_msg)) + // Handle new error format with ErrorDetail object (matching Python SDK) + if let Some(error_info) = response.get("error") { + if let Some(error_obj) = error_info.as_object() { + if let (Some(message), Some(code)) = ( + error_obj.get("message").and_then(|m| m.as_str()), + error_obj.get("code").and_then(|c| c.as_str()) + ) { + return Err(RunAgentError::server(format!("[{}] {}", code, message))); + } + } + // Fallback to old format + if let Some(error_msg) = error_info.as_str() { + return Err(RunAgentError::server(error_msg)); + } + } + Err(RunAgentError::server("Unknown error")) } } diff --git a/runagent-rust/runagent/src/client/socket_client.rs b/runagent-rust/runagent/src/client/socket_client.rs index f936a1c..86bc476 100644 --- a/runagent-rust/runagent/src/client/socket_client.rs +++ b/runagent-rust/runagent/src/client/socket_client.rs @@ -1,8 +1,7 @@ //! WebSocket client for streaming agent interactions use crate::types::{ - RunAgentError, RunAgentResult, SafeMessage, WebSocketActionType, WebSocketAgentRequest, - AgentInputArgs, MessageType, + RunAgentError, RunAgentResult, SafeMessage, MessageType, }; use crate::utils::config::Config; use crate::utils::serializer::CoreSerializer; @@ -55,9 +54,14 @@ impl SocketClient { Self::new(&ws_url, config.api_key(), Some("/api/v1")) } - fn get_websocket_url(&self, agent_id: &str, entrypoint_tag: &str) -> RunAgentResult { - let path = format!("agents/{}/execute/{}", agent_id, entrypoint_tag); - let full_url = format!("{}{}/{}", self.base_socket_url, self.api_prefix, path); + fn get_websocket_url(&self, agent_id: &str, _entrypoint_tag: &str) -> RunAgentResult { + let path = format!("agents/{}/run-stream", agent_id); + let mut full_url = format!("{}{}/{}", self.base_socket_url, self.api_prefix, path); + + // Add API key as token parameter if available + if let Some(ref api_key) = self.api_key { + full_url = format!("{}?token={}", full_url, api_key); + } Url::parse(&full_url) .map_err(|e| RunAgentError::validation(format!("Invalid WebSocket URL: {}", e))) @@ -81,39 +85,34 @@ impl SocketClient { let (mut write, mut read) = ws_stream.split(); - // Prepare start stream request - let request = WebSocketAgentRequest { - action: WebSocketActionType::StartStream, - agent_id: agent_id.to_string(), - input_data: AgentInputArgs { - input_args: input_args.to_vec(), - input_kwargs: input_kwargs.clone(), - }, - stream_config: HashMap::new(), - }; - - let start_msg = SafeMessage::new( - "stream_start".to_string(), - MessageType::Status, - serde_json::to_value(&request)?, - ); - - // Send start stream message - let serialized_msg = self.serializer.serialize_message(&start_msg)?; + // Prepare start stream request with id field (as middleware expects) + let request_data = serde_json::json!({ + "id": "stream_start", + "entrypoint_tag": entrypoint_tag, + "input_args": input_args, + "input_kwargs": input_kwargs, + "timeout_seconds": 600, + "async_execution": false + }); + + // Send the request data directly (matching Python SDK format) + let serialized_msg = serde_json::to_string(&request_data)?; write.send(Message::Text(serialized_msg)).await .map_err(|e| RunAgentError::connection(format!("Failed to send start message: {}", e)))?; - // Create stream that processes incoming messages - let serializer = self.serializer.clone(); + // Create stream that processes incoming messages (matching Python SDK behavior) let stream = async_stream::stream! { while let Some(message) = read.next().await { match message { Ok(Message::Text(text)) => { - match serializer.deserialize_message(&text) { - Ok(safe_msg) => { - match safe_msg.message_type { - MessageType::Status => { - if let Some(status) = safe_msg.data.get("status") { + // Parse as plain JSON (matching Python SDK) + match serde_json::from_str::(&text) { + Ok(msg) => { + let message_type = msg.get("type").and_then(|v| v.as_str()); + + match message_type { + Some("status") => { + if let Some(status) = msg.get("status").and_then(|v| v.as_str()) { if status == "stream_completed" { break; } else if status == "stream_started" { @@ -121,20 +120,28 @@ impl SocketClient { } } } - MessageType::Error => { - yield Err(RunAgentError::server( - safe_msg.error.unwrap_or_else(|| "Agent error".to_string()) - )); + Some("error") => { + let error_msg = msg.get("error") + .or_else(|| msg.get("detail")) + .and_then(|v| v.as_str()) + .unwrap_or("Unknown error"); + yield Err(RunAgentError::server(format!("Stream error: {}", error_msg))); break; } + Some("data") => { + // Yield the content field (matching Python SDK) + if let Some(content) = msg.get("content") { + yield Ok(content.clone()); + } + } _ => { - // Yield the actual chunk data - yield Ok(safe_msg.data); + // For other message types, yield the whole message + yield Ok(msg); } } } Err(e) => { - yield Err(RunAgentError::server(format!("Stream error: {}", e))); + yield Err(RunAgentError::server(format!("Stream error: JSON error: {}", e))); break; } } diff --git a/runagent-rust/runagent/src/constants.rs b/runagent-rust/runagent/src/constants.rs index 36cde3d..430a5e2 100644 --- a/runagent-rust/runagent/src/constants.rs +++ b/runagent-rust/runagent/src/constants.rs @@ -31,7 +31,7 @@ pub const ENV_LOCAL_CACHE_DIRECTORY: &str = "RUNAGENT_CACHE_DIR"; pub const ENV_RUNAGENT_LOGGING_LEVEL: &str = "RUNAGENT_LOGGING_LEVEL"; /// Default base URL -pub const DEFAULT_BASE_URL: &str = "http://52.237.88.147:8330/"; +pub const DEFAULT_BASE_URL: &str = "http://20.84.81.110:8335/"; /// Agent config file name pub const AGENT_CONFIG_FILE_NAME: &str = "runagent.config.json"; diff --git a/runagent-rust/runagent/src/db/manager.rs b/runagent-rust/runagent/src/db/manager.rs deleted file mode 100644 index 9636659..0000000 --- a/runagent-rust/runagent/src/db/manager.rs +++ /dev/null @@ -1,323 +0,0 @@ -//! Simplified Database manager without migrations - -use crate::constants::LOCAL_CACHE_DIRECTORY; -use crate::types::{RunAgentError, RunAgentResult}; -use crate::db::models::DatabaseStats; -use sqlx::{sqlite::SqliteConnectOptions, Pool, Sqlite, SqlitePool, Transaction, Row}; -use std::collections::HashMap; -use std::path::PathBuf; - -/// Database manager for SQLite operations (No migrations needed) -pub struct DatabaseManager { - pool: Pool, - db_path: PathBuf, -} - -impl DatabaseManager { - /// Create a new database manager with automatic schema creation - pub async fn new(db_path: Option) -> RunAgentResult { - let db_path = db_path.unwrap_or_else(|| { - LOCAL_CACHE_DIRECTORY.join("runagent_local.db") - }); - - // Ensure parent directory exists - if let Some(parent) = db_path.parent() { - std::fs::create_dir_all(parent) - .map_err(|e| RunAgentError::database(format!("Failed to create database directory: {}", e)))?; - } - - // Create connection options - let options = SqliteConnectOptions::new() - .filename(&db_path) - .create_if_missing(true); - - // Create connection pool - let pool = SqlitePool::connect_with(options).await - .map_err(|e| RunAgentError::database(format!("Failed to connect to database: {}", e)))?; - - let manager = Self { pool, db_path }; - - // Create tables if they don't exist - manager.create_tables_if_not_exist().await?; - - Ok(manager) - } - - /// Create tables if they don't exist (replaces migrations) - async fn create_tables_if_not_exist(&self) -> RunAgentResult<()> { - // Create agents table - sqlx::query( - r#" - CREATE TABLE IF NOT EXISTS agents ( - agent_id TEXT PRIMARY KEY, - agent_path TEXT NOT NULL, - host TEXT NOT NULL DEFAULT 'localhost', - port INTEGER NOT NULL DEFAULT 8450, - framework TEXT, - status TEXT NOT NULL DEFAULT 'deployed', - deployed_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, - last_run DATETIME, - run_count INTEGER NOT NULL DEFAULT 0, - success_count INTEGER NOT NULL DEFAULT 0, - error_count INTEGER NOT NULL DEFAULT 0, - created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, - updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP - ) - "# - ) - .execute(&self.pool) - .await - .map_err(|e| RunAgentError::database(format!("Failed to create agents table: {}", e)))?; - - // Create agent_runs table - sqlx::query( - r#" - CREATE TABLE IF NOT EXISTS agent_runs ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - agent_id TEXT NOT NULL, - input_data TEXT NOT NULL, - output_data TEXT, - success BOOLEAN NOT NULL, - error_message TEXT, - execution_time REAL, - started_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, - completed_at DATETIME, - FOREIGN KEY (agent_id) REFERENCES agents(agent_id) ON DELETE CASCADE - ) - "# - ) - .execute(&self.pool) - .await - .map_err(|e| RunAgentError::database(format!("Failed to create agent_runs table: {}", e)))?; - - // Create indexes - sqlx::query("CREATE INDEX IF NOT EXISTS idx_agents_status ON agents(status)") - .execute(&self.pool) - .await - .map_err(|e| RunAgentError::database(format!("Failed to create index: {}", e)))?; - - sqlx::query("CREATE INDEX IF NOT EXISTS idx_agent_runs_agent_id ON agent_runs(agent_id)") - .execute(&self.pool) - .await - .map_err(|e| RunAgentError::database(format!("Failed to create index: {}", e)))?; - - sqlx::query("CREATE INDEX IF NOT EXISTS idx_agent_runs_started_at ON agent_runs(started_at)") - .execute(&self.pool) - .await - .map_err(|e| RunAgentError::database(format!("Failed to create index: {}", e)))?; - - Ok(()) - } - - /// Get the database pool - pub fn pool(&self) -> &Pool { - &self.pool - } - - /// Get the database path - pub fn db_path(&self) -> &PathBuf { - &self.db_path - } - - /// Execute a transaction - pub async fn transaction(&self, f: F) -> RunAgentResult - where - F: FnOnce(&mut Transaction<'_, Sqlite>) -> Fut, - Fut: std::future::Future>, - { - let mut tx = self.pool.begin().await - .map_err(|e| RunAgentError::database(format!("Failed to begin transaction: {}", e)))?; - - let result = f(&mut tx).await?; - - tx.commit().await - .map_err(|e| RunAgentError::database(format!("Failed to commit transaction: {}", e)))?; - - Ok(result) - } - - /// Get database statistics - pub async fn get_stats(&self) -> RunAgentResult { - // Get total agents - let total_agents: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM agents") - .fetch_one(&self.pool) - .await - .map_err(|e| RunAgentError::database(format!("Failed to count agents: {}", e)))?; - - // Get agent status counts - let rows = sqlx::query("SELECT status, COUNT(*) as count FROM agents GROUP BY status") - .fetch_all(&self.pool) - .await - .map_err(|e| RunAgentError::database(format!("Failed to get status counts: {}", e)))?; - - let mut agent_status_counts = HashMap::new(); - for row in rows { - let status: String = row.get("status"); - let count: i64 = row.get("count"); - agent_status_counts.insert(status, count as usize); - } - - // Get total runs (handle case where table might not exist or be empty) - let total_runs: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM agent_runs") - .fetch_optional(&self.pool) - .await - .map_err(|e| RunAgentError::database(format!("Failed to count runs: {}", e)))? - .unwrap_or(0); - - // Calculate database size (approximate) - let database_size_mb = match std::fs::metadata(&self.db_path) { - Ok(metadata) => metadata.len() as f64 / (1024.0 * 1024.0), - Err(_) => 0.0, - }; - - Ok(DatabaseStats { - total_agents: total_agents as usize, - agent_status_counts, - total_runs: total_runs as usize, - database_size_mb, - database_path: self.db_path.to_string_lossy().to_string(), - rest_client_configured: false, // This would be set by the service layer - }) - } - - /// Get database file size in bytes - pub fn get_database_size(&self) -> u64 { - std::fs::metadata(&self.db_path) - .map(|metadata| metadata.len()) - .unwrap_or(0) - } - - /// Check if database file exists - pub fn database_exists(&self) -> bool { - self.db_path.exists() - } - - /// Get database connection info - pub fn get_connection_info(&self) -> HashMap { - let mut info = HashMap::new(); - info.insert("database_path".to_string(), serde_json::json!(self.db_path.to_string_lossy())); - info.insert("database_exists".to_string(), serde_json::json!(self.database_exists())); - info.insert("database_size_bytes".to_string(), serde_json::json!(self.get_database_size())); - info.insert("pool_size".to_string(), serde_json::json!(self.pool.size())); - info.insert("idle_connections".to_string(), serde_json::json!(self.pool.num_idle())); - info - } - - /// Perform database maintenance - pub async fn maintenance(&self) -> RunAgentResult<()> { - // Vacuum the database to reclaim space - sqlx::query("VACUUM") - .execute(&self.pool) - .await - .map_err(|e| RunAgentError::database(format!("Failed to vacuum database: {}", e)))?; - - // Analyze to update statistics - sqlx::query("ANALYZE") - .execute(&self.pool) - .await - .map_err(|e| RunAgentError::database(format!("Failed to analyze database: {}", e)))?; - - Ok(()) - } - - /// Check database integrity - pub async fn check_integrity(&self) -> RunAgentResult { - let result: String = sqlx::query_scalar("PRAGMA integrity_check") - .fetch_one(&self.pool) - .await - .map_err(|e| RunAgentError::database(format!("Failed to check integrity: {}", e)))?; - - Ok(result == "ok") - } - - /// Close the database connection - pub async fn close(self) { - self.pool.close().await; - } -} - -impl Drop for DatabaseManager { - fn drop(&mut self) { - // Note: We can't call async close() in Drop, but the pool will be cleaned up automatically - tracing::debug!("DatabaseManager dropped for path: {}", self.db_path.display()); - } -} - -#[cfg(test)] -mod tests { - use super::*; - use tempfile::TempDir; - - #[tokio::test] - async fn test_database_manager_creation() { - let temp_dir = TempDir::new().unwrap(); - let db_path = temp_dir.path().join("test.db"); - - let manager = DatabaseManager::new(Some(db_path.clone())).await; - assert!(manager.is_ok()); - - let manager = manager.unwrap(); - assert_eq!(manager.db_path(), &db_path); - assert!(manager.database_exists()); - } - - #[tokio::test] - async fn test_database_stats() { - let temp_dir = TempDir::new().unwrap(); - let db_path = temp_dir.path().join("test.db"); - - let manager = DatabaseManager::new(Some(db_path)).await.unwrap(); - let stats = manager.get_stats().await; - - assert!(stats.is_ok()); - let stats = stats.unwrap(); - assert_eq!(stats.total_agents, 0); - assert_eq!(stats.total_runs, 0); - } - - #[tokio::test] - async fn test_database_integrity() { - let temp_dir = TempDir::new().unwrap(); - let db_path = temp_dir.path().join("test.db"); - - let manager = DatabaseManager::new(Some(db_path)).await.unwrap(); - let integrity_ok = manager.check_integrity().await; - - assert!(integrity_ok.is_ok()); - assert!(integrity_ok.unwrap()); - } - - // #[tokio::test] - // async fn test_transaction() { - // let temp_dir = TempDir::new().unwrap(); - // let db_path = temp_dir.path().join("test.db"); - - // let manager = DatabaseManager::new(Some(db_path)).await.unwrap(); - - // let result = manager.transaction(|tx| async move { - // // Insert a test agent in transaction - // sqlx::query( - // "INSERT INTO agents (agent_id, agent_path) VALUES (?, ?)" - // ) - // .bind("test-agent") - // .bind("/test/path") - // .execute(tx) - // .await - // .map_err(|e| RunAgentError::database(format!("Insert failed: {}", e)))?; - - // Ok(42) - // }).await; - - // assert!(result.is_ok()); - // assert_eq!(result.unwrap(), 42); - - // // Verify the agent was inserted - // let count: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM agents WHERE agent_id = ?") - // .bind("test-agent") - // .fetch_one(manager.pool()) - // .await - // .unwrap(); - - // assert_eq!(count, 1); - // } -} \ No newline at end of file diff --git a/runagent-rust/runagent/src/db/mod.rs b/runagent-rust/runagent/src/db/mod.rs index c8b5f16..5fb6f94 100644 --- a/runagent-rust/runagent/src/db/mod.rs +++ b/runagent-rust/runagent/src/db/mod.rs @@ -1,13 +1,10 @@ -//! Database components for the RunAgent SDK +//! Minimal database module for agent lookups //! -//! This module provides database functionality for managing local agent deployments, -//! tracking agent runs, and maintaining capacity information. +//! This module provides a simple database interface for looking up local agent +//! metadata (host, port) by agent ID. This allows connecting to agents without +//! explicitly specifying the address. -pub mod manager; -pub mod models; pub mod service; -// Re-export commonly used types -pub use manager::DatabaseManager; -pub use models::*; -pub use service::DatabaseService; \ No newline at end of file +pub use service::DatabaseService; + diff --git a/runagent-rust/runagent/src/db/models.rs b/runagent-rust/runagent/src/db/models.rs deleted file mode 100644 index 369c3d5..0000000 --- a/runagent-rust/runagent/src/db/models.rs +++ /dev/null @@ -1,483 +0,0 @@ -//! Database models for the RunAgent SDK - -use chrono::{DateTime, Utc}; -use serde::{Deserialize, Serialize}; -use sqlx::FromRow; - -/// Agent model representing deployed agents -#[derive(Debug, Clone, FromRow, Serialize, Deserialize)] -pub struct Agent { - pub agent_id: String, - pub agent_path: String, - pub host: String, - pub port: i32, - pub framework: Option, - pub status: String, - pub deployed_at: DateTime, - pub last_run: Option>, - pub run_count: i64, - pub success_count: i64, - pub error_count: i64, - pub created_at: DateTime, - pub updated_at: DateTime, -} - -impl Default for Agent { - fn default() -> Self { - let now = Utc::now(); - Self { - agent_id: String::new(), - agent_path: String::new(), - host: "localhost".to_string(), - port: 8450, - framework: None, - status: "deployed".to_string(), - deployed_at: now, - last_run: None, - run_count: 0, - success_count: 0, - error_count: 0, - created_at: now, - updated_at: now, - } - } -} - -impl Agent { - /// Create a new Agent instance - pub fn new(agent_id: String, agent_path: String, host: String, port: u16) -> Self { - let now = Utc::now(); - Self { - agent_id, - agent_path, - host, - port: port as i32, - framework: None, - status: "deployed".to_string(), - deployed_at: now, - last_run: None, - run_count: 0, - success_count: 0, - error_count: 0, - created_at: now, - updated_at: now, - } - } - - /// Set the framework for this agent - pub fn with_framework(mut self, framework: String) -> Self { - self.framework = Some(framework); - self - } - - /// Set the status for this agent - pub fn with_status(mut self, status: String) -> Self { - self.status = status; - self - } - - /// Update the agent's updated_at timestamp - pub fn touch(&mut self) { - self.updated_at = Utc::now(); - } - - /// Increment run count and update last_run - pub fn record_run(&mut self, success: bool) { - self.run_count += 1; - self.last_run = Some(Utc::now()); - self.updated_at = Utc::now(); - - if success { - self.success_count += 1; - } else { - self.error_count += 1; - } - } - - /// Get success rate as percentage - pub fn success_rate(&self) -> f64 { - if self.run_count == 0 { - 0.0 - } else { - (self.success_count as f64 / self.run_count as f64) * 100.0 - } - } - - /// Check if the agent is healthy - pub fn is_healthy(&self) -> bool { - self.status == "deployed" && self.error_count < self.success_count * 2 - } -} - -/// Agent run model representing individual executions -#[derive(Debug, Clone, FromRow, Serialize, Deserialize)] -pub struct AgentRun { - pub id: i64, - pub agent_id: String, - pub input_data: String, // JSON string - pub output_data: Option, // JSON string - pub success: bool, - pub error_message: Option, - pub execution_time: Option, - pub started_at: DateTime, - pub completed_at: Option>, -} - -impl AgentRun { - /// Create a new AgentRun instance - pub fn new(agent_id: String, input_data: String) -> Self { - Self { - id: 0, // Will be set by the database - agent_id, - input_data, - output_data: None, - success: false, - error_message: None, - execution_time: None, - started_at: Utc::now(), - completed_at: None, - } - } - - /// Mark the run as completed successfully - pub fn complete_success(mut self, output_data: String, execution_time: f64) -> Self { - self.output_data = Some(output_data); - self.success = true; - self.execution_time = Some(execution_time); - self.completed_at = Some(Utc::now()); - self - } - - /// Mark the run as completed with error - pub fn complete_error(mut self, error_message: String, execution_time: f64) -> Self { - self.error_message = Some(error_message); - self.success = false; - self.execution_time = Some(execution_time); - self.completed_at = Some(Utc::now()); - self - } - - /// Get the duration of this run - pub fn duration(&self) -> Option { - self.completed_at.map(|completed| completed - self.started_at) - } -} - -/// Database capacity information -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct CapacityInfo { - pub current_count: usize, - pub max_capacity: usize, - pub default_limit: usize, - pub remaining_slots: Option, - pub is_full: bool, - pub agents: Vec, - pub oldest_agent: Option, - pub newest_agent: Option, - pub limit_info: LimitInfo, - pub rest_client_configured: bool, -} - -/// Agent summary for capacity info -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct AgentSummary { - pub agent_id: String, - pub deployed_at: DateTime, - pub framework: Option, - pub status: String, - pub host: String, - pub port: i32, -} - -impl From for AgentSummary { - fn from(agent: Agent) -> Self { - Self { - agent_id: agent.agent_id, - deployed_at: agent.deployed_at, - framework: agent.framework, - status: agent.status, - host: agent.host, - port: agent.port, - } - } -} - -/// Limit information from API or default -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct LimitInfo { - pub limit: usize, - pub enhanced: bool, - pub source: String, // "default", "api", "enhanced" - pub api_available: bool, - pub api_validated: bool, - pub tier_info: Option, - pub features: Vec, - pub expires_at: Option, - pub unlimited: bool, - pub error: Option, -} - -impl Default for LimitInfo { - fn default() -> Self { - Self { - limit: 5, - enhanced: false, - source: "default".to_string(), - api_available: false, - api_validated: false, - tier_info: None, - features: Vec::new(), - expires_at: None, - unlimited: false, - error: None, - } - } -} - -/// Database statistics -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct DatabaseStats { - pub total_agents: usize, - pub agent_status_counts: std::collections::HashMap, - pub total_runs: usize, - pub database_size_mb: f64, - pub database_path: String, - pub rest_client_configured: bool, -} - -/// Agent deployment information -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct DeploymentInfo { - pub agent_id: String, - pub host: String, - pub port: u16, - pub status: String, - pub framework: String, - pub deployed_at: DateTime, - pub exists: bool, - pub source_exists: bool, - pub deployment_path: String, - pub folder_path: String, - pub stats: AgentStats, -} - -/// Agent statistics -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct AgentStats { - pub total_runs: i64, - pub success_count: i64, - pub error_count: i64, - pub success_rate: f64, - pub last_run: Option>, - pub avg_execution_time: Option, -} - -impl From for AgentStats { - fn from(agent: Agent) -> Self { - Self { - total_runs: agent.run_count, - success_count: agent.success_count, - error_count: agent.error_count, - success_rate: agent.success_rate(), - last_run: agent.last_run, - avg_execution_time: None, // Would be calculated from runs - } - } -} - -/// Add operation result -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct AddAgentResult { - pub success: bool, - pub message: String, - pub current_count: usize, - pub limit_source: String, - pub api_check_performed: bool, - pub allocated_host: Option, - pub allocated_port: Option, - pub address: Option, - pub max_allowed: Option, - pub remaining_slots: Option, // Can be "unlimited" - pub limit_info: Option, - pub error: Option, - pub code: Option, - pub oldest_agent: Option, - pub suggestion: Option, -} - -impl AddAgentResult { - pub fn success( - message: String, - current_count: usize, - limit_source: String, - api_check_performed: bool, - ) -> Self { - Self { - success: true, - message, - current_count, - limit_source, - api_check_performed, - allocated_host: None, - allocated_port: None, - address: None, - max_allowed: None, - remaining_slots: None, - limit_info: None, - error: None, - code: None, - oldest_agent: None, - suggestion: None, - } - } - - pub fn error(error: String, code: String) -> Self { - Self { - success: false, - message: String::new(), - current_count: 0, - limit_source: String::new(), - api_check_performed: false, - allocated_host: None, - allocated_port: None, - address: None, - max_allowed: None, - remaining_slots: None, - limit_info: None, - error: Some(error), - code: Some(code), - oldest_agent: None, - suggestion: None, - } - } - - pub fn with_allocation(mut self, host: String, port: u16) -> Self { - self.allocated_host = Some(host.clone()); - self.allocated_port = Some(port); - self.address = Some(format!("{}:{}", host, port)); - self - } - - pub fn with_capacity_info(mut self, max_allowed: usize, remaining_slots: usize) -> Self { - self.max_allowed = Some(max_allowed); - self.remaining_slots = Some(if max_allowed == 999 { - "unlimited".to_string() - } else { - remaining_slots.to_string() - }); - self - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_agent_creation() { - let agent = Agent::new( - "test-agent".to_string(), - "/path/to/agent".to_string(), - "localhost".to_string(), - 8450, - ); - - assert_eq!(agent.agent_id, "test-agent"); - assert_eq!(agent.agent_path, "/path/to/agent"); - assert_eq!(agent.host, "localhost"); - assert_eq!(agent.port, 8450); - assert_eq!(agent.status, "deployed"); - assert_eq!(agent.run_count, 0); - } - - #[test] - fn test_agent_with_framework() { - let agent = Agent::new( - "test-agent".to_string(), - "/path/to/agent".to_string(), - "localhost".to_string(), - 8450, - ).with_framework("langchain".to_string()); - - assert_eq!(agent.framework, Some("langchain".to_string())); - } - - #[test] - fn test_agent_record_run() { - let mut agent = Agent::new( - "test-agent".to_string(), - "/path/to/agent".to_string(), - "localhost".to_string(), - 8450, - ); - - assert_eq!(agent.run_count, 0); - assert_eq!(agent.success_count, 0); - - agent.record_run(true); - assert_eq!(agent.run_count, 1); - assert_eq!(agent.success_count, 1); - assert_eq!(agent.error_count, 0); - assert!(agent.last_run.is_some()); - } - - - #[test] - fn test_agent_run_creation() { - let run = AgentRun::new( - "test-agent".to_string(), - r#"{"message": "test"}"#.to_string(), - ); - - assert_eq!(run.agent_id, "test-agent"); - assert!(!run.success); - assert!(run.output_data.is_none()); - assert!(run.completed_at.is_none()); - } - - #[test] - fn test_agent_run_completion() { - let run = AgentRun::new( - "test-agent".to_string(), - r#"{"message": "test"}"#.to_string(), - ).complete_success( - r#"{"response": "Hello"}"#.to_string(), - 1.5, - ); - - assert!(run.success); - assert!(run.output_data.is_some()); - assert!(run.completed_at.is_some()); - assert_eq!(run.execution_time, Some(1.5)); - } - - #[test] - fn test_add_agent_result() { - let result = AddAgentResult::success( - "Agent added".to_string(), - 1, - "default".to_string(), - false, - ).with_allocation("localhost".to_string(), 8450); - - assert!(result.success); - assert_eq!(result.allocated_host, Some("localhost".to_string())); - assert_eq!(result.allocated_port, Some(8450)); - assert_eq!(result.address, Some("localhost:8450".to_string())); - } - - #[test] - fn test_agent_summary_from_agent() { - let agent = Agent::new( - "test-agent".to_string(), - "/path/to/agent".to_string(), - "localhost".to_string(), - 8450, - ).with_framework("langchain".to_string()); - - let summary = AgentSummary::from(agent.clone()); - assert_eq!(summary.agent_id, agent.agent_id); - assert_eq!(summary.framework, agent.framework); - assert_eq!(summary.status, agent.status); - } -} \ No newline at end of file diff --git a/runagent-rust/runagent/src/db/service.rs b/runagent-rust/runagent/src/db/service.rs index 4dd627e..bde6d96 100644 --- a/runagent-rust/runagent/src/db/service.rs +++ b/runagent-rust/runagent/src/db/service.rs @@ -1,423 +1,113 @@ -//! High-level database service with business logic +//! Database service for agent lookups -use crate::client::RestClient; -use crate::db::{manager::DatabaseManager, models::*}; +use crate::constants::{DATABASE_FILE_NAME, LOCAL_CACHE_DIRECTORY}; use crate::types::{RunAgentError, RunAgentResult}; -use chrono::{DateTime, Duration, Utc}; -use sqlx::{Row, FromRow}; -// use std::collections::HashMap; +use sqlx::{sqlite::SqlitePool, Row}; use std::path::PathBuf; -/// High-level database service with business logic +/// Agent information stored in database +#[derive(Debug, Clone)] +pub struct AgentInfo { + pub agent_id: String, + pub agent_path: String, + pub host: String, + pub port: i32, + pub framework: Option, + pub status: Option, +} + +/// Minimal database service for agent lookups pub struct DatabaseService { - manager: DatabaseManager, - rest_client: Option, - limits_cache: Option, - cache_expiry: Option>, + pool: SqlitePool, } impl DatabaseService { /// Create a new database service pub async fn new(db_path: Option) -> RunAgentResult { - let manager = DatabaseManager::new(db_path).await?; - - Ok(Self { - manager, - rest_client: None, - limits_cache: None, - cache_expiry: None, - }) - } - - /// Create database service with REST client for enhanced limits - pub async fn with_rest_client( - db_path: Option, - rest_client: RestClient, - ) -> RunAgentResult { - let manager = DatabaseManager::new(db_path).await?; - - Ok(Self { - manager, - rest_client: Some(rest_client), - limits_cache: None, - cache_expiry: None, - }) - } - - /// Add a new agent to the database - pub async fn add_agent(&self, agent: Agent) -> RunAgentResult { - let current_count = self.get_agent_count().await?; - let default_limit = self.get_default_limit(); - - // Phase 1: Check if we're within default limits - if current_count < default_limit { - let agent_id = agent.agent_id.clone(); - - self.insert_agent(agent.clone()).await?; - - return Ok(AddAgentResult::success( - format!("Agent {} added successfully", agent_id), - current_count + 1, - "default".to_string(), - false, - )); - } - - // Phase 2: Check enhanced limits via API - if let Some(limit_info) = self.check_enhanced_limits().await? { - if current_count >= limit_info.limit { - let _oldest_agent = self.get_oldest_agent().await?; - - return Ok(AddAgentResult::error( - format!("Maximum {} agents allowed", limit_info.limit), - "DATABASE_FULL".to_string(), - ).with_capacity_info(limit_info.limit, limit_info.limit.saturating_sub(current_count))); - } - - self.insert_agent(agent.clone()).await?; - - return Ok(AddAgentResult::success( - format!("Agent added with enhanced limits"), - current_count + 1, - if limit_info.enhanced { "enhanced" } else { "default" }.to_string(), - true, - ).with_capacity_info(limit_info.limit, limit_info.limit.saturating_sub(current_count + 1))); - } - - // Fallback: Use default limits - if current_count >= default_limit { - return Ok(AddAgentResult::error( - format!("Maximum {} agents allowed", default_limit), - "DATABASE_FULL".to_string(), - )); - } - - self.insert_agent(agent.clone()).await?; - Ok(AddAgentResult::success( - "Agent added successfully".to_string(), - current_count + 1, - "default".to_string(), - false, - )) - } - - /// Get an agent by ID - pub async fn get_agent(&self, agent_id: &str) -> RunAgentResult> { - let row = sqlx::query("SELECT * FROM agents WHERE agent_id = ?") - .bind(agent_id) - .fetch_optional(self.manager.pool()) - .await - .map_err(|e| RunAgentError::database(format!("Failed to get agent: {}", e)))?; - - if let Some(row) = row { - Ok(Some(Agent::from_row(&row)?)) - } else { - Ok(None) - } - } - - /// List all agents - pub async fn list_agents(&self) -> RunAgentResult> { - let rows = sqlx::query("SELECT * FROM agents ORDER BY deployed_at DESC") - .fetch_all(self.manager.pool()) - .await - .map_err(|e| RunAgentError::database(format!("Failed to list agents: {}", e)))?; - - let mut agents = Vec::new(); - for row in rows { - agents.push(Agent::from_row(&row)?); - } - - Ok(agents) - } - - /// Update agent status - pub async fn update_agent_status(&self, agent_id: &str, status: &str) -> RunAgentResult { - let result = sqlx::query("UPDATE agents SET status = ?, updated_at = ? WHERE agent_id = ?") - .bind(status) - .bind(Utc::now()) - .bind(agent_id) - .execute(self.manager.pool()) - .await - .map_err(|e| RunAgentError::database(format!("Failed to update agent status: {}", e)))?; - - Ok(result.rows_affected() > 0) - } - - /// Replace an existing agent - pub async fn replace_agent( - &self, - old_agent_id: &str, - new_agent: Agent, - ) -> RunAgentResult { - // Use a transaction to ensure atomicity - let mut tx = self.manager.pool().begin().await - .map_err(|e| RunAgentError::database(format!("Failed to begin transaction: {}", e)))?; - - // Delete old agent - sqlx::query("DELETE FROM agents WHERE agent_id = ?") - .bind(old_agent_id) - .execute(&mut *tx) - .await - .map_err(|e| RunAgentError::database(format!("Failed to delete old agent: {}", e)))?; - - // Insert new agent - sqlx::query( - "INSERT INTO agents (agent_id, agent_path, host, port, framework, status, deployed_at, created_at, updated_at) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)" - ) - .bind(&new_agent.agent_id) - .bind(&new_agent.agent_path) - .bind(&new_agent.host) - .bind(new_agent.port) - .bind(&new_agent.framework) - .bind(&new_agent.status) - .bind(new_agent.deployed_at) - .bind(new_agent.created_at) - .bind(new_agent.updated_at) - .execute(&mut *tx) - .await - .map_err(|e| RunAgentError::database(format!("Failed to insert new agent: {}", e)))?; - - // Commit transaction - tx.commit().await - .map_err(|e| RunAgentError::database(format!("Failed to commit transaction: {}", e)))?; - - Ok(true) - } - - /// Record an agent run - pub async fn record_agent_run(&self, run: AgentRun) -> RunAgentResult { - let result = sqlx::query( - "INSERT INTO agent_runs (agent_id, input_data, output_data, success, error_message, execution_time, started_at, completed_at) - VALUES (?, ?, ?, ?, ?, ?, ?, ?) - RETURNING id" - ) - .bind(&run.agent_id) - .bind(&run.input_data) - .bind(&run.output_data) - .bind(run.success) - .bind(&run.error_message) - .bind(run.execution_time) - .bind(run.started_at) - .bind(run.completed_at) - .fetch_one(self.manager.pool()) - .await - .map_err(|e| RunAgentError::database(format!("Failed to record agent run: {}", e)))?; - - let run_id: i64 = result.get(0); - - // Update agent statistics - self.update_agent_stats(&run.agent_id, run.success, run.execution_time).await?; - - Ok(run_id) - } - - /// Get capacity information - pub async fn get_capacity_info(&self) -> RunAgentResult { - let current_count = self.get_agent_count().await?; - let default_limit = self.get_default_limit(); - - let limit_info = self.check_enhanced_limits().await?.unwrap_or_else(|| LimitInfo { - limit: default_limit, - enhanced: false, - source: "default".to_string(), - ..Default::default() + let db_path = db_path.unwrap_or_else(|| { + LOCAL_CACHE_DIRECTORY.join(DATABASE_FILE_NAME) }); - let agents = self.list_agents().await?; - let agent_summaries: Vec = agents.into_iter().map(|a| a.into()).collect(); - - Ok(CapacityInfo { - current_count, - max_capacity: limit_info.limit, - default_limit, - remaining_slots: if limit_info.unlimited { - None - } else { - Some(limit_info.limit.saturating_sub(current_count)) - }, - is_full: current_count >= limit_info.limit, - agents: agent_summaries.clone(), - oldest_agent: agent_summaries.first().cloned(), - newest_agent: agent_summaries.last().cloned(), - limit_info, - rest_client_configured: self.rest_client.is_some(), - }) - } + // Ensure directory exists + if let Some(parent) = db_path.parent() { + std::fs::create_dir_all(parent) + .map_err(|e| RunAgentError::database(format!("Failed to create db directory: {}", e)))?; + } - /// Cleanup old runs - pub async fn cleanup_old_runs(&self, days_old: i32) -> RunAgentResult { - let cutoff_date = Utc::now() - Duration::days(days_old as i64); + let database_url = format!("sqlite:{}", db_path.display()); - let result = sqlx::query("DELETE FROM agent_runs WHERE started_at < ?") - .bind(cutoff_date) - .execute(self.manager.pool()) - .await - .map_err(|e| RunAgentError::database(format!("Failed to cleanup old runs: {}", e)))?; + let pool = SqlitePool::connect(&database_url).await + .map_err(|e| RunAgentError::database(format!("Failed to connect to database: {}", e)))?; - Ok(result.rows_affected() as usize) - } + // Initialize database schema + Self::init_schema(&pool).await?; - /// Get database statistics - pub async fn get_stats(&self) -> RunAgentResult { - self.manager.get_stats().await + Ok(Self { pool }) } - // Private helper methods - - async fn insert_agent(&self, agent: Agent) -> RunAgentResult<()> { + /// Initialize database schema + async fn init_schema(pool: &SqlitePool) -> RunAgentResult<()> { sqlx::query( - "INSERT INTO agents (agent_id, agent_path, host, port, framework, status, deployed_at, created_at, updated_at) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)" + r#" + CREATE TABLE IF NOT EXISTS agents ( + agent_id TEXT PRIMARY KEY, + agent_path TEXT NOT NULL, + host TEXT NOT NULL DEFAULT 'localhost', + port INTEGER NOT NULL DEFAULT 8450, + framework TEXT, + status TEXT DEFAULT 'deployed', + is_local INTEGER DEFAULT 1, + fingerprint TEXT, + deployed_at TEXT, + created_at TEXT DEFAULT CURRENT_TIMESTAMP, + updated_at TEXT DEFAULT CURRENT_TIMESTAMP + ) + "#, ) - .bind(&agent.agent_id) - .bind(&agent.agent_path) - .bind(&agent.host) - .bind(agent.port) - .bind(&agent.framework) - .bind(&agent.status) - .bind(agent.deployed_at) - .bind(agent.created_at) - .bind(agent.updated_at) - .execute(self.manager.pool()) + .execute(pool) .await - .map_err(|e| RunAgentError::database(format!("Failed to insert agent: {}", e)))?; + .map_err(|e| RunAgentError::database(format!("Failed to create schema: {}", e)))?; Ok(()) } - async fn get_agent_count(&self) -> RunAgentResult { - let count: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM agents") - .fetch_one(self.manager.pool()) - .await - .map_err(|e| RunAgentError::database(format!("Failed to get agent count: {}", e)))?; - - Ok(count as usize) - } - - async fn get_oldest_agent(&self) -> RunAgentResult> { - let row = sqlx::query("SELECT * FROM agents ORDER BY deployed_at ASC LIMIT 1") - .fetch_optional(self.manager.pool()) - .await - .map_err(|e| RunAgentError::database(format!("Failed to get oldest agent: {}", e)))?; + /// Get agent by ID + pub async fn get_agent(&self, agent_id: &str) -> RunAgentResult> { + let row = sqlx::query( + "SELECT agent_id, agent_path, host, port, framework, status FROM agents WHERE agent_id = ?" + ) + .bind(agent_id) + .fetch_optional(&self.pool) + .await + .map_err(|e| RunAgentError::database(format!("Failed to query agent: {}", e)))?; if let Some(row) = row { - let agent = Agent::from_row(&row)?; - Ok(Some(agent.into())) + Ok(Some(AgentInfo { + agent_id: row.get("agent_id"), + agent_path: row.get("agent_path"), + host: row.get("host"), + port: row.get("port"), + framework: row.get::, _>("framework"), + status: row.get::, _>("status"), + })) } else { Ok(None) } } - async fn update_agent_stats( - &self, - agent_id: &str, - success: bool, - _execution_time: Option, - ) -> RunAgentResult<()> { - let mut query_str = "UPDATE agents SET run_count = run_count + 1, last_run = ?, updated_at = ?".to_string(); - - if success { - query_str.push_str(", success_count = success_count + 1"); + /// Get agent address (host, port) by ID + pub async fn get_agent_address(&self, agent_id: &str) -> RunAgentResult> { + if let Some(agent) = self.get_agent(agent_id).await? { + Ok(Some((agent.host, agent.port as u16))) } else { - query_str.push_str(", error_count = error_count + 1"); - } - - query_str.push_str(" WHERE agent_id = ?"); - - sqlx::query(&query_str) - .bind(Utc::now()) - .bind(Utc::now()) - .bind(agent_id) - .execute(self.manager.pool()) - .await - .map_err(|e| RunAgentError::database(format!("Failed to update agent stats: {}", e)))?; - - Ok(()) - } - - async fn check_enhanced_limits(&self) -> RunAgentResult> { - // Check cache first - if let (Some(limits), Some(expiry)) = (&self.limits_cache, &self.cache_expiry) { - if Utc::now() < *expiry { - return Ok(Some(limits.clone())); - } - } - - if let Some(rest_client) = &self.rest_client { - match rest_client.get_local_db_limits().await { - Ok(response) => { - if response.get("success").and_then(|v| v.as_bool()).unwrap_or(false) { - let max_agents = response.get("max_agents").and_then(|v| v.as_i64()).unwrap_or(5) as usize; - let enhanced = response.get("enhanced_limits").and_then(|v| v.as_bool()).unwrap_or(false); - let unlimited = max_agents == 999; - - let limit_info = LimitInfo { - limit: max_agents, - enhanced, - source: if enhanced { "api" } else { "default" }.to_string(), - api_available: true, - api_validated: response.get("api_validated").and_then(|v| v.as_bool()).unwrap_or(false), - tier_info: response.get("tier_info").cloned(), - features: response.get("features") - .and_then(|v| v.as_array()) - .map(|arr| arr.iter().filter_map(|v| v.as_str().map(|s| s.to_string())).collect()) - .unwrap_or_default(), - expires_at: response.get("expires_at").and_then(|v| v.as_str()).map(|s| s.to_string()), - unlimited, - error: None, - }; - - // Cache for 5 minutes - // Note: In a real implementation, we'd need to handle mutable self properly - - return Ok(Some(limit_info)); - } - } - Err(_) => { - // API call failed, use default limits - } - } + Ok(None) } - - Ok(None) - } - - fn get_default_limit(&self) -> usize { - 5 // Default limit from constants } } -#[cfg(test)] -mod tests { - use super::*; - use tempfile::TempDir; - - #[tokio::test] - async fn test_database_service_creation() { - let temp_dir = TempDir::new().unwrap(); - let db_path = temp_dir.path().join("test.db"); - - let service = DatabaseService::new(Some(db_path)).await; - assert!(service.is_ok()); +impl Drop for DatabaseService { + fn drop(&mut self) { + // Note: sqlx pool handles cleanup automatically } +} - #[tokio::test] - async fn test_add_agent() { - let temp_dir = TempDir::new().unwrap(); - let db_path = temp_dir.path().join("test.db"); - - let service = DatabaseService::new(Some(db_path)).await.unwrap(); - - let agent = Agent::new( - "test-agent".to_string(), - "/path/to/agent".to_string(), - "localhost".to_string(), - 8450, - ); - - let result = service.add_agent(agent).await; - assert!(result.is_ok()); - } -} \ No newline at end of file diff --git a/runagent-rust/runagent/src/lib.rs b/runagent-rust/runagent/src/lib.rs index 5a1ccc9..a525854 100644 --- a/runagent-rust/runagent/src/lib.rs +++ b/runagent-rust/runagent/src/lib.rs @@ -5,11 +5,8 @@ //! //! ## Features //! -//! - **Multi-Framework Support**: Built-in support for LangChain, LangGraph, LlamaIndex, Letta, CrewAI, and AutoGen -//! - **Local & Remote Deployment**: Deploy agents locally for testing or to remote servers +//! - **Client SDK**: REST and WebSocket clients for interacting with deployed agents //! - **Real-time Streaming**: WebSocket-based streaming for real-time agent interactions -//! - **Database Management**: SQLite-based storage for agent metadata and execution history -//! - **Template System**: Pre-built templates for quick agent setup //! - **Type Safety**: Full Rust type safety with comprehensive error handling //! - **Async/Await**: Built on Tokio for high-performance async operations //! @@ -68,26 +65,28 @@ //! } //! ``` //! -//! ### Local Server Setup +//! ### Connecting to Local Agents //! //! ```rust,no_run -//! use runagent::server::LocalServer; -//! use std::path::PathBuf; +//! use runagent::prelude::*; +//! use serde_json::json; //! //! #[tokio::main] //! async fn main() -> Result<(), Box> { -//! // Create a local server for testing -//! let server = LocalServer::from_path( -//! PathBuf::from("./my-agent"), +//! // Connect to a local agent running on localhost:8450 +//! let client = RunAgentClient::with_address( +//! "my-agent-id", +//! "generic", +//! true, //! Some("127.0.0.1"), //! Some(8450) //! ).await?; //! -//! println!("Server info: {:?}", server.get_info()); -//! -//! // Start the server (this will block) -//! server.start().await?; +//! let response = client.run(&[ +//! ("message", json!("Hello, world!")) +//! ]).await?; //! +//! println!("Response: {}", response); //! Ok(()) //! } //! ``` @@ -176,37 +175,6 @@ //! .build(); //! ``` //! -//! ## Database Management -//! -//! ```rust,no_run -//! use runagent::db::{DatabaseService, models::Agent}; -//! -//! #[tokio::main] -//! async fn main() -> Result<(), Box> { -//! // Initialize database service -//! let db_service = DatabaseService::new(None).await?; -//! -//! // Create a new agent record -//! let agent = Agent::new( -//! "my-agent".to_string(), -//! "/path/to/agent".to_string(), -//! "localhost".to_string(), -//! 8450 -//! ).with_framework("langchain".to_string()); -//! -//! // Add agent to database -//! let result = db_service.add_agent(agent).await?; -//! println!("Agent added: {:?}", result); -//! -//! // List all agents -//! let agents = db_service.list_agents().await?; -//! for agent in agents { -//! println!("Agent: {} ({}:{})", agent.agent_id, agent.host, agent.port); -//! } -//! -//! Ok(()) -//! } -//! ``` //! //! ## Error Handling //! @@ -249,40 +217,20 @@ //! } //! ``` //! -//! ## Features -//! -//! The SDK supports optional features that can be enabled/disabled: -//! -//! ```toml -//! [dependencies] -//! runagent = { version = "0.1.0", features = ["db", "server"] } -//! ``` -//! -//! Available features: -//! - `db` (default): Database functionality for agent metadata storage -//! - `server` (default): Local server capabilities for testing -//! //! ## Architecture Overview //! -//! The RunAgent SDK is built around several core components: +//! The RunAgent SDK focuses on client-side functionality for interacting with agents: //! -//! - **Client Components**: High-level clients for interacting with deployed agents -//! - **Local Server**: FastAPI-like local server for testing agents -//! - **Database Management**: SQLite-based storage for agent metadata -//! - **Multi-Framework Support**: Support for LangChain, LangGraph, LlamaIndex, and more +//! - **Client Components**: High-level REST and WebSocket clients for interacting with deployed agents +//! - **Configuration Management**: Environment-based and programmatic configuration //! - **Streaming Support**: WebSocket-based streaming for real-time agent interactions -//! -//! Each component is designed to work independently or together, allowing you to use -//! only the parts you need for your specific use case. +//! - **Type Safety**: Comprehensive error handling and type definitions pub mod client; pub mod constants; pub mod types; pub mod utils; -#[cfg(feature = "server")] -pub mod server; - #[cfg(feature = "db")] pub mod db; @@ -290,11 +238,8 @@ pub mod db; pub use client::{RunAgentClient, RestClient, SocketClient}; pub use types::{RunAgentError, RunAgentResult}; -#[cfg(feature = "server")] -pub use server::LocalServer; - #[cfg(feature = "db")] -pub use db::{DatabaseService, DatabaseManager}; +pub use db::DatabaseService; // Version information pub const VERSION: &str = env!("CARGO_PKG_VERSION"); @@ -322,7 +267,7 @@ pub fn init_logging() { /// Configuration builder for the RunAgent SDK /// /// Provides a fluent interface for configuring the SDK with various options -/// including API keys, base URLs, database paths, and logging. +/// including API keys, base URLs, and logging. /// /// # Example /// @@ -341,8 +286,6 @@ pub struct RunAgentConfig { pub api_key: Option, /// Base URL for API endpoints pub base_url: Option, - /// Path to local database file - pub local_db_path: Option, /// Whether to enable logging pub enable_logging: bool, } @@ -394,26 +337,6 @@ impl RunAgentConfig { self } - /// Set the local database path - /// - /// # Arguments - /// - /// * `path` - Path to the database file - /// - /// # Example - /// - /// ```rust - /// use runagent::RunAgentConfig; - /// use std::path::PathBuf; - /// - /// let config = RunAgentConfig::new() - /// .with_local_db_path(PathBuf::from("./my_agents.db")); - /// ``` - pub fn with_local_db_path>(mut self, path: P) -> Self { - self.local_db_path = Some(path.into()); - self - } - /// Enable logging initialization /// /// When enabled, the `build()` method will automatically initialize @@ -468,11 +391,8 @@ pub mod prelude { pub use crate::types::{RunAgentError, RunAgentResult}; pub use crate::RunAgentConfig; - #[cfg(feature = "server")] - pub use crate::server::LocalServer; - #[cfg(feature = "db")] - pub use crate::db::{DatabaseService, DatabaseManager}; + pub use crate::db::DatabaseService; } #[cfg(test)] diff --git a/runagent-rust/runagent/src/server/framework/generic.rs b/runagent-rust/runagent/src/server/framework/generic.rs deleted file mode 100644 index ca56061..0000000 --- a/runagent-rust/runagent/src/server/framework/generic.rs +++ /dev/null @@ -1,421 +0,0 @@ -//! Generic executor for any Python framework -//! -//! This executor provides a framework-agnostic way to execute Python functions -//! and can be used as a fallback for any AI agent framework. - -use super::FrameworkExecutor; -use crate::types::{RunAgentError, RunAgentResult, EntryPoint}; -use crate::utils::imports::ImportResolver; -use futures::Stream; -use serde_json::Value; -use std::collections::HashMap; -use std::path::{Path, PathBuf}; -use std::pin::Pin; - -/// Generic executor for any Python framework -pub struct GenericExecutor { - agent_dir: PathBuf, - import_resolver: ImportResolver, - reserved_tags: Vec, -} - -impl GenericExecutor { - /// Create a new generic executor - pub fn new>(agent_dir: P) -> RunAgentResult { - let agent_dir = agent_dir.as_ref().to_path_buf(); - let import_resolver = ImportResolver::new(&agent_dir)?; - - Ok(Self { - agent_dir, - import_resolver, - reserved_tags: vec![], // No reserved tags for generic - }) - } - - /// Create a new generic executor with verbose logging - pub fn with_verbose>(agent_dir: P, verbose: bool) -> RunAgentResult { - let agent_dir = agent_dir.as_ref().to_path_buf(); - let import_resolver = ImportResolver::with_verbose(&agent_dir, verbose)?; - - Ok(Self { - agent_dir, - import_resolver, - reserved_tags: vec![], - }) - } - - /// Resolve an entrypoint to its executable function reference - fn resolve_entrypoint(&self, entrypoint: &EntryPoint) -> RunAgentResult { - let entrypoint_filepath = self.agent_dir.join(&entrypoint.file); - - if !entrypoint_filepath.exists() { - return Err(RunAgentError::validation(format!( - "Entrypoint file not found: {}", - entrypoint_filepath.display() - ))); - } - - // Split module into primary and secondary attributes - let module_parts: Vec<&str> = entrypoint.module.split('.').collect(); - let primary_module = module_parts[0]; - let secondary_attrs = &module_parts[1..]; - - // Create the function reference path - let mut function_ref = primary_module.to_string(); - for attr in secondary_attrs { - function_ref.push('.'); - function_ref.push_str(attr); - } - - // In a real implementation, this would validate that the function exists - // For now, we just return the constructed reference - tracing::debug!( - "Resolved entrypoint {} -> {} in file {}", - entrypoint.tag, - function_ref, - entrypoint_filepath.display() - ); - - Ok(function_ref) - } - - /// Create a mock execution result - fn create_execution_result( - &self, - entrypoint: &EntryPoint, - function_ref: &str, - input_args: &[Value], - input_kwargs: &HashMap, - ) -> Value { - serde_json::json!({ - "success": true, - "framework": "generic", - "executor": "GenericExecutor", - "entrypoint": { - "tag": entrypoint.tag, - "file": entrypoint.file, - "module": entrypoint.module, - "function_ref": function_ref - }, - "input": { - "args": input_args, - "kwargs": input_kwargs - }, - "result": { - "type": "mock_response", - "content": "This is a mock response from the Generic executor. In a real implementation, this would execute the Python function using PyO3 or subprocess.", - "timestamp": chrono::Utc::now().to_rfc3339(), - "agent_dir": self.agent_dir.to_string_lossy() - }, - "metadata": { - "note": "Python integration via PyO3 needed for actual execution", - "execution_method": "mock", - "rust_version": env!("CARGO_PKG_VERSION") - } - }) - } - - /// Get the runner function for a specific entrypoint - pub fn get_runner(&self, entrypoint: &EntryPoint) -> RunAgentResult) -> RunAgentResult> { - let function_ref = self.resolve_entrypoint(entrypoint)?; - let entrypoint_clone = entrypoint.clone(); - - Ok(move |input_args: &[Value], input_kwargs: &HashMap| -> RunAgentResult { - tracing::debug!("Executing non-streaming entrypoint: {}", entrypoint_clone.tag); - - // In a real implementation, this would: - // 1. Set up Python environment - // 2. Import the module - // 3. Call the function with input_args and input_kwargs - // 4. Return the result - - // For now, return a mock result - Ok(serde_json::json!({ - "success": true, - "result": format!("Mock execution of {} with {} args and {} kwargs", - function_ref, input_args.len(), input_kwargs.len()), - "function": function_ref, - "entrypoint": entrypoint_clone.tag - })) - }) - } - - /// Get the streaming runner function for a specific entrypoint - pub fn get_stream_runner(&self, entrypoint: &EntryPoint) -> RunAgentResult) -> Pin> + Send>>> { - let function_ref = self.resolve_entrypoint(entrypoint)?; - let entrypoint_clone = entrypoint.clone(); - - Ok(move |input_args: &[Value], input_kwargs: &HashMap| -> Pin> + Send>> { - use futures::stream; - - tracing::debug!("Executing streaming entrypoint: {}", entrypoint_clone.tag); - - // Create a mock stream that yields several chunks - let chunks = vec![ - Ok(serde_json::json!({ - "chunk_id": 1, - "type": "start", - "data": "Starting execution...", - "function": function_ref, - "entrypoint": entrypoint_clone.tag - })), - Ok(serde_json::json!({ - "chunk_id": 2, - "type": "processing", - "data": "Processing input...", - "input_summary": { - "args_count": input_args.len(), - "kwargs_count": input_kwargs.len() - } - })), - Ok(serde_json::json!({ - "chunk_id": 3, - "type": "progress", - "data": "Mock streaming progress...", - "progress": 50 - })), - Ok(serde_json::json!({ - "chunk_id": 4, - "type": "result", - "data": format!("Mock streaming result from {}", function_ref), - "progress": 100 - })), - Ok(serde_json::json!({ - "chunk_id": 5, - "type": "complete", - "data": "Stream completed", - "final": true - })), - ]; - - Box::pin(stream::iter(chunks)) - }) - } -} - -impl FrameworkExecutor for GenericExecutor { - fn execute( - &self, - entrypoint: &EntryPoint, - input_args: &[Value], - input_kwargs: &HashMap, - ) -> RunAgentResult { - let function_ref = self.resolve_entrypoint(entrypoint)?; - - tracing::info!("Executing generic entrypoint: {} -> {}", entrypoint.tag, function_ref); - - // Create execution result - Ok(self.create_execution_result(entrypoint, &function_ref, input_args, input_kwargs)) - } - - fn execute_stream( - &self, - entrypoint: &EntryPoint, - input_args: &[Value], - input_kwargs: &HashMap, - ) -> RunAgentResult> + Send>>> { - use futures::stream; - - let function_ref = self.resolve_entrypoint(entrypoint)?; - - tracing::info!("Executing generic streaming entrypoint: {} -> {}", entrypoint.tag, function_ref); - - // Create a mock stream - let entrypoint_clone = entrypoint.clone(); - let function_ref_clone = function_ref.clone(); - let input_args_clone = input_args.to_vec(); - let input_kwargs_clone = input_kwargs.clone(); - - let stream = stream::iter(vec![ - Ok(serde_json::json!({ - "chunk": 1, - "type": "initialization", - "data": "Initializing streaming execution...", - "entrypoint": entrypoint_clone.tag, - "function": function_ref_clone - })), - Ok(serde_json::json!({ - "chunk": 2, - "type": "input_processing", - "data": "Processing input arguments...", - "input_summary": { - "args": input_args_clone.len(), - "kwargs": input_kwargs_clone.len() - } - })), - Ok(serde_json::json!({ - "chunk": 3, - "type": "execution", - "data": "Mock streaming execution in progress...", - "progress": 33 - })), - Ok(serde_json::json!({ - "chunk": 4, - "type": "execution", - "data": "Continuing mock execution...", - "progress": 66 - })), - Ok(serde_json::json!({ - "chunk": 5, - "type": "result", - "data": format!("Mock streaming result from {}", function_ref), - "progress": 100 - })), - Ok(serde_json::json!({ - "chunk": 6, - "type": "completion", - "data": "Streaming execution completed", - "final": true, - "metadata": { - "total_chunks": 6, - "execution_method": "mock" - } - })), - ]); - - Ok(Box::pin(stream)) - } - - fn get_entrypoints(&self) -> Vec { - // Generic executor supports any entrypoint - vec![ - "generic".to_string(), - "generic_stream".to_string(), - "run".to_string(), - "run_stream".to_string(), - "main".to_string(), - "execute".to_string(), - "process".to_string(), - ] - } - - fn framework_name(&self) -> &'static str { - "generic" - } - - fn supports_entrypoint(&self, _entrypoint: &EntryPoint) -> bool { - // Generic executor supports any entrypoint - true - } -} - -#[cfg(test)] -mod tests { - use super::*; - use tempfile::TempDir; - use std::fs; - - fn create_test_agent() -> TempDir { - let temp_dir = TempDir::new().unwrap(); - let agent_path = temp_dir.path(); - - // Create a simple main.py file - fs::write( - agent_path.join("main.py"), - r#" -def run(*args, **kwargs): - return {"result": "Hello from Python!", "args": args, "kwargs": kwargs} - -def run_stream(*args, **kwargs): - for i in range(3): - yield {"chunk": i, "data": f"Streaming chunk {i}"} -"#, - ).unwrap(); - - temp_dir - } - - #[test] - fn test_generic_executor_creation() { - let temp_dir = TempDir::new().unwrap(); - let executor = GenericExecutor::new(temp_dir.path()); - assert!(executor.is_ok()); - } - - #[test] - fn test_entrypoint_resolution() { - let temp_dir = create_test_agent(); - let executor = GenericExecutor::new(temp_dir.path()).unwrap(); - - let entrypoint = EntryPoint { - file: "main.py".to_string(), - module: "run".to_string(), - tag: "generic".to_string(), - }; - - let result = executor.resolve_entrypoint(&entrypoint); - assert!(result.is_ok()); - assert_eq!(result.unwrap(), "run"); - } - - #[test] - fn test_execution() { - let temp_dir = create_test_agent(); - let executor = GenericExecutor::new(temp_dir.path()).unwrap(); - - let entrypoint = EntryPoint { - file: "main.py".to_string(), - module: "run".to_string(), - tag: "generic".to_string(), - }; - - let input_args = vec![serde_json::json!("test")]; - let mut input_kwargs = HashMap::new(); - input_kwargs.insert("key".to_string(), serde_json::json!("value")); - - let result = executor.execute(&entrypoint, &input_args, &input_kwargs); - assert!(result.is_ok()); - - let response = result.unwrap(); - assert!(response.get("success").and_then(|v| v.as_bool()).unwrap_or(false)); - } - - #[test] - fn test_streaming_execution() { - let temp_dir = create_test_agent(); - let executor = GenericExecutor::new(temp_dir.path()).unwrap(); - - let entrypoint = EntryPoint { - file: "main.py".to_string(), - module: "run_stream".to_string(), - tag: "generic_stream".to_string(), - }; - - let input_args = vec![]; - let input_kwargs = HashMap::new(); - - let result = executor.execute_stream(&entrypoint, &input_args, &input_kwargs); - assert!(result.is_ok()); - - // In a real test, we would consume the stream and verify chunks - // For now, just verify that we get a stream back - } - - #[test] - fn test_framework_name() { - let temp_dir = TempDir::new().unwrap(); - let executor = GenericExecutor::new(temp_dir.path()).unwrap(); - assert_eq!(executor.framework_name(), "generic"); - } - - #[test] - fn test_supports_any_entrypoint() { - let temp_dir = TempDir::new().unwrap(); - let executor = GenericExecutor::new(temp_dir.path()).unwrap(); - - let entrypoint = EntryPoint { - file: "main.py".to_string(), - module: "any_function".to_string(), - tag: "custom_tag".to_string(), - }; - - assert!(executor.supports_entrypoint(&entrypoint)); - } - - #[test] - fn test_verbose_mode() { - let temp_dir = TempDir::new().unwrap(); - let executor = GenericExecutor::with_verbose(temp_dir.path(), true); - assert!(executor.is_ok()); - } -} \ No newline at end of file diff --git a/runagent-rust/runagent/src/server/framework/langchain.rs b/runagent-rust/runagent/src/server/framework/langchain.rs deleted file mode 100644 index 418a577..0000000 --- a/runagent-rust/runagent/src/server/framework/langchain.rs +++ /dev/null @@ -1,515 +0,0 @@ -//! LangChain-specific executor -//! -//! This executor provides specialized handling for LangChain agents and chains, -//! with support for common LangChain patterns like invoke, stream, and batch operations. - -use super::{FrameworkExecutor, GenericExecutor}; -use crate::types::{RunAgentError, RunAgentResult, EntryPoint}; -use futures::Stream; -use serde_json::Value; -use std::collections::HashMap; -use std::path::{Path, PathBuf}; -use std::pin::Pin; - -/// LangChain-specific executor with support for LangChain patterns -pub struct LangChainExecutor { - /// Generic executor for fallback functionality - generic: GenericExecutor, - /// Agent directory - agent_dir: PathBuf, - /// Reserved LangChain entrypoint tags - reserved_tags: Vec, -} - -impl LangChainExecutor { - /// Create a new LangChain executor - pub fn new>(agent_dir: P) -> RunAgentResult { - let agent_dir = agent_dir.as_ref().to_path_buf(); - let generic = GenericExecutor::new(&agent_dir)?; - - Ok(Self { - generic, - agent_dir, - reserved_tags: vec![ - "invoke".to_string(), - "stream".to_string(), - "stream_token".to_string(), - "batch".to_string(), - "ainvoke".to_string(), - "astream".to_string(), - "abatch".to_string(), - ], - }) - } - - /// Create a new LangChain executor with verbose logging - pub fn with_verbose>(agent_dir: P, verbose: bool) -> RunAgentResult { - let agent_dir = agent_dir.as_ref().to_path_buf(); - let generic = GenericExecutor::with_verbose(&agent_dir, verbose)?; - - Ok(Self { - generic, - agent_dir, - reserved_tags: vec![ - "invoke".to_string(), - "stream".to_string(), - "stream_token".to_string(), - "batch".to_string(), - "ainvoke".to_string(), - "astream".to_string(), - "abatch".to_string(), - ], - }) - } - - /// Handle LangChain-specific invoke pattern - fn handle_invoke( - &self, - entrypoint: &EntryPoint, - input_args: &[Value], - input_kwargs: &HashMap, - ) -> RunAgentResult { - tracing::info!("Executing LangChain invoke: {}", entrypoint.tag); - - // LangChain invoke typically takes a single input or a dict - let langchain_input = if !input_kwargs.is_empty() { - // Use kwargs as the input dict - Value::Object(input_kwargs.iter() - .map(|(k, v)| (k.clone(), v.clone())) - .collect()) - } else if !input_args.is_empty() { - // Use first arg as input - input_args[0].clone() - } else { - Value::Object(serde_json::Map::new()) - }; - - // Execute with generic executor but add LangChain-specific metadata - let mut result = self.generic.execute(entrypoint, input_args, input_kwargs)?; - - if let Some(obj) = result.as_object_mut() { - obj.insert("framework".to_string(), Value::String("langchain".to_string())); - obj.insert("method".to_string(), Value::String("invoke".to_string())); - obj.insert("langchain_input".to_string(), langchain_input); - - // Add LangChain-specific result structure - obj.insert("langchain_result".to_string(), serde_json::json!({ - "type": "invoke_response", - "content": "Mock LangChain invoke response", - "usage_metadata": { - "input_tokens": 10, - "output_tokens": 20, - "total_tokens": 30 - }, - "response_metadata": { - "model_name": "mock-model", - "system_fingerprint": "mock-fingerprint" - } - })); - } - - Ok(result) - } - - /// Handle LangChain-specific streaming patterns - fn handle_stream( - &self, - entrypoint: &EntryPoint, - input_args: &[Value], - input_kwargs: &HashMap, - ) -> RunAgentResult> + Send>>> { - use futures::stream; - - tracing::info!("Executing LangChain stream: {}", entrypoint.tag); - - let langchain_input = if !input_kwargs.is_empty() { - Value::Object(input_kwargs.iter() - .map(|(k, v)| (k.clone(), v.clone())) - .collect()) - } else if !input_args.is_empty() { - input_args[0].clone() - } else { - Value::Object(serde_json::Map::new()) - }; - - // Create LangChain-specific streaming response - let chunks = vec![ - Ok(serde_json::json!({ - "type": "langchain_stream_start", - "data": { - "input": langchain_input, - "entrypoint": entrypoint.tag - }, - "metadata": { - "framework": "langchain", - "stream_type": "content" - } - })), - Ok(serde_json::json!({ - "type": "langchain_content", - "data": { - "content": "Hello", - "additional_kwargs": {}, - "response_metadata": {} - }, - "metadata": { - "chunk_index": 0 - } - })), - Ok(serde_json::json!({ - "type": "langchain_content", - "data": { - "content": " from", - "additional_kwargs": {}, - "response_metadata": {} - }, - "metadata": { - "chunk_index": 1 - } - })), - Ok(serde_json::json!({ - "type": "langchain_content", - "data": { - "content": " LangChain!", - "additional_kwargs": {}, - "response_metadata": {} - }, - "metadata": { - "chunk_index": 2 - } - })), - Ok(serde_json::json!({ - "type": "langchain_stream_end", - "data": { - "finish_reason": "stop", - "usage_metadata": { - "input_tokens": 10, - "output_tokens": 15, - "total_tokens": 25 - } - }, - "metadata": { - "framework": "langchain", - "final": true - } - })), - ]; - - Ok(Box::pin(stream::iter(chunks))) - } - - /// Handle LangChain token streaming - fn handle_stream_token( - &self, - entrypoint: &EntryPoint, - input_args: &[Value], - input_kwargs: &HashMap, - ) -> RunAgentResult> + Send>>> { - use futures::stream; - - tracing::info!("Executing LangChain token stream: {}", entrypoint.tag); - - // Token-level streaming with individual tokens - let tokens = vec!["Hello", " ", "from", " ", "Lang", "Chain", "!", " ", "This", " ", "is", " ", "token", " ", "streaming", "."]; - - let mut chunks = vec![ - Ok(serde_json::json!({ - "type": "langchain_token_stream_start", - "data": { - "input": input_kwargs, - "entrypoint": entrypoint.tag - }, - "metadata": { - "framework": "langchain", - "stream_type": "token" - } - })) - ]; - - for (i, token) in tokens.iter().enumerate() { - chunks.push(Ok(serde_json::json!({ - "type": "langchain_token", - "data": { - "token": token, - "token_index": i, - "is_final": false - }, - "metadata": { - "chunk_index": i - } - }))); - } - - chunks.push(Ok(serde_json::json!({ - "type": "langchain_token_stream_end", - "data": { - "total_tokens": tokens.len(), - "finish_reason": "stop" - }, - "metadata": { - "framework": "langchain", - "final": true - } - }))); - - Ok(Box::pin(stream::iter(chunks))) - } - - /// Handle LangChain batch operations - fn handle_batch( - &self, - entrypoint: &EntryPoint, - input_args: &[Value], - input_kwargs: &HashMap, - ) -> RunAgentResult { - tracing::info!("Executing LangChain batch: {}", entrypoint.tag); - - // Batch operations process multiple inputs - let batch_inputs = if let Some(inputs) = input_kwargs.get("inputs") { - inputs.as_array().unwrap_or(&vec![]).clone() - } else if !input_args.is_empty() && input_args[0].is_array() { - input_args[0].as_array().unwrap_or(&vec![]).clone() - } else { - vec![serde_json::json!({"default": "input"})] - }; - - let batch_results: Vec = batch_inputs.iter().enumerate().map(|(i, input)| { - serde_json::json!({ - "index": i, - "input": input, - "output": { - "content": format!("Mock batch response for input {}", i), - "metadata": { - "batch_index": i, - "processed_at": chrono::Utc::now().to_rfc3339() - } - } - }) - }).collect(); - - let mut result = self.generic.execute(entrypoint, input_args, input_kwargs)?; - - if let Some(obj) = result.as_object_mut() { - obj.insert("framework".to_string(), Value::String("langchain".to_string())); - obj.insert("method".to_string(), Value::String("batch".to_string())); - obj.insert("batch_size".to_string(), Value::Number(batch_inputs.len().into())); - obj.insert("batch_results".to_string(), Value::Array(batch_results)); - } - - Ok(result) - } -} - -impl FrameworkExecutor for LangChainExecutor { - fn execute( - &self, - entrypoint: &EntryPoint, - input_args: &[Value], - input_kwargs: &HashMap, - ) -> RunAgentResult { - match entrypoint.tag.as_str() { - "invoke" | "ainvoke" => self.handle_invoke(entrypoint, input_args, input_kwargs), - "batch" | "abatch" => self.handle_batch(entrypoint, input_args, input_kwargs), - _ => { - // Fall back to generic execution with LangChain metadata - let mut result = self.generic.execute(entrypoint, input_args, input_kwargs)?; - - if let Some(obj) = result.as_object_mut() { - obj.insert("framework".to_string(), Value::String("langchain".to_string())); - obj.insert("executor".to_string(), Value::String("LangChainExecutor".to_string())); - } - - Ok(result) - } - } - } - - fn execute_stream( - &self, - entrypoint: &EntryPoint, - input_args: &[Value], - input_kwargs: &HashMap, - ) -> RunAgentResult> + Send>>> { - match entrypoint.tag.as_str() { - "stream" | "astream" => self.handle_stream(entrypoint, input_args, input_kwargs), - "stream_token" => self.handle_stream_token(entrypoint, input_args, input_kwargs), - _ => { - // Fall back to generic streaming with LangChain metadata - self.generic.execute_stream(entrypoint, input_args, input_kwargs) - } - } - } - - fn get_entrypoints(&self) -> Vec { - vec![ - "invoke".to_string(), - "stream".to_string(), - "stream_token".to_string(), - "batch".to_string(), - "ainvoke".to_string(), - "astream".to_string(), - "abatch".to_string(), - // Include generic entrypoints as fallback - "run".to_string(), - "process".to_string(), - ] - } - - fn framework_name(&self) -> &'static str { - "langchain" - } - - fn supports_entrypoint(&self, entrypoint: &EntryPoint) -> bool { - self.reserved_tags.contains(&entrypoint.tag) || self.generic.supports_entrypoint(entrypoint) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use tempfile::TempDir; - use std::fs; - - fn create_test_langchain_agent() -> TempDir { - let temp_dir = TempDir::new().unwrap(); - let agent_path = temp_dir.path(); - - // Create a LangChain-style agent file - fs::write( - agent_path.join("agent.py"), - r#" -from langchain.chains import ConversationChain -from langchain.memory import ConversationBufferMemory -from langchain_openai import ChatOpenAI - -def invoke(input_data): - # Mock LangChain invoke - return {"output": f"LangChain response to: {input_data}"} - -def stream(input_data): - # Mock LangChain streaming - for chunk in ["Hello", " from", " LangChain!"]: - yield {"content": chunk} - -def batch(inputs): - # Mock LangChain batch - return [{"output": f"Response to {inp}"} for inp in inputs] -"#, - ).unwrap(); - - temp_dir - } - - #[test] - fn test_langchain_executor_creation() { - let temp_dir = TempDir::new().unwrap(); - let executor = LangChainExecutor::new(temp_dir.path()); - assert!(executor.is_ok()); - } - - #[test] - fn test_invoke_execution() { - let temp_dir = create_test_langchain_agent(); - let executor = LangChainExecutor::new(temp_dir.path()).unwrap(); - - let entrypoint = EntryPoint { - file: "agent.py".to_string(), - module: "invoke".to_string(), - tag: "invoke".to_string(), - }; - - let input_args = vec![]; - let mut input_kwargs = HashMap::new(); - input_kwargs.insert("input".to_string(), serde_json::json!("test message")); - - let result = executor.execute(&entrypoint, &input_args, &input_kwargs); - assert!(result.is_ok()); - - let response = result.unwrap(); - assert_eq!(response.get("framework").and_then(|v| v.as_str()), Some("langchain")); - assert_eq!(response.get("method").and_then(|v| v.as_str()), Some("batch")); - assert!(response.get("batch_results").is_some()); - } - - #[test] - fn test_framework_name() { - let temp_dir = TempDir::new().unwrap(); - let executor = LangChainExecutor::new(temp_dir.path()).unwrap(); - assert_eq!(executor.framework_name(), "langchain"); - } - - #[test] - fn test_supported_entrypoints() { - let temp_dir = TempDir::new().unwrap(); - let executor = LangChainExecutor::new(temp_dir.path()).unwrap(); - - let entrypoints = executor.get_entrypoints(); - assert!(entrypoints.contains(&"invoke".to_string())); - assert!(entrypoints.contains(&"stream".to_string())); - assert!(entrypoints.contains(&"batch".to_string())); - } - - #[test] - fn test_fallback_to_generic() { - let temp_dir = create_test_langchain_agent(); - let executor = LangChainExecutor::new(temp_dir.path()).unwrap(); - - let entrypoint = EntryPoint { - file: "agent.py".to_string(), - module: "custom_function".to_string(), - tag: "custom".to_string(), - }; - - let input_args = vec![]; - let input_kwargs = HashMap::new(); - - let result = executor.execute(&entrypoint, &input_args, &input_kwargs); - assert!(result.is_ok()); - - let response = result.unwrap(); - assert_eq!(response.get("framework").and_then(|v| v.as_str()), Some("langchain")); - assert_eq!(response.get("executor").and_then(|v| v.as_str()), Some("LangChainExecutor")); - } -}_eq!(response.get("method").and_then(|v| v.as_str()), Some("invoke")); - } - - #[test] - fn test_stream_execution() { - let temp_dir = create_test_langchain_agent(); - let executor = LangChainExecutor::new(temp_dir.path()).unwrap(); - - let entrypoint = EntryPoint { - file: "agent.py".to_string(), - module: "stream".to_string(), - tag: "stream".to_string(), - }; - - let input_args = vec![]; - let mut input_kwargs = HashMap::new(); - input_kwargs.insert("input".to_string(), serde_json::json!("test message")); - - let result = executor.execute_stream(&entrypoint, &input_args, &input_kwargs); - assert!(result.is_ok()); - } - - #[test] - fn test_batch_execution() { - let temp_dir = create_test_langchain_agent(); - let executor = LangChainExecutor::new(temp_dir.path()).unwrap(); - - let entrypoint = EntryPoint { - file: "agent.py".to_string(), - module: "batch".to_string(), - tag: "batch".to_string(), - }; - - let input_args = vec![]; - let mut input_kwargs = HashMap::new(); - input_kwargs.insert("inputs".to_string(), serde_json::json!(["input1", "input2"])); - - let result = executor.execute(&entrypoint, &input_args, &input_kwargs); - assert!(result.is_ok()); - - let response = result.unwrap(); - assert_eq!(response.get("framework").and_then(|v| v.as_str()), Some("langchain")); - assert \ No newline at end of file diff --git a/runagent-rust/runagent/src/server/framework/langgraph.rs b/runagent-rust/runagent/src/server/framework/langgraph.rs deleted file mode 100644 index 0519676..0000000 --- a/runagent-rust/runagent/src/server/framework/langgraph.rs +++ /dev/null @@ -1,647 +0,0 @@ -//! LangGraph-specific executor -//! -//! This executor provides specialized handling for LangGraph agents and workflows, -//! with support for graph execution, state management, and conditional routing. - -use super::{FrameworkExecutor, GenericExecutor}; -use crate::types::{RunAgentError, RunAgentResult, EntryPoint}; -use futures::Stream; -use serde_json::Value; -use std::collections::HashMap; -use std::path::{Path, PathBuf}; -use std::pin::Pin; - -/// LangGraph-specific executor with support for graph workflows -pub struct LangGraphExecutor { - /// Generic executor for fallback functionality - generic: GenericExecutor, - /// Agent directory - agent_dir: PathBuf, - /// Reserved LangGraph entrypoint tags - reserved_tags: Vec, -} - -impl LangGraphExecutor { - /// Create a new LangGraph executor - pub fn new>(agent_dir: P) -> RunAgentResult { - let agent_dir = agent_dir.as_ref().to_path_buf(); - let generic = GenericExecutor::new(&agent_dir)?; - - Ok(Self { - generic, - agent_dir, - reserved_tags: vec![ - "invoke".to_string(), - "stream".to_string(), - "ainvoke".to_string(), - "astream".to_string(), - "get_graph".to_string(), - "get_state".to_string(), - "update_state".to_string(), - "compile".to_string(), - ], - }) - } - - /// Create a new LangGraph executor with verbose logging - pub fn with_verbose>(agent_dir: P, verbose: bool) -> RunAgentResult { - let agent_dir = agent_dir.as_ref().to_path_buf(); - let generic = GenericExecutor::with_verbose(&agent_dir, verbose)?; - - Ok(Self { - generic, - agent_dir, - reserved_tags: vec![ - "invoke".to_string(), - "stream".to_string(), - "ainvoke".to_string(), - "astream".to_string(), - "get_graph".to_string(), - "get_state".to_string(), - "update_state".to_string(), - "compile".to_string(), - ], - }) - } - - /// Handle LangGraph invoke with graph execution simulation - fn handle_invoke( - &self, - entrypoint: &EntryPoint, - input_args: &[Value], - input_kwargs: &HashMap, - ) -> RunAgentResult { - tracing::info!("Executing LangGraph invoke: {}", entrypoint.tag); - - // LangGraph invoke processes input through a graph workflow - let graph_input = if !input_kwargs.is_empty() { - Value::Object(input_kwargs.iter() - .map(|(k, v)| (k.clone(), v.clone())) - .collect()) - } else if !input_args.is_empty() { - input_args[0].clone() - } else { - serde_json::json!({"messages": []}) - }; - - // Simulate graph execution steps - let execution_steps = vec![ - serde_json::json!({ - "node": "agent", - "action": "reasoning", - "input": graph_input, - "output": { - "thoughts": "Processing user input through graph workflow", - "action": "continue" - } - }), - serde_json::json!({ - "node": "tools", - "action": "tool_selection", - "input": { - "available_tools": ["search", "calculator", "code_executor"], - "selected_tool": "search" - }, - "output": { - "tool_result": "Mock tool execution result" - } - }), - serde_json::json!({ - "node": "final_response", - "action": "synthesis", - "input": { - "agent_thoughts": "Processing user input through graph workflow", - "tool_results": "Mock tool execution result" - }, - "output": { - "final_answer": "This is a mock LangGraph response generated through graph execution" - } - }) - ]; - - let mut result = self.generic.execute(entrypoint, input_args, input_kwargs)?; - - if let Some(obj) = result.as_object_mut() { - obj.insert("framework".to_string(), Value::String("langgraph".to_string())); - obj.insert("method".to_string(), Value::String("invoke".to_string())); - obj.insert("graph_execution".to_string(), Value::Bool(true)); - obj.insert("execution_steps".to_string(), Value::Array(execution_steps)); - obj.insert("langgraph_result".to_string(), serde_json::json!({ - "type": "graph_response", - "final_state": { - "messages": [ - { - "role": "user", - "content": graph_input - }, - { - "role": "assistant", - "content": "This is a mock LangGraph response generated through graph execution" - } - ] - }, - "graph_metadata": { - "nodes_executed": ["agent", "tools", "final_response"], - "total_steps": 3, - "execution_time": "0.5s" - } - })); - } - - Ok(result) - } - - /// Handle LangGraph streaming with step-by-step graph execution - fn handle_stream( - &self, - entrypoint: &EntryPoint, - input_args: &[Value], - input_kwargs: &HashMap, - ) -> RunAgentResult> + Send>>> { - use futures::stream; - - tracing::info!("Executing LangGraph stream: {}", entrypoint.tag); - - let graph_input = if !input_kwargs.is_empty() { - Value::Object(input_kwargs.iter() - .map(|(k, v)| (k.clone(), v.clone())) - .collect()) - } else if !input_args.is_empty() { - input_args[0].clone() - } else { - serde_json::json!({"messages": []}) - }; - - // Create streaming response that shows graph execution steps - let chunks = vec![ - Ok(serde_json::json!({ - "type": "langgraph_stream_start", - "data": { - "input": graph_input, - "graph_info": { - "nodes": ["agent", "tools", "conditional", "final_response"], - "edges": [ - {"from": "agent", "to": "conditional"}, - {"from": "conditional", "to": "tools"}, - {"from": "tools", "to": "final_response"} - ] - } - }, - "metadata": { - "framework": "langgraph", - "stream_type": "graph_execution" - } - })), - Ok(serde_json::json!({ - "type": "langgraph_node_start", - "data": { - "node": "agent", - "node_type": "agent_node", - "input": graph_input - }, - "metadata": { - "step": 1, - "node_index": 0 - } - })), - Ok(serde_json::json!({ - "type": "langgraph_node_output", - "data": { - "node": "agent", - "output": { - "thoughts": "I need to process this request step by step", - "action": "use_tools", - "reasoning": "The user query requires tool assistance" - } - }, - "metadata": { - "step": 2, - "node_index": 0 - } - })), - Ok(serde_json::json!({ - "type": "langgraph_node_start", - "data": { - "node": "conditional", - "node_type": "conditional_node", - "input": { - "agent_decision": "use_tools" - } - }, - "metadata": { - "step": 3, - "node_index": 1 - } - })), - Ok(serde_json::json!({ - "type": "langgraph_node_output", - "data": { - "node": "conditional", - "output": { - "next_node": "tools", - "condition_met": true - } - }, - "metadata": { - "step": 4, - "node_index": 1 - } - })), - Ok(serde_json::json!({ - "type": "langgraph_node_start", - "data": { - "node": "tools", - "node_type": "tool_node", - "input": { - "tool_name": "search", - "tool_args": {"query": "mock search query"} - } - }, - "metadata": { - "step": 5, - "node_index": 2 - } - })), - Ok(serde_json::json!({ - "type": "langgraph_node_output", - "data": { - "node": "tools", - "output": { - "tool_result": "Mock search results from LangGraph tool execution", - "tool_metadata": { - "execution_time": "0.2s", - "success": true - } - } - }, - "metadata": { - "step": 6, - "node_index": 2 - } - })), - Ok(serde_json::json!({ - "type": "langgraph_node_start", - "data": { - "node": "final_response", - "node_type": "response_node", - "input": { - "agent_thoughts": "I need to process this request step by step", - "tool_results": "Mock search results from LangGraph tool execution" - } - }, - "metadata": { - "step": 7, - "node_index": 3 - } - })), - Ok(serde_json::json!({ - "type": "langgraph_node_output", - "data": { - "node": "final_response", - "output": { - "final_answer": "Based on the graph execution and tool results, here is the response", - "confidence": 0.95 - } - }, - "metadata": { - "step": 8, - "node_index": 3, - "final_node": true - } - })), - Ok(serde_json::json!({ - "type": "langgraph_stream_end", - "data": { - "final_state": { - "messages": [graph_input, "Based on the graph execution and tool results, here is the response"], - "execution_complete": true - }, - "graph_metadata": { - "total_nodes_executed": 4, - "total_steps": 8, - "execution_path": ["agent", "conditional", "tools", "final_response"] - } - }, - "metadata": { - "framework": "langgraph", - "final": true - } - })), - ]; - - Ok(Box::pin(stream::iter(chunks))) - } - - /// Handle graph structure inspection - fn handle_get_graph( - &self, - entrypoint: &EntryPoint, - _input_args: &[Value], - _input_kwargs: &HashMap, - ) -> RunAgentResult { - tracing::info!("Getting LangGraph structure: {}", entrypoint.tag); - - Ok(serde_json::json!({ - "framework": "langgraph", - "method": "get_graph", - "graph_structure": { - "nodes": [ - { - "id": "agent", - "type": "agent_node", - "description": "Main reasoning agent" - }, - { - "id": "conditional", - "type": "conditional_node", - "description": "Decision routing node" - }, - { - "id": "tools", - "type": "tool_node", - "description": "Tool execution node" - }, - { - "id": "final_response", - "type": "response_node", - "description": "Final response generation" - } - ], - "edges": [ - {"from": "agent", "to": "conditional", "condition": null}, - {"from": "conditional", "to": "tools", "condition": "use_tools"}, - {"from": "conditional", "to": "final_response", "condition": "direct_response"}, - {"from": "tools", "to": "final_response", "condition": null} - ], - "entry_point": "agent", - "end_nodes": ["final_response"] - }, - "compiled": true, - "checkpointer": "memory_saver", - "interrupt_before": [], - "interrupt_after": [] - })) - } - - /// Handle state management operations - fn handle_get_state( - &self, - entrypoint: &EntryPoint, - input_args: &[Value], - input_kwargs: &HashMap, - ) -> RunAgentResult { - tracing::info!("Getting LangGraph state: {}", entrypoint.tag); - - let thread_id = input_kwargs.get("thread_id") - .or_else(|| input_args.get(0)) - .unwrap_or(&serde_json::json!("default")) - .clone(); - - Ok(serde_json::json!({ - "framework": "langgraph", - "method": "get_state", - "thread_id": thread_id, - "state": { - "messages": [ - { - "role": "user", - "content": "Previous user message" - }, - { - "role": "assistant", - "content": "Previous assistant response" - } - ], - "current_node": "agent", - "execution_metadata": { - "step_count": 5, - "last_update": chrono::Utc::now().to_rfc3339() - } - }, - "next_nodes": ["conditional"], - "checkpoint": { - "thread_id": thread_id, - "checkpoint_id": "mock-checkpoint-123", - "created_at": chrono::Utc::now().to_rfc3339() - } - })) - } -} - -impl FrameworkExecutor for LangGraphExecutor { - fn execute( - &self, - entrypoint: &EntryPoint, - input_args: &[Value], - input_kwargs: &HashMap, - ) -> RunAgentResult { - match entrypoint.tag.as_str() { - "invoke" | "ainvoke" => self.handle_invoke(entrypoint, input_args, input_kwargs), - "get_graph" => self.handle_get_graph(entrypoint, input_args, input_kwargs), - "get_state" => self.handle_get_state(entrypoint, input_args, input_kwargs), - _ => { - // Fall back to generic execution with LangGraph metadata - let mut result = self.generic.execute(entrypoint, input_args, input_kwargs)?; - - if let Some(obj) = result.as_object_mut() { - obj.insert("framework".to_string(), Value::String("langgraph".to_string())); - obj.insert("executor".to_string(), Value::String("LangGraphExecutor".to_string())); - obj.insert("graph_execution".to_string(), Value::Bool(true)); - } - - Ok(result) - } - } - } - - fn execute_stream( - &self, - entrypoint: &EntryPoint, - input_args: &[Value], - input_kwargs: &HashMap, - ) -> RunAgentResult> + Send>>> { - match entrypoint.tag.as_str() { - "stream" | "astream" => self.handle_stream(entrypoint, input_args, input_kwargs), - _ => { - // Fall back to generic streaming - self.generic.execute_stream(entrypoint, input_args, input_kwargs) - } - } - } - - fn get_entrypoints(&self) -> Vec { - vec![ - "invoke".to_string(), - "stream".to_string(), - "ainvoke".to_string(), - "astream".to_string(), - "get_graph".to_string(), - "get_state".to_string(), - "update_state".to_string(), - "compile".to_string(), - // Include generic entrypoints as fallback - "run".to_string(), - "process".to_string(), - ] - } - - fn framework_name(&self) -> &'static str { - "langgraph" - } - - fn supports_entrypoint(&self, entrypoint: &EntryPoint) -> bool { - self.reserved_tags.contains(&entrypoint.tag) || self.generic.supports_entrypoint(entrypoint) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use tempfile::TempDir; - use std::fs; - - fn create_test_langgraph_agent() -> TempDir { - let temp_dir = TempDir::new().unwrap(); - let agent_path = temp_dir.path(); - - // Create a LangGraph-style agent file - fs::write( - agent_path.join("graph.py"), - r#" -from langgraph.graph import StateGraph, END -from langgraph.checkpoint.memory import MemorySaver - -def invoke(input_data): - # Mock LangGraph invoke with graph execution - return {"output": f"LangGraph graph response to: {input_data}"} - -def stream(input_data): - # Mock LangGraph streaming with node-by-node execution - nodes = ["agent", "tools", "final_response"] - for node in nodes: - yield {"node": node, "output": f"Processing in {node}"} - -def get_graph(): - # Mock graph structure - return { - "nodes": ["agent", "tools", "final_response"], - "edges": [("agent", "tools"), ("tools", "final_response")] - } -"#, - ).unwrap(); - - temp_dir - } - - #[test] - fn test_langgraph_executor_creation() { - let temp_dir = TempDir::new().unwrap(); - let executor = LangGraphExecutor::new(temp_dir.path()); - assert!(executor.is_ok()); - } - - #[test] - fn test_invoke_execution() { - let temp_dir = create_test_langgraph_agent(); - let executor = LangGraphExecutor::new(temp_dir.path()).unwrap(); - - let entrypoint = EntryPoint { - file: "graph.py".to_string(), - module: "invoke".to_string(), - tag: "invoke".to_string(), - }; - - let input_args = vec![]; - let mut input_kwargs = HashMap::new(); - input_kwargs.insert("input".to_string(), serde_json::json!("test message")); - - let result = executor.execute(&entrypoint, &input_args, &input_kwargs); - assert!(result.is_ok()); - - let response = result.unwrap(); - assert_eq!(response.get("framework").and_then(|v| v.as_str()), Some("langgraph")); - assert_eq!(response.get("method").and_then(|v| v.as_str()), Some("invoke")); - assert_eq!(response.get("graph_execution").and_then(|v| v.as_bool()), Some(true)); - } - - #[test] - fn test_stream_execution() { - let temp_dir = create_test_langgraph_agent(); - let executor = LangGraphExecutor::new(temp_dir.path()).unwrap(); - - let entrypoint = EntryPoint { - file: "graph.py".to_string(), - module: "stream".to_string(), - tag: "stream".to_string(), - }; - - let input_args = vec![]; - let mut input_kwargs = HashMap::new(); - input_kwargs.insert("input".to_string(), serde_json::json!("test message")); - - let result = executor.execute_stream(&entrypoint, &input_args, &input_kwargs); - assert!(result.is_ok()); - } - - #[test] - fn test_get_graph() { - let temp_dir = create_test_langgraph_agent(); - let executor = LangGraphExecutor::new(temp_dir.path()).unwrap(); - - let entrypoint = EntryPoint { - file: "graph.py".to_string(), - module: "get_graph".to_string(), - tag: "get_graph".to_string(), - }; - - let result = executor.execute(&entrypoint, &[], &HashMap::new()); - assert!(result.is_ok()); - - let response = result.unwrap(); - assert_eq!(response.get("framework").and_then(|v| v.as_str()), Some("langgraph")); - assert_eq!(response.get("method").and_then(|v| v.as_str()), Some("get_graph")); - assert!(response.get("graph_structure").is_some()); - } - - #[test] - fn test_get_state() { - let temp_dir = create_test_langgraph_agent(); - let executor = LangGraphExecutor::new(temp_dir.path()).unwrap(); - - let entrypoint = EntryPoint { - file: "graph.py".to_string(), - module: "get_state".to_string(), - tag: "get_state".to_string(), - }; - - let mut input_kwargs = HashMap::new(); - input_kwargs.insert("thread_id".to_string(), serde_json::json!("test-thread")); - - let result = executor.execute(&entrypoint, &[], &input_kwargs); - assert!(result.is_ok()); - - let response = result.unwrap(); - assert_eq!(response.get("framework").and_then(|v| v.as_str()), Some("langgraph")); - assert_eq!(response.get("method").and_then(|v| v.as_str()), Some("get_state")); - assert!(response.get("state").is_some()); - } - - #[test] - fn test_framework_name() { - let temp_dir = TempDir::new().unwrap(); - let executor = LangGraphExecutor::new(temp_dir.path()).unwrap(); - assert_eq!(executor.framework_name(), "langgraph"); - } - - #[test] - fn test_supported_entrypoints() { - let temp_dir = TempDir::new().unwrap(); - let executor = LangGraphExecutor::new(temp_dir.path()).unwrap(); - - let entrypoints = executor.get_entrypoints(); - assert!(entrypoints.contains(&"invoke".to_string())); - assert!(entrypoints.contains(&"stream".to_string())); - assert!(entrypoints.contains(&"get_graph".to_string())); - assert!(entrypoints.contains(&"get_state".to_string())); - } -} \ No newline at end of file diff --git a/runagent-rust/runagent/src/server/framework/mod.rs b/runagent-rust/runagent/src/server/framework/mod.rs deleted file mode 100644 index 124dcd6..0000000 --- a/runagent-rust/runagent/src/server/framework/mod.rs +++ /dev/null @@ -1,146 +0,0 @@ -//! Framework-specific executors for different AI agent frameworks -//! -//! This module provides execution environments for various AI agent frameworks -//! including LangChain, LangGraph, LlamaIndex, and generic Python frameworks. - -pub mod generic; -pub mod langchain; -pub mod langgraph; - -// Re-export the main types and functions -pub use generic::GenericExecutor; -pub use langchain::LangChainExecutor; -pub use langgraph::LangGraphExecutor; - -use crate::types::{RunAgentError, RunAgentResult, EntryPoint}; -use crate::utils::imports::ImportResolver; -use futures::Stream; -use serde_json::Value; -use std::collections::HashMap; -use std::path::Path; -use std::pin::Pin; - -/// Trait for framework-specific executors -pub trait FrameworkExecutor { - /// Execute a non-streaming entrypoint - fn execute( - &self, - entrypoint: &EntryPoint, - input_args: &[Value], - input_kwargs: &HashMap, - ) -> RunAgentResult; - - /// Execute a streaming entrypoint - fn execute_stream( - &self, - entrypoint: &EntryPoint, - input_args: &[Value], - input_kwargs: &HashMap, - ) -> RunAgentResult> + Send>>>; - - /// Get available entrypoints for this framework - fn get_entrypoints(&self) -> Vec; - - /// Get framework name - fn framework_name(&self) -> &'static str; - - /// Check if an entrypoint is supported - fn supports_entrypoint(&self, entrypoint: &EntryPoint) -> bool { - self.get_entrypoints().contains(&entrypoint.tag) - } -} - -/// Factory function to create appropriate executor based on framework -pub fn create_executor>( - framework: &str, - agent_dir: P, -) -> RunAgentResult> { - match framework.to_lowercase().as_str() { - "langchain" => Ok(Box::new(LangChainExecutor::new(agent_dir)?)), - "langgraph" => Ok(Box::new(LangGraphExecutor::new(agent_dir)?)), - "llamaindex" => { - // For now, use generic executor for LlamaIndex - // Could be extended with specific LlamaIndex logic - Ok(Box::new(GenericExecutor::new(agent_dir)?)) - } - "letta" => { - // Letta uses generic execution patterns - Ok(Box::new(GenericExecutor::new(agent_dir)?)) - } - "crewai" => { - // CrewAI uses generic execution patterns - Ok(Box::new(GenericExecutor::new(agent_dir)?)) - } - "autogen" => { - // AutoGen uses generic execution patterns - Ok(Box::new(GenericExecutor::new(agent_dir)?)) - } - "generic" | "default" | _ => Ok(Box::new(GenericExecutor::new(agent_dir)?)), - } -} - -/// Get supported frameworks -pub fn supported_frameworks() -> Vec<&'static str> { - vec![ - "generic", - "default", - "langchain", - "langgraph", - "llamaindex", - "letta", - "crewai", - "autogen", - ] -} - -/// Check if a framework is supported -pub fn is_framework_supported(framework: &str) -> bool { - supported_frameworks().contains(&framework.to_lowercase().as_str()) -} - -#[cfg(test)] -mod tests { - use super::*; - use tempfile::TempDir; - - #[test] - fn test_create_executor_generic() { - let temp_dir = TempDir::new().unwrap(); - let executor = create_executor("generic", temp_dir.path()); - assert!(executor.is_ok()); - assert_eq!(executor.unwrap().framework_name(), "generic"); - } - - #[test] - fn test_create_executor_langchain() { - let temp_dir = TempDir::new().unwrap(); - let executor = create_executor("langchain", temp_dir.path()); - assert!(executor.is_ok()); - assert_eq!(executor.unwrap().framework_name(), "langchain"); - } - - #[test] - fn test_create_executor_langgraph() { - let temp_dir = TempDir::new().unwrap(); - let executor = create_executor("langgraph", temp_dir.path()); - assert!(executor.is_ok()); - assert_eq!(executor.unwrap().framework_name(), "langgraph"); - } - - #[test] - fn test_supported_frameworks() { - let frameworks = supported_frameworks(); - assert!(frameworks.contains(&"generic")); - assert!(frameworks.contains(&"langchain")); - assert!(frameworks.contains(&"langgraph")); - assert!(frameworks.contains(&"letta")); - } - - #[test] - fn test_is_framework_supported() { - assert!(is_framework_supported("langchain")); - assert!(is_framework_supported("LangChain")); // Case insensitive - assert!(is_framework_supported("generic")); - assert!(!is_framework_supported("unknown_framework")); - } -} \ No newline at end of file diff --git a/runagent-rust/runagent/src/server/handlers.rs b/runagent-rust/runagent/src/server/handlers.rs deleted file mode 100644 index 6d52da7..0000000 --- a/runagent-rust/runagent/src/server/handlers.rs +++ /dev/null @@ -1,335 +0,0 @@ -//! HTTP handlers for the local server - -use crate::server::local_server::ServerState; -use crate::types::*; -use axum::{ - extract::{Path, State, WebSocketUpgrade}, - http::StatusCode, - response::{IntoResponse, Json}, - extract::ws::{Message, WebSocket}, -}; -use chrono::Utc; -use futures::{sink::SinkExt, stream::StreamExt}; -use serde_json::{json, Value}; -use std::collections::HashMap; - -/// Root endpoint showing server info -pub async fn root(State(state): State) -> impl IntoResponse { - let info = AgentInfo { - message: format!("RunAgent API - Agent {}", state.agent_id), - version: crate::VERSION.to_string(), - host: "127.0.0.1".to_string(), - port: 8450, // Would be dynamic in real implementation - config: { - let mut config = HashMap::new(); - config.insert("agent_id".to_string(), json!(state.agent_id)); - config.insert("agent_path".to_string(), json!(state.agent_path)); - config.insert("framework".to_string(), json!("langchain")); - config - }, - endpoints: { - let mut endpoints = HashMap::new(); - endpoints.insert("GET /".to_string(), "Agent info".to_string()); - endpoints.insert("GET /health".to_string(), "Health check".to_string()); - endpoints.insert("GET /api/v1/agents/{id}/architecture".to_string(), "Agent architecture".to_string()); - endpoints.insert("POST /api/v1/agents/{id}/execute/{entrypoint}".to_string(), "Run agent".to_string()); - endpoints.insert("WS /api/v1/agents/{id}/execute/{entrypoint}/ws".to_string(), "Stream agent".to_string()); - endpoints - }, - }; - - Json(info) -} - -/// Health check endpoint -pub async fn health_check() -> impl IntoResponse { - let health = json!({ - "status": "healthy", - "server": "RunAgent Local Server", - "timestamp": Utc::now().to_rfc3339(), - "version": crate::VERSION - }); - - Json(health) -} - -/// Get agent architecture -pub async fn get_agent_architecture(State(state): State) -> impl IntoResponse { - // Load agent config or provide default architecture - let architecture = json!({ - "agent_id": state.agent_id, - "framework": "langchain", - "entrypoints": [ - { - "file": "main.py", - "module": "run", - "tag": "generic" - }, - { - "file": "main.py", - "module": "run_stream", - "tag": "generic_stream" - }, - { - "file": "main.py", - "module": "health_check", - "tag": "health" - } - ] - }); - - Json(architecture) -} - -/// Run agent endpoint -pub async fn run_agent( - State(state): State, - Path((_agent_id, entrypoint)): Path<(String, String)>, - Json(request): Json, -) -> impl IntoResponse { - let start_time = std::time::Instant::now(); - - // Clone the input data to avoid borrow checker issues - let input_kwargs = request.input_data.input_kwargs.clone(); - let input_args = request.input_data.input_args.clone(); - - // Keep the original request data for serialization - let input_data_json = serde_json::to_string(&request.input_data).unwrap_or_default(); - - // Simple execution based on entrypoint - let (success, output_data, error) = match entrypoint.as_str() { - "generic" => execute_generic_entrypoint(&input_kwargs, &input_args), - "health" => execute_health_entrypoint(), - _ => (false, None, Some(format!("Unknown entrypoint: {}", entrypoint))), - }; - - let execution_time = start_time.elapsed().as_secs_f64(); - - // Record the run in the database if available - #[cfg(feature = "db")] - if let Some(ref db_service) = state.db_service { - let agent_run = crate::db::models::AgentRun { - id: 0, // Will be set by database - agent_id: state.agent_id.clone(), - input_data: input_data_json, - output_data: output_data.as_ref().map(|v| serde_json::to_string(v).unwrap_or_default()), - success, - error_message: error.clone(), - execution_time: Some(execution_time), - started_at: Utc::now(), - completed_at: Some(Utc::now()), - }; - - let _ = db_service.record_agent_run(agent_run).await; - } - - let response = AgentRunResponse { - success, - output_data, - error, - execution_time: Some(execution_time), - agent_id: state.agent_id.clone(), - }; - - Json(response) -} - -/// Execute generic entrypoint -fn execute_generic_entrypoint( - input_kwargs: &HashMap, - input_args: &[Value], -) -> (bool, Option, Option) { - // Extract message from kwargs or args - let message = input_kwargs - .get("message") - .and_then(|v| v.as_str()) - .or_else(|| input_args.first().and_then(|v| v.as_str())) - .unwrap_or("Hello from RunAgent!"); - - let temperature = input_kwargs - .get("temperature") - .and_then(|v| v.as_f64()) - .unwrap_or(0.7); - - let model = input_kwargs - .get("model") - .and_then(|v| v.as_str()) - .unwrap_or("gpt-3.5-turbo"); - - // Create mock response - let output = json!({ - "success": true, - "response": format!("Mock LangChain response to: {}", message), - "input": { - "message": message, - "temperature": temperature, - "model": model - }, - "metadata": { - "timestamp": Utc::now().to_rfc3339(), - "framework": "langchain", - "agent_type": "test_mock", - "model_used": model, - "response_length": message.len() + 25, - "mock": true - } - }); - - (true, Some(output), None) -} - -/// Execute health entrypoint -fn execute_health_entrypoint() -> (bool, Option, Option) { - let output = json!({ - "status": "healthy", - "framework": "langchain", - "agent_type": "test", - "timestamp": Utc::now().to_rfc3339(), - "environment": { - "server": "rust", - "version": crate::VERSION - } - }); - - (true, Some(output), None) -} - -/// WebSocket handler for streaming -pub async fn websocket_handler( - ws: WebSocketUpgrade, - State(state): State, - Path((_agent_id, entrypoint)): Path<(String, String)>, -) -> impl IntoResponse { - ws.on_upgrade(move |socket| handle_websocket(socket, state, entrypoint)) -} - -/// Handle WebSocket connection -async fn handle_websocket(socket: WebSocket, _state: ServerState, entrypoint: String) { - let (mut sender, mut receiver) = socket.split(); - - // Handle incoming messages - while let Some(msg) = receiver.next().await { - match msg { - Ok(Message::Text(text)) => { - // Parse the incoming message - if let Ok(request) = serde_json::from_str::(&text) { - // Extract message from request - let message = request - .get("input_data") - .and_then(|d| d.get("input_kwargs")) - .and_then(|k| k.get("message")) - .and_then(|m| m.as_str()) - .unwrap_or("Hello from streaming agent"); - - // Mock streaming response based on entrypoint - let chunks = match entrypoint.as_str() { - "generic_stream" => create_mock_stream_chunks(message), - _ => vec![ - json!({ - "type": "error", - "error": format!("Unsupported streaming entrypoint: {}", entrypoint), - "timestamp": Utc::now().to_rfc3339() - }) - ], - }; - - // Send chunks - for (_i, chunk) in chunks.iter().enumerate() { - if sender - .send(Message::Text(chunk.to_string())) - .await - .is_err() - { - break; - } - - // Small delay to simulate processing - tokio::time::sleep(tokio::time::Duration::from_millis(200)).await; - - // Break on completion chunk - if chunk.get("type").and_then(|t| t.as_str()) == Some("complete") { - break; - } - } - } - } - Ok(Message::Close(_)) => { - break; - } - _ => {} - } - } -} - -/// Create mock streaming chunks -fn create_mock_stream_chunks(message: &str) -> Vec { - let response_text = format!("Mock streaming response to: {}", message); - let words: Vec<&str> = response_text.split_whitespace().collect(); - - let mut chunks = Vec::new(); - - // Add content chunks - for (i, word) in words.iter().enumerate() { - chunks.push(json!({ - "chunk_id": i + 1, - "content": format!("{} ", word), - "type": "content", - "framework": "langchain", - "mock": true, - "timestamp": Utc::now().to_rfc3339() - })); - } - - // Add completion chunk - chunks.push(json!({ - "type": "complete", - "total_chunks": words.len(), - "framework": "langchain", - "mock": true, - "timestamp": Utc::now().to_rfc3339() - })); - - chunks -} - -/// Error handler -pub async fn handle_error(err: Box) -> impl IntoResponse { - let error_response = json!({ - "error": "Internal server error", - "message": err.to_string(), - "timestamp": Utc::now().to_rfc3339() - }); - - (StatusCode::INTERNAL_SERVER_ERROR, Json(error_response)) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[tokio::test] - async fn test_health_check() { - let response = health_check().await; - // In a real test, we'd assert the response content - assert!(true); // Placeholder assertion - } - - #[test] - fn test_execute_generic_entrypoint() { - let mut kwargs = HashMap::new(); - kwargs.insert("message".to_string(), json!("test message")); - - let (success, output, error) = execute_generic_entrypoint(&kwargs, &[]); - assert!(success); - assert!(output.is_some()); - assert!(error.is_none()); - } - - #[test] - fn test_execute_health_entrypoint() { - let (success, output, error) = execute_health_entrypoint(); - assert!(success); - assert!(output.is_some()); - assert!(error.is_none()); - } -} \ No newline at end of file diff --git a/runagent-rust/runagent/src/server/local_server.rs b/runagent-rust/runagent/src/server/local_server.rs deleted file mode 100644 index b7ad624..0000000 --- a/runagent-rust/runagent/src/server/local_server.rs +++ /dev/null @@ -1,215 +0,0 @@ -//! Local FastAPI-like server for testing deployed agents - -use crate::server::handlers; -use crate::types::{RunAgentError, RunAgentResult}; -use axum::{ - routing::{get, post}, - Router, -}; -use std::net::SocketAddr; -use std::path::PathBuf; -use std::sync::Arc; -use tokio::net::TcpListener; -use tower::ServiceBuilder; -use tower_http::{ - cors::CorsLayer, - trace::TraceLayer, -}; - -#[cfg(feature = "db")] -use crate::db::DatabaseService; - -/// Shared server state -#[derive(Clone)] -pub struct ServerState { - pub agent_id: String, - pub agent_path: PathBuf, - #[cfg(feature = "db")] - pub db_service: Option>, -} - -/// Local server for testing deployed agents -pub struct LocalServer { - app: Router, - addr: SocketAddr, - state: ServerState, -} - -impl LocalServer { - /// Create a new local server - pub async fn new( - agent_id: String, - agent_path: PathBuf, - host: &str, - port: u16, - ) -> RunAgentResult { - let addr = format!("{}:{}", host, port) - .parse() - .map_err(|e| RunAgentError::config(format!("Invalid address: {}", e)))?; - - // Initialize database service if feature is enabled - #[cfg(feature = "db")] - let db_service = match DatabaseService::new(None).await { - Ok(service) => Some(Arc::new(service)), - Err(e) => { - tracing::warn!("Failed to initialize database service: {}", e); - None - } - }; - - let state = ServerState { - agent_id: agent_id.clone(), - agent_path, - #[cfg(feature = "db")] - db_service, - }; - - let app = Self::create_router(state.clone(), &agent_id); - - Ok(Self { app, addr, state }) - } - - /// Create a new local server from agent path with auto-discovery - pub async fn from_path( - agent_path: PathBuf, - host: Option<&str>, - port: Option, - ) -> RunAgentResult { - let host = host.unwrap_or("127.0.0.1"); - let port = port.unwrap_or(8450); - - // Auto-generate agent ID - let agent_id = uuid::Uuid::new_v4().to_string(); - - Self::new(agent_id, agent_path, host, port).await - } - - /// Create the Axum router with all routes - fn create_router(state: ServerState, agent_id: &str) -> Router { - Router::new() - // Root and health - .route("/", get(handlers::root)) - .route("/health", get(handlers::health_check)) - - // API routes - .route("/api/v1", get(handlers::root)) - .route("/api/v1/health", get(handlers::health_check)) - .route( - &format!("/api/v1/agents/{}/architecture", agent_id), - get(handlers::get_agent_architecture), - ) - .route( - "/api/v1/agents/:agent_id/execute/:entrypoint", - post(handlers::run_agent), - ) - - // WebSocket routes for streaming - .route( - "/api/v1/agents/:agent_id/execute/:entrypoint/ws", - get(handlers::websocket_handler), - ) - - // State - .with_state(state) - - // Middleware - .layer( - ServiceBuilder::new() - .layer(TraceLayer::new_for_http()) - .layer(CorsLayer::permissive()) - ) - } - - /// Start the server - pub async fn start(self) -> RunAgentResult<()> { - tracing::info!("Starting local server on {}", self.addr); - tracing::info!("Agent ID: {}", self.state.agent_id); - tracing::info!("Agent Path: {}", self.state.agent_path.display()); - tracing::info!("API Docs: http://{}/docs", self.addr); - - let listener = TcpListener::bind(self.addr) - .await - .map_err(|e| RunAgentError::connection(format!("Failed to bind to {}: {}", self.addr, e)))?; - - tracing::info!("🚀 Server ready at http://{}", self.addr); - tracing::info!("🆔 Agent ID: {}", self.state.agent_id); - tracing::info!("📁 Agent Path: {}", self.state.agent_path.display()); - - axum::serve(listener, self.app) - .await - .map_err(|e| RunAgentError::server(format!("Server error: {}", e)))?; - - Ok(()) - } - - /// Get server information - pub fn get_info(&self) -> ServerInfo { - ServerInfo { - agent_id: self.state.agent_id.clone(), - agent_path: self.state.agent_path.clone(), - host: self.addr.ip().to_string(), - port: self.addr.port(), - url: format!("http://{}", self.addr), - docs_url: format!("http://{}/docs", self.addr), - status: "running".to_string(), - } - } - - /// Get the agent ID - pub fn agent_id(&self) -> &str { - &self.state.agent_id - } - - /// Get the server address - pub fn addr(&self) -> SocketAddr { - self.addr - } -} - -/// Server information -#[derive(Debug, Clone)] -pub struct ServerInfo { - pub agent_id: String, - pub agent_path: PathBuf, - pub host: String, - pub port: u16, - pub url: String, - pub docs_url: String, - pub status: String, -} - -#[cfg(test)] -mod tests { - use super::*; - use tempfile::TempDir; - - #[tokio::test] - async fn test_local_server_creation() { - let temp_dir = TempDir::new().unwrap(); - let agent_path = temp_dir.path().join("agent"); - std::fs::create_dir_all(&agent_path).unwrap(); - - let server = LocalServer::new( - "test-agent".to_string(), - agent_path, - "127.0.0.1", - 8450, - ).await; - - assert!(server.is_ok()); - } - - #[tokio::test] - async fn test_server_from_path() { - let temp_dir = TempDir::new().unwrap(); - let agent_path = temp_dir.path().join("agent"); - std::fs::create_dir_all(&agent_path).unwrap(); - - let server = LocalServer::from_path(agent_path, None, None).await; - assert!(server.is_ok()); - - if let Ok(server) = server { - assert!(!server.agent_id().is_empty()); - } - } -} \ No newline at end of file diff --git a/runagent-rust/runagent/src/server/mod.rs b/runagent-rust/runagent/src/server/mod.rs deleted file mode 100644 index 0187de5..0000000 --- a/runagent-rust/runagent/src/server/mod.rs +++ /dev/null @@ -1,10 +0,0 @@ -//! Local server components for testing deployed agents -//! -//! This module provides a FastAPI-like local server implementation for testing -//! AI agents locally before deploying to production. - -pub mod handlers; -pub mod local_server; - -// Re-export main server -pub use local_server::LocalServer; \ No newline at end of file diff --git a/runagent-rust/runagent/src/types/errors.rs b/runagent-rust/runagent/src/types/errors.rs index 0e5b541..950efe5 100644 --- a/runagent-rust/runagent/src/types/errors.rs +++ b/runagent-rust/runagent/src/types/errors.rs @@ -50,10 +50,6 @@ pub enum RunAgentError { #[error("HTTP error: {0}")] Http(#[from] reqwest::Error), - /// Database errors - #[error("SQL error: {0}")] - Sql(#[from] sqlx::Error), - /// Generic error with context #[error("RunAgent error: {message}")] Generic { message: String }, @@ -137,7 +133,6 @@ impl RunAgentError { Self::Io(_) => "io", Self::Json(_) => "json", Self::Http(_) => "http", - Self::Sql(_) => "sql", Self::Generic { .. } => "generic", } } diff --git a/runagent-rust/runagent/src/types/schema.rs b/runagent-rust/runagent/src/types/schema.rs index 1ff7f15..81d1511 100644 --- a/runagent-rust/runagent/src/types/schema.rs +++ b/runagent-rust/runagent/src/types/schema.rs @@ -94,6 +94,7 @@ pub enum WebSocketActionType { pub struct WebSocketAgentRequest { pub action: WebSocketActionType, pub agent_id: String, + pub entrypoint_tag: String, pub input_data: AgentInputArgs, #[serde(default)] pub stream_config: HashMap, @@ -147,6 +148,7 @@ pub enum MessageType { AgentThought, FinalResponse, Error, + ExecutionError, Status, RawData, Data, diff --git a/runagent-rust/runagent/src/utils/agent.rs b/runagent-rust/runagent/src/utils/agent.rs deleted file mode 100644 index cc8d381..0000000 --- a/runagent-rust/runagent/src/utils/agent.rs +++ /dev/null @@ -1,289 +0,0 @@ -//! Agent utilities for framework detection and validation - -use crate::constants::AGENT_CONFIG_FILE_NAME; -use crate::types::{RunAgentError, RunAgentResult, RunAgentConfig}; -use std::fs; -use std::path::Path; - -/// Detect the framework used by an agent -pub fn detect_framework>(agent_path: P) -> RunAgentResult { - let agent_path = agent_path.as_ref(); - - // Try to read the agent config file - let config_path = agent_path.join(AGENT_CONFIG_FILE_NAME); - - if config_path.exists() { - let config_content = fs::read_to_string(&config_path) - .map_err(|e| RunAgentError::validation(format!("Failed to read config file: {}", e)))?; - - let config: RunAgentConfig = serde_json::from_str(&config_content) - .map_err(|e| RunAgentError::validation(format!("Failed to parse config file: {}", e)))?; - - return Ok(config.framework); - } - - // Fallback: try to detect from file contents - detect_framework_from_files(agent_path) -} - -/// Detect framework from analyzing Python files -fn detect_framework_from_files>(agent_path: P) -> RunAgentResult { - let agent_path = agent_path.as_ref(); - - let framework_keywords = [ - ("langgraph", vec!["langgraph", "StateGraph", "Graph"]), - ("langchain", vec!["langchain", "ConversationChain", "AgentExecutor"]), - ("llamaindex", vec!["llama_index", "VectorStoreIndex", "QueryEngine"]), - ("letta", vec!["letta", "MemGPT"]), - ]; - - // Check main Python files - for file_name in ["main.py", "agent.py", "run.py"] { - let file_path = agent_path.join(file_name); - if file_path.exists() { - if let Ok(content) = fs::read_to_string(&file_path) { - let content_lower = content.to_lowercase(); - - for (framework, keywords) in &framework_keywords { - if keywords.iter().any(|keyword| content_lower.contains(&keyword.to_lowercase())) { - return Ok(framework.to_string()); - } - } - } - } - } - - // Check requirements.txt - let req_file = agent_path.join("requirements.txt"); - if req_file.exists() { - if let Ok(content) = fs::read_to_string(&req_file) { - let content_lower = content.to_lowercase(); - - for (framework, keywords) in &framework_keywords { - if keywords.iter().any(|keyword| content_lower.contains(&keyword.to_lowercase())) { - return Ok(framework.to_string()); - } - } - } - } - - Ok("unknown".to_string()) -} - -/// Validate an agent project structure -pub fn validate_agent>(agent_path: P) -> RunAgentResult { - let agent_path = agent_path.as_ref(); - - let mut result = ValidationResult { - valid: false, - errors: Vec::new(), - warnings: Vec::new(), - files_found: Vec::new(), - missing_files: Vec::new(), - }; - - // Check if directory exists - if !agent_path.exists() { - result.errors.push(format!("Agent directory not found: {}", agent_path.display())); - return Ok(result); - } - - if !agent_path.is_dir() { - result.errors.push(format!("Agent path is not a directory: {}", agent_path.display())); - return Ok(result); - } - - // Check for required files - let required_files = [AGENT_CONFIG_FILE_NAME]; - - for file_name in &required_files { - let file_path = agent_path.join(file_name); - if file_path.exists() { - result.files_found.push(file_name.to_string()); - } else { - result.missing_files.push(file_name.to_string()); - result.errors.push(format!("Required file missing: {}", file_name)); - } - } - - // Check for suggested files - let suggested_files = ["requirements.txt", "main.py", "agent.py"]; - - for file_name in &suggested_files { - let file_path = agent_path.join(file_name); - if file_path.exists() { - result.files_found.push(file_name.to_string()); - } else { - result.warnings.push(format!("Suggested file missing: {}", file_name)); - } - } - - // Validate config file if it exists - if result.files_found.contains(&AGENT_CONFIG_FILE_NAME.to_string()) { - if let Err(e) = validate_config_file(agent_path) { - result.errors.push(format!("Config file validation failed: {}", e)); - } - } - - // Check for unwanted files - let unwanted_files = [".env"]; - - for file_name in &unwanted_files { - let file_path = agent_path.join(file_name); - if file_path.exists() { - result.warnings.push(format!("Unwanted file found: {} (should not be committed)", file_name)); - } - } - - // Determine if validation passed - result.valid = result.errors.is_empty(); - - Ok(result) -} - -/// Validate the agent config file -fn validate_config_file>(agent_path: P) -> RunAgentResult<()> { - let config_path = agent_path.as_ref().join(AGENT_CONFIG_FILE_NAME); - - let config_content = fs::read_to_string(&config_path) - .map_err(|e| RunAgentError::validation(format!("Failed to read config file: {}", e)))?; - - let _config: RunAgentConfig = serde_json::from_str(&config_content) - .map_err(|e| RunAgentError::validation(format!("Invalid config file format: {}", e)))?; - - // Additional validation could be added here - - Ok(()) -} - -/// Get agent configuration from config file -pub fn get_agent_config>(agent_path: P) -> RunAgentResult { - let config_path = agent_path.as_ref().join(AGENT_CONFIG_FILE_NAME); - - let config_content = fs::read_to_string(&config_path) - .map_err(|e| RunAgentError::validation(format!("Failed to read config file: {}", e)))?; - - let config: RunAgentConfig = serde_json::from_str(&config_content) - .map_err(|e| RunAgentError::validation(format!("Failed to parse config file: {}", e)))?; - - Ok(config) -} - -/// Result of agent validation -#[derive(Debug, Clone)] -pub struct ValidationResult { - /// Whether the agent passed validation - pub valid: bool, - /// List of validation errors - pub errors: Vec, - /// List of validation warnings - pub warnings: Vec, - /// List of files that were found - pub files_found: Vec, - /// List of required files that are missing - pub missing_files: Vec, -} - -#[cfg(test)] -mod tests { - use super::*; - use serde_json::json; - use tempfile::TempDir; - - fn create_test_agent_config() -> serde_json::Value { - json!({ - "agent_name": "test-agent", - "description": "A test agent", - "framework": "langchain", - "template": "basic", - "version": "1.0.0", - "created_at": "2023-01-01T00:00:00Z", - "template_source": { - "repo_url": "https://github.com/test/test.git", - "author": "test", - "path": "test" - }, - "agent_architecture": { - "entrypoints": [ - { - "file": "main.py", - "module": "run", - "tag": "generic" - } - ] - }, - "env_vars": {} - }) - } - - #[test] - fn test_detect_framework_from_config() { - let temp_dir = TempDir::new().unwrap(); - let agent_path = temp_dir.path(); - - // Create config file - let config = create_test_agent_config(); - let config_path = agent_path.join(AGENT_CONFIG_FILE_NAME); - fs::write(&config_path, config.to_string()).unwrap(); - - let framework = detect_framework(agent_path).unwrap(); - assert_eq!(framework, "langchain"); - } - - #[test] - fn test_detect_framework_from_files() { - let temp_dir = TempDir::new().unwrap(); - let agent_path = temp_dir.path(); - - // Create a Python file with LangChain imports - let main_py = agent_path.join("main.py"); - fs::write(&main_py, "from langchain.chains import ConversationChain").unwrap(); - - let framework = detect_framework(agent_path).unwrap(); - assert_eq!(framework, "langchain"); - } - - #[test] - fn test_validate_agent_valid() { - let temp_dir = TempDir::new().unwrap(); - let agent_path = temp_dir.path(); - - // Create required files - let config = create_test_agent_config(); - let config_path = agent_path.join(AGENT_CONFIG_FILE_NAME); - fs::write(&config_path, config.to_string()).unwrap(); - - let result = validate_agent(agent_path).unwrap(); - assert!(result.valid); - assert!(result.errors.is_empty()); - } - - #[test] - fn test_validate_agent_missing_config() { - let temp_dir = TempDir::new().unwrap(); - let agent_path = temp_dir.path(); - - let result = validate_agent(agent_path).unwrap(); - assert!(!result.valid); - assert!(!result.errors.is_empty()); - assert!(result.missing_files.contains(&AGENT_CONFIG_FILE_NAME.to_string())); - } - - #[test] - fn test_get_agent_config() { - let temp_dir = TempDir::new().unwrap(); - let agent_path = temp_dir.path(); - - // Create config file - let config = create_test_agent_config(); - let config_path = agent_path.join(AGENT_CONFIG_FILE_NAME); - fs::write(&config_path, config.to_string()).unwrap(); - - let result = get_agent_config(agent_path); - assert!(result.is_ok()); - - let config = result.unwrap(); - assert_eq!(config.agent_name, "test-agent"); - assert_eq!(config.framework, "langchain"); - } -} \ No newline at end of file diff --git a/runagent-rust/runagent/src/utils/config.rs b/runagent-rust/runagent/src/utils/config.rs index c09fa44..cd0b3ed 100644 --- a/runagent-rust/runagent/src/utils/config.rs +++ b/runagent-rust/runagent/src/utils/config.rs @@ -15,6 +15,11 @@ use std::path::PathBuf; pub struct Config { pub api_key: Option, pub base_url: String, + pub user_email: Option, + pub user_id: Option, + pub user_tier: Option, + pub auth_validated: Option, + #[serde(default)] pub user_info: HashMap, } @@ -23,6 +28,10 @@ impl Default for Config { Self { api_key: None, base_url: DEFAULT_BASE_URL.to_string(), + user_email: None, + user_id: None, + user_tier: None, + auth_validated: None, user_info: HashMap::new(), } } @@ -69,8 +78,10 @@ impl Config { let content = fs::read_to_string(&config_path) .map_err(|e| RunAgentError::config(format!("Failed to read config file: {}", e)))?; - serde_json::from_str(&content) - .map_err(|e| RunAgentError::config(format!("Failed to parse config file: {}", e))) + match serde_json::from_str::(&content) { + Ok(parsed_config) => Ok(parsed_config), + Err(_) => Ok(Self::default()) + } } else { Ok(Self::default()) } diff --git a/runagent-rust/runagent/src/utils/imports.rs b/runagent-rust/runagent/src/utils/imports.rs deleted file mode 100644 index b9c7394..0000000 --- a/runagent-rust/runagent/src/utils/imports.rs +++ /dev/null @@ -1,490 +0,0 @@ -//! Import resolution utilities for Python modules -//! -//! This module provides functionality to resolve Python imports and module paths -//! for agent execution. It handles dynamic import resolution similar to Python's -//! import system. - -use crate::types::{RunAgentError, RunAgentResult}; -use std::collections::HashMap; -use std::fs; -use std::path::{Path, PathBuf}; - -/// Import resolver for Python modules -pub struct ImportResolver { - /// Base directory for the agent - base_dir: PathBuf, - /// Cache of resolved imports - import_cache: HashMap, - /// Verbose logging flag - verbose: bool, -} - -impl ImportResolver { - /// Create a new import resolver - pub fn new>(base_dir: P) -> RunAgentResult { - let base_dir = base_dir.as_ref().to_path_buf(); - - if !base_dir.exists() { - return Err(RunAgentError::validation(format!( - "Base directory does not exist: {}", - base_dir.display() - ))); - } - - Ok(Self { - base_dir, - import_cache: HashMap::new(), - verbose: false, - }) - } - - /// Create a new import resolver with verbose logging - pub fn with_verbose>(base_dir: P, verbose: bool) -> RunAgentResult { - let mut resolver = Self::new(base_dir)?; - resolver.verbose = verbose; - Ok(resolver) - } - - /// Resolve an import from a file path and module name - pub fn resolve_import>( - &mut self, - file_path: P, - module_name: &str, - ) -> RunAgentResult { - let file_path = file_path.as_ref(); - let cache_key = format!("{}:{}", file_path.display(), module_name); - - // Check cache first - if let Some(cached_path) = self.import_cache.get(&cache_key) { - return Ok(ImportInfo { - module_name: module_name.to_string(), - file_path: PathBuf::from(cached_path), - import_path: cached_path.clone(), - is_builtin: false, - }); - } - - if self.verbose { - tracing::debug!("Resolving import: {} from {}", module_name, file_path.display()); - } - - let import_info = self.resolve_import_internal(file_path, module_name)?; - - // Cache the result - self.import_cache.insert(cache_key, import_info.import_path.clone()); - - Ok(import_info) - } - - /// Internal import resolution logic - fn resolve_import_internal>( - &self, - file_path: P, - module_name: &str, - ) -> RunAgentResult { - let file_path = file_path.as_ref(); - - // Handle relative imports (starting with .) - if module_name.starts_with('.') { - return self.resolve_relative_import(file_path, module_name); - } - - // Handle absolute imports - self.resolve_absolute_import(module_name) - } - - /// Resolve relative imports (e.g., .module, ..module) - fn resolve_relative_import>( - &self, - file_path: P, - module_name: &str, - ) -> RunAgentResult { - let file_path = file_path.as_ref(); - let file_dir = file_path.parent().unwrap_or(&self.base_dir); - - // Count leading dots to determine relative level - let dots = module_name.chars().take_while(|&c| c == '.').count(); - let actual_module = &module_name[dots..]; - - // Go up the directory tree based on dot count - let mut target_dir = file_dir.to_path_buf(); - for _ in 1..dots { - target_dir = target_dir.parent() - .ok_or_else(|| RunAgentError::validation("Cannot go above base directory"))? - .to_path_buf(); - } - - // Look for the module in the target directory - if actual_module.is_empty() { - // Import the directory itself - return Ok(ImportInfo { - module_name: module_name.to_string(), - file_path: target_dir.clone(), - import_path: format!("relative:{}", target_dir.display()), - is_builtin: false, - }); - } - - self.find_module_in_directory(&target_dir, actual_module) - } - - /// Resolve absolute imports - fn resolve_absolute_import(&self, module_name: &str) -> RunAgentResult { - // Check if it's a built-in module - if self.is_builtin_module(module_name) { - return Ok(ImportInfo { - module_name: module_name.to_string(), - file_path: PathBuf::new(), - import_path: format!("builtin:{}", module_name), - is_builtin: true, - }); - } - - // Split module path (e.g., package.submodule) - let parts: Vec<&str> = module_name.split('.').collect(); - - // Start from base directory and navigate through module path - let mut current_dir = self.base_dir.clone(); - - for (i, part) in parts.iter().enumerate() { - let potential_file = current_dir.join(format!("{}.py", part)); - let potential_dir = current_dir.join(part); - let potential_init = potential_dir.join("__init__.py"); - - if potential_file.exists() { - // Found a .py file - return Ok(ImportInfo { - module_name: module_name.to_string(), - file_path: potential_file.clone(), - import_path: potential_file.to_string_lossy().to_string(), - is_builtin: false, - }); - } else if potential_init.exists() { - // Found a package directory - if i == parts.len() - 1 { - // This is the final part, return the package - return Ok(ImportInfo { - module_name: module_name.to_string(), - file_path: potential_init.clone(), - import_path: potential_init.to_string_lossy().to_string(), - is_builtin: false, - }); - } else { - // Continue searching in the package directory - current_dir = potential_dir; - } - } else if potential_dir.exists() && potential_dir.is_dir() { - // Directory exists but no __init__.py, continue anyway - current_dir = potential_dir; - } else { - return Err(RunAgentError::validation(format!( - "Cannot find module '{}' (looking for part '{}')", - module_name, part - ))); - } - } - - Err(RunAgentError::validation(format!( - "Module '{}' not found", - module_name - ))) - } - - /// Find a module within a specific directory - fn find_module_in_directory>( - &self, - directory: P, - module_name: &str, - ) -> RunAgentResult { - let directory = directory.as_ref(); - - // Look for module.py - let module_file = directory.join(format!("{}.py", module_name)); - if module_file.exists() { - return Ok(ImportInfo { - module_name: module_name.to_string(), - file_path: module_file.clone(), - import_path: module_file.to_string_lossy().to_string(), - is_builtin: false, - }); - } - - // Look for module/__init__.py - let module_dir = directory.join(module_name); - let module_init = module_dir.join("__init__.py"); - if module_init.exists() { - return Ok(ImportInfo { - module_name: module_name.to_string(), - file_path: module_init.clone(), - import_path: module_init.to_string_lossy().to_string(), - is_builtin: false, - }); - } - - Err(RunAgentError::validation(format!( - "Module '{}' not found in directory '{}'", - module_name, - directory.display() - ))) - } - - /// Check if a module is a Python built-in module - fn is_builtin_module(&self, module_name: &str) -> bool { - // Common Python built-in modules - const BUILTIN_MODULES: &[&str] = &[ - "sys", "os", "time", "datetime", "json", "re", "math", "random", - "collections", "itertools", "functools", "operator", "typing", - "pathlib", "subprocess", "threading", "asyncio", "logging", - "urllib", "http", "email", "xml", "sqlite3", "csv", "configparser", - "argparse", "inspect", "importlib", "pkgutil", "traceback", - "warnings", "weakref", "copy", "pickle", "base64", "hashlib", - "hmac", "secrets", "uuid", "decimal", "fractions", "statistics", - "enum", "dataclasses", "contextlib", "tempfile", "shutil", - "glob", "fnmatch", "linecache", "fileinput", "zipfile", "tarfile", - "gzip", "bz2", "lzma", "socket", "ssl", "select", "signal", - "multiprocessing", "concurrent", "queue", "sched", "string", - "struct", "codecs", "locale", "gettext", "calendar", "pprint", - "reprlib", "textwrap", "unicodedata", "stringprep", "readline", - "rlcompleter", "cmd", "shlex", "io", "bufferedwriter", "platform", - "errno", "ctypes", "mmap", "winreg", "_winapi", "winsound", - "posix", "pwd", "grp", "termios", "tty", "pty", "fcntl", "pipes", - "resource", "nis", "syslog", "optparse", "getopt", "distutils", - "venv", "zipapp", "faulthandler", "trace", "timeit", "cProfile", - "profile", "pstats", "pdb", "doctest", "unittest", "test", - ]; - - // Check if the module (or its top-level package) is builtin - let top_level = module_name.split('.').next().unwrap_or(module_name); - BUILTIN_MODULES.contains(&top_level) - } - - /// Get all Python files in the base directory recursively - pub fn discover_modules(&self) -> RunAgentResult> { - let mut modules = Vec::new(); - self.discover_modules_recursive(&self.base_dir, String::new(), &mut modules)?; - Ok(modules) - } - - /// Recursively discover modules in a directory - fn discover_modules_recursive( - &self, - dir: &Path, - prefix: String, - modules: &mut Vec, - ) -> RunAgentResult<()> { - if !dir.exists() || !dir.is_dir() { - return Ok(()); - } - - let entries = fs::read_dir(dir) - .map_err(|e| RunAgentError::validation(format!("Cannot read directory: {}", e)))?; - - for entry in entries { - let entry = entry - .map_err(|e| RunAgentError::validation(format!("Cannot read entry: {}", e)))?; - let path = entry.path(); - let name = entry.file_name().to_string_lossy().to_string(); - - // Skip hidden files and __pycache__ - if name.starts_with('.') || name == "__pycache__" { - continue; - } - - if path.is_file() && name.ends_with(".py") { - let module_name = name.trim_end_matches(".py").to_string(); - let full_name = if prefix.is_empty() { - module_name.clone() - } else { - format!("{}.{}", prefix, module_name) - }; - - modules.push(ModuleInfo { - name: full_name, - file_path: path, - is_package: false, - }); - } else if path.is_dir() { - let init_file = path.join("__init__.py"); - let is_package = init_file.exists(); - - let full_name = if prefix.is_empty() { - name.clone() - } else { - format!("{}.{}", prefix, name) - }; - - if is_package { - modules.push(ModuleInfo { - name: full_name.clone(), - file_path: init_file, - is_package: true, - }); - } - - // Recurse into subdirectory - self.discover_modules_recursive(&path, full_name, modules)?; - } - } - - Ok(()) - } - - /// Clear the import cache - pub fn clear_cache(&mut self) { - self.import_cache.clear(); - } - - /// Get cache statistics - pub fn cache_stats(&self) -> CacheStats { - CacheStats { - total_entries: self.import_cache.len(), - base_directory: self.base_dir.clone(), - } - } -} - -/// Information about a resolved import -#[derive(Debug, Clone)] -pub struct ImportInfo { - /// The original module name - pub module_name: String, - /// Path to the module file - pub file_path: PathBuf, - /// Import path string for caching - pub import_path: String, - /// Whether this is a built-in module - pub is_builtin: bool, -} - -/// Information about a discovered module -#[derive(Debug, Clone)] -pub struct ModuleInfo { - /// Full module name (with dots) - pub name: String, - /// Path to the module file - pub file_path: PathBuf, - /// Whether this is a package (has __init__.py) - pub is_package: bool, -} - -/// Cache statistics -#[derive(Debug)] -pub struct CacheStats { - /// Total number of cached entries - pub total_entries: usize, - /// Base directory being resolved from - pub base_directory: PathBuf, -} - -#[cfg(test)] -mod tests { - use super::*; - use std::fs; - use tempfile::TempDir; - - fn create_test_module_structure() -> TempDir { - let temp_dir = TempDir::new().unwrap(); - let base = temp_dir.path(); - - // Create main.py - fs::write(base.join("main.py"), "def run(): pass").unwrap(); - - // Create package/ - fs::create_dir(base.join("package")).unwrap(); - fs::write(base.join("package/__init__.py"), "").unwrap(); - fs::write(base.join("package/module.py"), "def func(): pass").unwrap(); - - // Create subpackage/ - fs::create_dir(base.join("package/subpackage")).unwrap(); - fs::write(base.join("package/subpackage/__init__.py"), "").unwrap(); - fs::write(base.join("package/subpackage/deep.py"), "def deep_func(): pass").unwrap(); - - temp_dir - } - - #[test] - fn test_import_resolver_creation() { - let temp_dir = TempDir::new().unwrap(); - let resolver = ImportResolver::new(temp_dir.path()); - assert!(resolver.is_ok()); - } - - #[test] - fn test_builtin_module_detection() { - let temp_dir = TempDir::new().unwrap(); - let resolver = ImportResolver::new(temp_dir.path()).unwrap(); - - assert!(resolver.is_builtin_module("sys")); - assert!(resolver.is_builtin_module("os")); - assert!(resolver.is_builtin_module("json")); - assert!(!resolver.is_builtin_module("custom_module")); - } - - #[test] - fn test_absolute_import_resolution() { - let temp_dir = create_test_module_structure(); - let mut resolver = ImportResolver::new(temp_dir.path()).unwrap(); - - // Test resolving main.py - let result = resolver.resolve_import(temp_dir.path().join("main.py"), "main"); - assert!(result.is_ok()); - - // Test resolving package - let result = resolver.resolve_import(temp_dir.path().join("main.py"), "package"); - assert!(result.is_ok()); - - // Test resolving package.module - let result = resolver.resolve_import(temp_dir.path().join("main.py"), "package.module"); - assert!(result.is_ok()); - } - - #[test] - fn test_module_discovery() { - let temp_dir = create_test_module_structure(); - let resolver = ImportResolver::new(temp_dir.path()).unwrap(); - - let modules = resolver.discover_modules().unwrap(); - assert!(!modules.is_empty()); - - // Should find main.py - assert!(modules.iter().any(|m| m.name == "main")); - - // Should find package - assert!(modules.iter().any(|m| m.name == "package" && m.is_package)); - - // Should find package.module - assert!(modules.iter().any(|m| m.name == "package.module")); - } - - #[test] - fn test_cache_functionality() { - let temp_dir = create_test_module_structure(); - let mut resolver = ImportResolver::new(temp_dir.path()).unwrap(); - - // First resolution - let result1 = resolver.resolve_import(temp_dir.path().join("main.py"), "main"); - assert!(result1.is_ok()); - - // Second resolution should use cache - let result2 = resolver.resolve_import(temp_dir.path().join("main.py"), "main"); - assert!(result2.is_ok()); - - let stats = resolver.cache_stats(); - assert!(stats.total_entries > 0); - - resolver.clear_cache(); - let stats = resolver.cache_stats(); - assert_eq!(stats.total_entries, 0); - } - - #[test] - fn test_relative_import_resolution() { - let temp_dir = create_test_module_structure(); - let mut resolver = ImportResolver::new(temp_dir.path()).unwrap(); - - // Test relative import from package/module.py to package - let module_file = temp_dir.path().join("package/module.py"); - let result = resolver.resolve_import(&module_file, ".subpackage"); - assert!(result.is_ok()); - } -} \ No newline at end of file diff --git a/runagent-rust/runagent/src/utils/mod.rs b/runagent-rust/runagent/src/utils/mod.rs index eaba2be..7c3c3f5 100644 --- a/runagent-rust/runagent/src/utils/mod.rs +++ b/runagent-rust/runagent/src/utils/mod.rs @@ -1,16 +1,11 @@ //! Utility modules for the RunAgent SDK //! //! This module contains various utility functions and helpers used throughout -//! the SDK for configuration management, agent validation, serialization, etc. +//! the SDK for configuration management and serialization. -pub mod agent; pub mod config; -pub mod imports; -pub mod port; pub mod serializer; // Re-export commonly used utilities -pub use agent::{detect_framework, validate_agent}; pub use config::Config; -pub use port::PortManager; pub use serializer::CoreSerializer; \ No newline at end of file diff --git a/runagent-rust/runagent/src/utils/port.rs b/runagent-rust/runagent/src/utils/port.rs deleted file mode 100644 index ec1d696..0000000 --- a/runagent-rust/runagent/src/utils/port.rs +++ /dev/null @@ -1,192 +0,0 @@ -//! Port management utilities for allocating available ports - -use crate::constants::{DEFAULT_PORT_START, DEFAULT_PORT_END}; -use crate::types::{RunAgentError, RunAgentResult}; -use std::net::{SocketAddr, TcpListener}; - -/// Port manager for finding and allocating available ports -pub struct PortManager; - -impl PortManager { - /// Check if a specific port is available on the given host - pub fn is_port_available(host: &str, port: u16) -> bool { - let addr = format!("{}:{}", host, port); - - if let Ok(socket_addr) = addr.parse::() { - TcpListener::bind(socket_addr).is_ok() - } else { - false - } - } - - /// Find the next available port starting from a given port - pub fn find_available_port(host: &str, start_port: u16) -> RunAgentResult { - for port in start_port..=DEFAULT_PORT_END { - if Self::is_port_available(host, port) { - return Ok(port); - } - } - - Err(RunAgentError::connection(format!( - "No available ports found in range {}-{}", - start_port, DEFAULT_PORT_END - ))) - } - - /// Allocate a unique host:port combination, avoiding used ports - pub fn allocate_unique_address(used_ports: &[u16]) -> RunAgentResult<(String, u16)> { - let host = "127.0.0.1".to_string(); - - for port in DEFAULT_PORT_START..=DEFAULT_PORT_END { - if !used_ports.contains(&port) && Self::is_port_available(&host, port) { - return Ok((host, port)); - } - } - - Err(RunAgentError::connection( - "No available ports found for allocation".to_string() - )) - } - - /// Get a list of available ports in the default range - pub fn get_available_ports(host: &str, count: usize) -> Vec { - let mut available_ports = Vec::new(); - - for port in DEFAULT_PORT_START..=DEFAULT_PORT_END { - if Self::is_port_available(host, port) { - available_ports.push(port); - if available_ports.len() >= count { - break; - } - } - } - - available_ports - } - - /// Check if any port in a range is available - pub fn has_available_ports(host: &str, start_port: u16, end_port: u16) -> bool { - for port in start_port..=end_port { - if Self::is_port_available(host, port) { - return true; - } - } - false - } - - /// Get port usage statistics for a range - pub fn get_port_usage_stats(host: &str) -> PortUsageStats { - let mut available_count = 0; - let mut used_count = 0; - - for port in DEFAULT_PORT_START..=DEFAULT_PORT_END { - if Self::is_port_available(host, port) { - available_count += 1; - } else { - used_count += 1; - } - } - - let total_ports = (DEFAULT_PORT_END - DEFAULT_PORT_START + 1) as usize; - - PortUsageStats { - total_ports, - available_count, - used_count, - usage_percentage: (used_count as f64 / total_ports as f64) * 100.0, - start_port: DEFAULT_PORT_START, - end_port: DEFAULT_PORT_END, - } - } -} - -/// Port usage statistics -#[derive(Debug, Clone)] -pub struct PortUsageStats { - /// Total number of ports in the range - pub total_ports: usize, - /// Number of available ports - pub available_count: usize, - /// Number of used ports - pub used_count: usize, - /// Percentage of ports that are used - pub usage_percentage: f64, - /// Start of the port range - pub start_port: u16, - /// End of the port range - pub end_port: u16, -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_is_port_available() { - // Test with a likely available port - assert!(PortManager::is_port_available("127.0.0.1", 0)); // Port 0 lets OS choose - - // Test with an invalid host - assert!(!PortManager::is_port_available("invalid.host", 8080)); - } - - #[test] - fn test_find_available_port() { - let result = PortManager::find_available_port("127.0.0.1", DEFAULT_PORT_START); - assert!(result.is_ok()); - - if let Ok(port) = result { - assert!(port >= DEFAULT_PORT_START); - assert!(port <= DEFAULT_PORT_END); - } - } - - #[test] - fn test_allocate_unique_address() { - let used_ports = vec![8450, 8451, 8452]; // Some used ports - let result = PortManager::allocate_unique_address(&used_ports); - - assert!(result.is_ok()); - - if let Ok((host, port)) = result { - assert_eq!(host, "127.0.0.1"); - assert!(!used_ports.contains(&port)); - assert!(port >= DEFAULT_PORT_START); - assert!(port <= DEFAULT_PORT_END); - } - } - - #[test] - fn test_get_available_ports() { - let available_ports = PortManager::get_available_ports("127.0.0.1", 5); - assert!(available_ports.len() <= 5); - - // All returned ports should be in the valid range - for port in available_ports { - assert!(port >= DEFAULT_PORT_START); - assert!(port <= DEFAULT_PORT_END); - } - } - - #[test] - fn test_has_available_ports() { - let has_ports = PortManager::has_available_ports("127.0.0.1", DEFAULT_PORT_START, DEFAULT_PORT_END); - assert!(has_ports); // Should have some available ports - - // Test with impossible range - let no_ports = PortManager::has_available_ports("invalid.host", 1, 1); - assert!(!no_ports); - } - - #[test] - fn test_get_port_usage_stats() { - let stats = PortManager::get_port_usage_stats("127.0.0.1"); - - assert_eq!(stats.start_port, DEFAULT_PORT_START); - assert_eq!(stats.end_port, DEFAULT_PORT_END); - assert_eq!(stats.total_ports, (DEFAULT_PORT_END - DEFAULT_PORT_START + 1) as usize); - assert_eq!(stats.available_count + stats.used_count, stats.total_ports); - assert!(stats.usage_percentage >= 0.0); - assert!(stats.usage_percentage <= 100.0); - } -} \ No newline at end of file diff --git a/runagent/__version__.py b/runagent/__version__.py index a1e8857..9eb734d 100644 --- a/runagent/__version__.py +++ b/runagent/__version__.py @@ -1,5 +1 @@ -<<<<<<< HEAD __version__ = "0.1.23" -======= -__version__ = "0.1.23" ->>>>>>> sawra/runagent_cloud_support diff --git a/runagent/cli/branding.py b/runagent/cli/branding.py new file mode 100644 index 0000000..c52a17e --- /dev/null +++ b/runagent/cli/branding.py @@ -0,0 +1,113 @@ +""" +CLI Branding - ASCII art logo and styling for RunAgent +""" + +from rich.console import Console + +console = Console() + + +def print_logo(show_tagline: bool = True, brand_color: str = "cyan"): + """ + Print the RunAgent ASCII art logo + "Run" in brand color (cyan), "Agent" in white + + Args: + show_tagline: Whether to show the tagline below the logo + brand_color: Brand color for "Run" part (default: cyan) + """ + # Split logo into "Run" part (cyan) and "Agent" part (white) + logo = f"""[dim]╔═══════════════════════════════════════════════════════════════╗ +║ ║[/dim] +[bold {brand_color}]║ ██████╗ ██╗ ██╗███╗ ██╗[/bold {brand_color}][bold white] █████╗ ██████╗ ███████╗███╗ ██╗████████╗[/bold white] +[bold {brand_color}]║ ██╔══██╗██║ ██║████╗ ██║[/bold {brand_color}][bold white]██╔══██╗██╔════╝ ██╔════╝████╗ ██║╚══██╔══╝[/bold white] +[bold {brand_color}]║ ██████╔╝██║ ██║██╔██╗ ██║[/bold {brand_color}][bold white]███████║██║ ███╗█████╗ ██╔██╗ ██║ ██║ [/bold white] +[bold {brand_color}]║ ██╔══██╗██║ ██║██║╚██╗██║[/bold {brand_color}][bold white]██╔══██║██║ ██║██╔══╝ ██║╚██╗██║ ██║ [/bold white] +[bold {brand_color}]║ ██║ ██║╚██████╔╝██║ ╚████║[/bold {brand_color}][bold white]██║ ██║╚██████╔╝███████╗██║ ╚████║ ██║ [/bold white] +[bold {brand_color}]║ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═══╝[/bold {brand_color}][bold white]╚═╝ ╚═╝ ╚═════╝ ╚══════╝╚═╝ ╚═══╝ ╚═╝ [/bold white] +[dim]║ ║ +╚═══════════════════════════════════════════════════════════════╝[/dim]""" + + console.print(logo, highlight=False) + + if show_tagline: + console.print(f"[dim] Deploy and manage AI agents with ease 🚀[/dim]\n") + + +def print_compact_logo(brand_color: str = "cyan"): + """ + Print a compact version of the RunAgent logo for smaller spaces + "Run" in brand color, "Agent" in white + + Args: + brand_color: Brand color for "Run" part (default: cyan) + """ + logo = f""" +[bold {brand_color}] ██████╗ ██╗ ██╗███╗ ██╗[/bold {brand_color}][bold white] █████╗ ██████╗ ███████╗███╗ ██╗████████╗[/bold white] +[bold {brand_color}] ██╔══██╗██║ ██║████╗ ██║[/bold {brand_color}][bold white]██╔══██╗██╔════╝ ██╔════╝████╗ ██║╚══██╔══╝[/bold white] +[bold {brand_color}] ██████╔╝██║ ██║██╔██╗ ██║[/bold {brand_color}][bold white]███████║██║ ███╗█████╗ ██╔██╗ ██║ ██║ [/bold white] +[bold {brand_color}] ██╔══██╗██║ ██║██║╚██╗██║[/bold {brand_color}][bold white]██╔══██║██║ ██║██╔══╝ ██║╚██╗██║ ██║ [/bold white] +[bold {brand_color}] ██║ ██║╚██████╔╝██║ ╚████║[/bold {brand_color}][bold white]██║ ██║╚██████╔╝███████╗██║ ╚████║ ██║ [/bold white] +[bold {brand_color}] ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═══╝[/bold {brand_color}][bold white]╚═╝ ╚═╝ ╚═════╝ ╚══════╝╚═╝ ╚═══╝ ╚═╝ [/bold white]""" + console.print(logo, highlight=False) + + +def print_minimal_logo(brand_color: str = "cyan"): + """ + Print a minimal single-line logo + "Run" in brand color, "Agent" in white + + Args: + brand_color: Brand color for "Run" part (default: cyan) + """ + console.print(f"[bold {brand_color}]Run[/bold {brand_color}][bold white]Agent[/bold white] [dim]|[/dim] [dim]Deploy AI agents with ease 🚀[/dim]") + + +def print_header(command_name: str = None, brand_color: str = "cyan"): + """ + Print a simple header bar like a webpage header + Perfect for internal pages without overwhelming the user + + Args: + command_name: Optional command name to show (e.g., "Configuration", "Database") + brand_color: Brand color for "Run" part (default: cyan) + """ + # Top border + console.print(f"[dim]{'─' * 70}[/dim]") + + # Header content + if command_name: + console.print( + f"[bold {brand_color}]Run[/bold {brand_color}][bold white]Agent[/bold white] " + f"[dim]›[/dim] {command_name}" + ) + else: + console.print( + f"[bold {brand_color}]Run[/bold {brand_color}][bold white]Agent[/bold white] " + f"[dim]CLI[/dim]" + ) + + # Bottom border + console.print(f"[dim]{'─' * 70}[/dim]\n") + + +def print_welcome_banner(version: str = None): + """ + Print a welcome banner with logo and version + + Args: + version: Version string to display + """ + print_logo(show_tagline=True, brand_color="cyan") + + if version: + console.print(f"[dim] Version {version}[/dim]\n") + else: + console.print() + + +def print_setup_banner(): + """Print a special banner for the setup command""" + print_logo(show_tagline=False, brand_color="cyan") + console.print("[bold cyan] 🎉 Welcome to RunAgent! 🎉[/bold cyan]") + console.print("[dim] Let's get you set up in a few steps...[/dim]\n") diff --git a/runagent/cli/commands.py b/runagent/cli/commands.py deleted file mode 100644 index ff33016..0000000 --- a/runagent/cli/commands.py +++ /dev/null @@ -1,1931 +0,0 @@ -""" -CLI commands that use the restructured SDK internally. -""" -import os -import json -import uuid - -from pathlib import Path - -import click -from rich.console import Console -from rich.table import Table - -from runagent import RunAgent -from runagent.sdk.exceptions import ( # RunAgentError,; ConnectionError - AuthenticationError, - TemplateError, - ValidationError, -) -from runagent.client.client import RunAgentClient -from runagent.sdk.server.local_server import LocalServer -from runagent.utils.agent import detect_framework -from runagent.utils.animation import show_subtle_robotic_runner, show_quick_runner -from runagent.utils.config import Config -from runagent.sdk.deployment.middleware_sync import get_middleware_sync -from runagent.cli.utils import add_framework_options, get_selected_framework -from runagent.utils.enums.framework import Framework -console = Console() - - -def format_error_message(error_info): - """Format error information from API responses""" - if isinstance(error_info, dict) and "message" in error_info: - # New format with ErrorDetail object - error_message = error_info.get("message", "Unknown error") - error_code = error_info.get("code", "UNKNOWN_ERROR") - return f"[{error_code}] {error_message}" - else: - # Fallback to old format for backward compatibility - return str(error_info) if error_info else "Unknown error" - - -def print_version(ctx, param, value): - """Custom version callback with colored output""" - if not value or ctx.resilient_parsing: - return - try: - from runagent.__version__ import __version__ - console.print(f"[bold cyan]runagent {__version__}[/bold cyan]") - except ImportError: - console.print("[red]runagent version unknown[/red]") - ctx.exit() - - -@click.command() -def version(): - """Show version information""" - try: - from runagent.__version__ import __version__ - console.print(f"[bold cyan]runagent {__version__}[/bold cyan]") - except ImportError: - console.print("[red]runagent version unknown[/red]") - -@click.command() -@click.option("--api-key", required=True, help="Your API key") -@click.option("--base-url", help="API base URL") -@click.option("--force", is_flag=True, help="Force reconfiguration") -def setup(api_key, base_url, force): - """Setup RunAgent authentication""" - try: - sdk = RunAgent() - - # Check if already configured - if sdk.is_configured() and not force: - config_status = sdk.get_config_status() - console.print("⚠️ RunAgent is already configured:") - console.print(f" Base URL: [blue]{config_status.get('base_url')}[/blue]") - user_info = config_status.get('user_info', {}) - if user_info.get('email'): - console.print(f" User: [green]{user_info.get('email')}[/green]") - - if not click.confirm("Do you want to reconfigure?"): - return - - console.print("🔑 [cyan]Setting up RunAgent authentication...[/cyan]") - - # Configure SDK with validation - try: - sdk.configure(api_key=api_key, base_url=base_url, save=True) - console.print("✅ [green]Setup completed successfully![/green]") - except AuthenticationError as auth_err: - if os.getenv('DISABLE_TRY_CATCH'): - raise - console.print(f"❌ [red]Authentication failed:[/red] {auth_err}") - - # Provide specific troubleshooting based on error message - error_msg = str(auth_err).lower() - console.print("\n💡 [yellow]Troubleshooting:[/yellow]") - - if "invalid api key" in error_msg or "not authenticated" in error_msg: - console.print(" • Check that your API key is correct") - console.print(" • Verify the API key is not expired") - console.print(" • Ensure you have access to the middleware") - elif "connection" in error_msg or "timeout" in error_msg: - console.print(" • Check your internet connection") - console.print(" • Verify the middleware server is accessible") - console.print(f" • Trying to connect to: {base_url or sdk.config.base_url}") - else: - console.print(" • Check your API key and network connection") - console.print(" • Contact support if the issue persists") - - raise click.ClickException("Authentication failed") - - # Show user information (from cached data) - config_status = sdk.get_config_status() - user_info = config_status.get('user_info', {}) - - if user_info and user_info.get('email'): - console.print("\n👤 [bold]User Information:[/bold]") - console.print(f" Email: [cyan]{user_info.get('email')}[/cyan]") - if user_info.get('user_id'): - console.print(f" User ID: [dim]{user_info.get('user_id')}[/dim]") - if user_info.get('tier'): - console.print(f" Tier: [yellow]{user_info.get('tier')}[/yellow]") - - # Show sync status (simplified) - console.print("\n🔄 [bold]Middleware Sync Status:[/bold]") - try: - from runagent.sdk.deployment.middleware_sync import MiddlewareSyncService - sync_service = MiddlewareSyncService(sdk.config) - - if sync_service.is_sync_enabled(): - console.print(" Status: [green]✅ ENABLED[/green]") - console.print(" 📊 Local agent runs will sync to middleware") - else: - console.print(" Status: [yellow]⚠️ DISABLED[/yellow]") - console.print(" 📊 Only local storage will be used") - - except Exception as e: - console.print(f" Status: [yellow]Unknown - {e}[/yellow]") - - # Show next steps - console.print("\n💡 [bold]Next Steps:[/bold]") - console.print(" • Test with a local agent: [cyan]runagent serve [/cyan]") - console.print(" • Check middleware sync: [cyan]runagent local-sync --status[/cyan]") - console.print(" • Upload agent to middleware: [cyan]runagent upload --folder [/cyan]") - - except AuthenticationError: - # Already handled above - raise - except Exception as e: - if os.getenv('DISABLE_TRY_CATCH'): - raise - console.print(f"❌ [red]Setup error:[/red] {e}") - raise click.ClickException("Setup failed") - - - -@click.command() -@click.option("--yes", is_flag=True, help="Skip confirmation") -def teardown(yes): - """Remove RunAgent configuration""" - try: - sdk = RunAgent() - - if not yes: - config_status = sdk.get_config_status() - if config_status.get("configured"): - console.print("📋 [bold]Current configuration:[/bold]") - console.print( - f" Base URL: [blue]{config_status.get('base_url')}[/blue]" - ) - user_info = config_status.get("user_info", {}) - if user_info.get("email"): - console.print(f" User: [green]{user_info.get('email')}[/green]") - - if not click.confirm( - "⚠️ This will remove all RunAgent configuration. Continue?" - ): - console.print("Teardown cancelled.") - return - - # Clear configuration - sdk.config.clear() - - console.print("✅ [green]RunAgent teardown completed successfully![/green]") - console.print( - "💡 Run [cyan]'runagent setup --api-key '[/cyan] to reconfigure" - ) - - except Exception as e: - if os.getenv('DISABLE_TRY_CATCH'): - raise - console.print(f"❌ [red]Teardown error:[/red] {e}") - raise click.ClickException("Teardown failed") - - -@click.command() -@click.option("--id", "agent_id", required=True, help="Agent ID to delete") -@click.option("--yes", is_flag=True, help="Skip confirmation") -def delete(agent_id, yes): - """Delete an agent from the local database""" - try: - sdk = RunAgent() - - # Get agent info first - agent = sdk.db_service.get_agent(agent_id) - if not agent: - console.print(f"❌ [red]Agent {agent_id} not found in database[/red]") - - # Show available agents - console.print("\n💡 Available agents:") - agents = sdk.db_service.list_agents() - if agents: - table = Table(title="Available Agents") - table.add_column("Agent ID", style="magenta") - table.add_column("Framework", style="green") - table.add_column("Status", style="yellow") - table.add_column("Deployed At", style="dim") - - for agent in agents[:10]: # Show first 10 - table.add_row( - agent['agent_id'][:8] + "...", - agent['framework'], - agent['status'], - agent['deployed_at'] or "Unknown" - ) - console.print(table) - else: - console.print(" No agents found in database") - - raise click.ClickException("Agent not found") - - # Show agent details - console.print(f"\n🔍 [yellow]Agent to be deleted:[/yellow]") - console.print(f" Agent ID: [bold magenta]{agent['agent_id']}[/bold magenta]") - console.print(f" Framework: [green]{agent['framework']}[/green]") - console.print(f" Path: [blue]{agent['agent_path']}[/blue]") - console.print(f" Status: [yellow]{agent['status']}[/yellow]") - console.print(f" Deployed: [dim]{agent['deployed_at']}[/dim]") - console.print(f" Total Runs: [cyan]{agent['run_count']}[/cyan]") - - # Confirmation - if not yes: - if not click.confirm("\n⚠️ This will permanently delete the agent from the database. Continue?"): - console.print("Deletion cancelled.") - return - - # Delete the agent - result = sdk.db_service.force_delete_agent(agent_id) - - if result["success"]: - console.print(f"\n✅ [green]Agent {agent_id} deleted successfully![/green]") - - # Show updated capacity - capacity_info = sdk.db_service.get_database_capacity_info() - console.print(f"📊 Updated capacity: [cyan]{capacity_info.get('current_count', 0)}/5[/cyan] agents") - else: - console.print(f"❌ [red]Failed to delete agent:[/red] {format_error_message(result.get('error'))}") - import sys - sys.exit(1) - - except Exception as e: - if os.getenv('DISABLE_TRY_CATCH'): - raise - console.print(f"❌ [red]Delete error:[/red] {e}") - import sys - sys.exit(1) - - -@click.command() -@click.option("--template", default="default", help="Template variant (basic, advanced, default)") -@click.option("--interactive", "-i", is_flag=True, help="Enable interactive prompts") -@click.option("--overwrite", is_flag=True, help="Overwrite existing folder") -@add_framework_options # This automatically adds all framework options! -@click.argument( - "path", - type=click.Path( - file_okay=False, - dir_okay=True, - readable=True, - resolve_path=True, - path_type=Path, - ), - default=".", - required=False, -) -def init(template, interactive, overwrite, path, **kwargs): - """Initialize a new RunAgent project""" - - try: - sdk = RunAgent() - - # Extract selected framework using our helper - selected_framework = get_selected_framework(kwargs) - framework = selected_framework if selected_framework else Framework.DEFAULT - - if interactive: - if framework == Framework.DEFAULT: - console.print("🎯 [bold]Available frameworks:[/bold]") - selectable_frameworks = Framework.get_selectable_frameworks() - - for i, fw in enumerate(selectable_frameworks, 1): - category_emoji = "🐍" if fw.is_pythonic() else "🌐" if fw.is_webhook() else "❓" - console.print(f" {i}. {category_emoji} {fw.value} ({fw.category})") - - choice = click.prompt( - "Select framework", - type=click.IntRange(1, len(selectable_frameworks)), - default=1 - ) - framework = selectable_frameworks[choice - 1] - - if template == "default": - templates = sdk.list_templates(framework.value) - template_list = templates.get(framework.value, ["default"]) - - console.print(f"\n🧱 [bold]Available templates for {framework.value}:[/bold]") - for i, tmpl in enumerate(template_list, 1): - console.print(f" {i}. {tmpl}") - - choice = click.prompt( - "Select template", - type=click.IntRange(1, len(template_list)), - default=1 - ) - template = template_list[choice - 1] - - if path.resolve() == Path.cwd(): - project_name = click.prompt( - "Enter project name", - type=str, - default="runagent-project" - ) - path = Path.cwd() / project_name - - # Validate framework if it came from string input - if isinstance(framework, str): - try: - framework = Framework.from_string(framework) - except ValueError as e: - raise click.UsageError(str(e)) - - # Use the path as the project location - project_path = path.resolve() - relative_project_path = project_path.relative_to(Path.cwd()) - - # Ensure the path exists - project_path.parent.mkdir(parents=True, exist_ok=True) - - # Show configuration with enhanced formatting - console.print(f"\n🚀 [bold]Initializing project:[/bold]") - console.print(f" Path: [cyan]{relative_project_path}[/cyan]") - - # Enhanced framework display with category - framework_display = framework.value - if not framework.is_default(): - category_emoji = "🐍" if framework.is_pythonic() else "🌐" if framework.is_webhook() else "❓" - framework_display = f"{category_emoji} {framework.value} ({framework.category})" - - console.print(f" Framework: [magenta]{framework_display}[/magenta]") - console.print(f" Template: [yellow]{template}[/yellow]") - - # Initialize project - success = sdk.init_project( - folder_path=project_path, - framework=framework.value, # Pass the string value - template=template, - overwrite=overwrite - ) - - if success: - console.print(f"\n✅ [green]Project initialized successfully![/green]") - console.print(f"📁 Created at: [cyan]{relative_project_path}[/cyan]") - - # Enhanced next steps with framework-specific guidance - console.print("\n📝 [bold]Next steps:[/bold]") - console.print(f" 1. [cyan]cd {relative_project_path}[/cyan]") - console.print(f" 2. Update your API keys in [yellow].env[/yellow] file") - - # Framework-specific guidance - if framework.is_pythonic(): - console.print(f" 3. Install dependencies: [cyan]pip install -r requirements.txt[/cyan]") - console.print(f" 4. Deploy locally: [cyan]runagent serve {relative_project_path}[/cyan]") - elif framework.is_webhook(): - console.print(f" 3. Configure webhook endpoints in your workflow") - console.print(f" 4. Deploy locally: [cyan]runagent serve {relative_project_path}[/cyan]") - else: - console.print(f" 3. Deploy locally: [cyan]runagent serve {relative_project_path}[/cyan]") - - console.print( - f" 5. Test: [cyan]Test the agent with any of our SDKs. For more details, refer to: [link]https://docs.run-agent.ai/sdk/overview[/link][/cyan]" - ) - - except TemplateError as e: - if os.getenv('DISABLE_TRY_CATCH'): - raise - console.print(f"❌ [red]Template error:[/red] {e}") - raise click.ClickException("Project initialization failed") - except FileExistsError as e: - if os.getenv('DISABLE_TRY_CATCH'): - raise - console.print(f"❌ [red]Path exists:[/red] {e}") - console.print("💡 Use [cyan]--overwrite[/cyan] to force initialization") - raise click.ClickException("Project initialization failed") - except click.UsageError: - # Re-raise UsageError as-is for proper click handling - raise - except Exception as e: - if os.getenv('DISABLE_TRY_CATCH'): - raise - console.print(f"❌ [red]Initialization error:[/red] {e}") - raise click.ClickException("Project initialization failed") - - -@click.command() -@click.option( - "--list", "action_list", is_flag=True, help="List all available templates" -) -@click.option( - "--info", "action_info", is_flag=True, help="Get detailed template information" -) -@click.option("--framework", help="Framework name (required for --info)") -@click.option("--template", help="Template name (required for --info)") -@click.option("--filter-framework", help="Filter templates by framework") -@click.option( - "--format", - type=click.Choice(["table", "json"]), - default="table", - help="Output format", -) -def template(action_list, action_info, framework, template, filter_framework, format): - """Manage project templates""" - - if not action_list and not action_info: - console.print( - "❌ Please specify either [cyan]--list[/cyan] or [cyan]--info[/cyan]" - ) - raise click.ClickException("No action specified") - - try: - sdk = RunAgent() - - if action_list: - templates = sdk.list_templates(framework=filter_framework) - - if format == "json": - console.print(json.dumps(templates, indent=2)) - else: - console.print("📋 [bold cyan]Available Templates:[/bold cyan]") - for framework_name, template_list in templates.items(): - console.print(f"\n🎯 [bold blue]{framework_name}:[/bold blue]") - for tmpl in template_list: - console.print(f" • {tmpl}") - - console.print( - f"\n💡 Use [cyan]'runagent template --info --framework --template '[/cyan] for details" - ) - - elif action_info: - if not framework or not template: - console.print( - "❌ Both [cyan]--framework[/cyan] and [cyan]--template[/cyan] are required for --info" - ) - raise click.ClickException("Missing required parameters") - - template_info = sdk.get_template_info(framework, template) - - if template_info: - console.print( - f"📋 [bold cyan]Template: {framework}/{template}[/bold cyan]" - ) - console.print( - f"Framework: [magenta]{template_info['framework']}[/magenta]" - ) - console.print(f"Template: [yellow]{template_info['template']}[/yellow]") - - if "metadata" in template_info: - metadata = template_info["metadata"] - if "description" in metadata: - console.print(f"Description: {metadata['description']}") - if "requirements" in metadata: - console.print( - f"Requirements: {', '.join(metadata['requirements'])}" - ) - - console.print(f"\n📁 [bold]Structure:[/bold]") - console.print(f"Files: {', '.join(template_info['files'])}") - if template_info.get("directories"): - console.print( - f"Directories: {', '.join(template_info['directories'])}" - ) - - if "readme" in template_info: - console.print(f"\n📖 [bold]README:[/bold]") - console.print("-" * 50) - console.print( - template_info["readme"][:500] + "..." - if len(template_info["readme"]) > 500 - else template_info["readme"] - ) - console.print("-" * 50) - - console.print(f"\n🚀 [bold]To use this template:[/bold]") - console.print( - f"[cyan]runagent init --framework {framework} --template {template}[/cyan]" - ) - else: - console.print( - f"❌ Template [yellow]{framework}/{template}[/yellow] not found" - ) - - # Show available templates - templates = sdk.list_templates() - if framework in templates: - console.print( - f"Available templates for {framework}: {', '.join(templates[framework])}" - ) - else: - console.print( - f"Available frameworks: {', '.join(templates.keys())}" - ) - - except Exception as e: - if os.getenv('DISABLE_TRY_CATCH'): - raise - console.print(f"❌ [red]Template error:[/red] {e}") - raise click.ClickException("Template operation failed") - - -@click.command() -@click.argument( - "path", - type=click.Path( - exists=True, - file_okay=False, - dir_okay=True, - readable=True, - resolve_path=True, - path_type=Path, - ), - default=".", -) -def upload(path: Path): - """Upload agent to remote server""" - - try: - sdk = RunAgent() - - # Check authentication - if not sdk.is_configured(): - console.print( - "❌ [red]Not authenticated.[/red] Run [cyan]'runagent setup --api-key '[/cyan] first" - ) - raise click.ClickException("Authentication required") - - # Validate folder - if not Path(path).exists(): - raise click.ClickException(f"Folder not found: {path}") - - console.print(f"📤 [bold]Uploading agent...[/bold]") - console.print(f"📁 Source: [cyan]{path}[/cyan]") - - # Upload agent (framework auto-detected) - result = sdk.upload_agent(folder=path) - - if result.get("success"): - agent_id = result["agent_id"] - console.print(f"\n✅ [green]Upload successful![/green]") - console.print(f"🆔 Agent ID: [bold magenta]{agent_id}[/bold magenta]") - console.print(f"\n💡 [bold]Next step:[/bold]") - console.print(f"[cyan]runagent start --id {agent_id}[/cyan]") - else: - console.print(f"❌ [red]Upload failed:[/red] {format_error_message(result.get('error'))}") - import sys - sys.exit(1) - - except AuthenticationError as e: - if os.getenv('DISABLE_TRY_CATCH'): - raise - console.print(f"❌ [red]Authentication error:[/red] {e}") - import sys - sys.exit(1) - except Exception as e: - if os.getenv('DISABLE_TRY_CATCH'): - raise - console.print(f"❌ [red]Upload error:[/red] {e}") - import sys - sys.exit(1) - - -@click.command() -@click.option("--id", "agent_id", required=True, help="Agent ID to start") -@click.option("--config", help="JSON configuration for deployment") -def start(agent_id, config): - """Start an uploaded agent on remote server""" - - try: - sdk = RunAgent() - - # Check authentication - if not sdk.is_configured(): - console.print( - "❌ [red]Not authenticated.[/red] Run [cyan]'runagent setup --api-key '[/cyan] first" - ) - raise click.ClickException("Authentication required") - - # Parse config - config_dict = {} - if config: - try: - config_dict = json.loads(config) - except json.JSONDecodeError: - if os.getenv('DISABLE_TRY_CATCH'): - raise - raise click.ClickException("Invalid JSON in config parameter") - - console.print(f"🚀 [bold]Starting agent...[/bold]") - console.print(f"🆔 Agent ID: [magenta]{agent_id}[/magenta]") - - # Start agent - result = sdk.start_remote_agent(agent_id, config_dict) - - if result.get("success"): - console.print(f"\n✅ [green]Agent started successfully![/green]") - console.print(f"🌐 Endpoint: [link]{result.get('endpoint')}[/link]") - else: - console.print(f"❌ [red]Start failed:[/red] {format_error_message(result.get('error'))}") - import sys - sys.exit(1) - - except AuthenticationError as e: - if os.getenv('DISABLE_TRY_CATCH'): - raise - console.print(f"❌ [red]Authentication error:[/red] {e}") - import sys - sys.exit(1) - except Exception as e: - if os.getenv('DISABLE_TRY_CATCH'): - raise - console.print(f"❌ [red]Start error:[/red] {e}") - import sys - sys.exit(1) - - -@click.command() -@click.argument( - "path", - type=click.Path( - exists=True, - file_okay=False, - dir_okay=True, - readable=True, - resolve_path=True, - path_type=Path, - ), - default=".", -) -def deploy(path: Path): - """Deploy agent (upload + start) to remote server""" - - try: - sdk = RunAgent() - - # Check authentication - if not sdk.is_configured(): - console.print( - "❌ [red]Not authenticated.[/red] Run [cyan]'runagent setup --api-key '[/cyan] first" - ) - raise click.ClickException("Authentication required") - - # Validate folder - if not Path(path).exists(): - raise click.ClickException(f"Folder not found: {path}") - - console.print(f"🎯 [bold]Deploying agent (upload + start)...[/bold]") - console.print(f"📁 Source: [cyan]{path}[/cyan]") - - # Deploy agent (framework auto-detected) - result = sdk.deploy_remote(folder=str(path)) - - if result.get("success"): - console.print(f"\n✅ [green]Deployment successful![/green]") - console.print(f"🆔 Agent ID: [bold magenta]{result.get('agent_id')}[/bold magenta]") - console.print(f"🌐 Endpoint: [link]{result.get('endpoint')}[/link]") - else: - console.print(f"❌ [red]Deployment failed:[/red] {format_error_message(result.get('error'))}") - import sys - sys.exit(1) - - except AuthenticationError as e: - if os.getenv('DISABLE_TRY_CATCH'): - raise - console.print(f"❌ [red]Authentication error:[/red] {e}") - import sys - sys.exit(1) - except Exception as e: - if os.getenv('DISABLE_TRY_CATCH'): - raise - console.print(f"❌ [red]Deployment error:[/red] {e}") - import sys - sys.exit(1) - - - -@click.command() -@click.option("--port", type=int, help="Preferred port (auto-allocated if unavailable)") -@click.option("--host", default="127.0.0.1", help="Host to bind server to") -@click.option("--debug", is_flag=True, help="Run server in debug mode") -@click.option("--replace", help="Replace existing agent with this agent ID") -@click.option("--no-animation", is_flag=True, help="Skip startup animation") -@click.option("--animation-style", - type=click.Choice(["field", "ascii", "minimal", "quick"]), - default="field", - help="Animation style") -@click.argument( - "path", - type=click.Path( - exists=True, - file_okay=False, - dir_okay=True, - readable=True, - resolve_path=True, - path_type=Path, - ), - default=".", -) -def serve(port, host, debug, replace, no_animation, animation_style, path): - """Start local FastAPI server with subtle robotic runner animation""" - - try: - # Show subtle startup animation - if not no_animation: - console.print("\n") - - if animation_style == "quick": - show_quick_runner(duration=1.5) - else: - show_subtle_robotic_runner(duration=2.0, style=animation_style) - - sdk = RunAgent() - - # Handle replace operation - if replace: - console.print(f"🔄 [yellow]Replacing agent: {replace}[/yellow]") - - # Check if the agent to replace exists - existing_agent = sdk.db_service.get_agent(replace) - if not existing_agent: - console.print(f"⚠️ [yellow]Agent {replace} not found in database[/yellow]") - console.print("💡 Available agents:") - agents = sdk.db_service.list_agents() - for agent in agents[:5]: # Show first 5 - console.print(f" • {agent['agent_id']} ({agent['framework']})") - raise click.ClickException("Agent to replace not found") - - # Generate new agent ID - import uuid - new_agent_id = str(uuid.uuid4()) - - # Get currently used ports to avoid conflicts - used_ports = [] - all_agents = sdk.db_service.list_agents() - for agent in all_agents: - if agent.get('port') and agent['agent_id'] != replace: # Exclude the agent being replaced - used_ports.append(agent['port']) - - # Allocate host and port - from runagent.utils.port import PortManager - if port and PortManager.is_port_available(host, port): - allocated_host = host - allocated_port = port - console.print(f"🎯 Using specified address: [blue]{allocated_host}:{allocated_port}[/blue]") - else: - allocated_host, allocated_port = PortManager.allocate_unique_address(used_ports) - console.print(f"🔌 Auto-allocated address: [blue]{allocated_host}:{allocated_port}[/blue]") - - # Use the existing replace_agent method with proper port allocation - result = sdk.db_service.replace_agent( - old_agent_id=replace, - new_agent_id=new_agent_id, - agent_path=str(path), - host=allocated_host, - port=allocated_port, # Ensure port is not None - framework=detect_framework(path).value, - ) - - if not result["success"]: - raise click.ClickException(f"Failed to replace agent: {result['error']}") - - console.print(f"✅ [green]Agent replaced successfully![/green]") - console.print(f"🆔 New Agent ID: [bold magenta]{new_agent_id}[/bold magenta]") - console.print(f"🔌 Address: [bold blue]{allocated_host}:{allocated_port}[/bold blue]") - - # Create server with the new agent ID and allocated host/port - from runagent.sdk.db import DBService - db_service = DBService() - - server = LocalServer( - db_service=db_service, - agent_id=new_agent_id, - agent_path=path, - port=allocated_port, - host=allocated_host, - ) - else: - # Normal operation - check capacity if not replacing - capacity_info = sdk.db_service.get_database_capacity_info() - if capacity_info["is_full"] and not replace: - console.print("❌ [red]Database is full![/red]") - oldest_agent = capacity_info.get("oldest_agent", {}) - if oldest_agent: - console.print(f"💡 [yellow]Suggested commands:[/yellow]") - console.print(f" Replace: [cyan]runagent serve {path} --replace {oldest_agent.get('agent_id', '')}[/cyan]") - console.print(f" Delete: [cyan]runagent delete --id {oldest_agent.get('agent_id', '')}[/cyan]") - raise click.ClickException("Database at capacity. Use --replace or use 'runagent delete' to free space.") - - console.print("⚡ [bold]Starting local server with auto port allocation...[/bold]") - - # Use the existing LocalServer.from_path method - server = LocalServer.from_path(path, port=port, host=host) - - # Common server startup code - allocated_host = server.host - allocated_port = server.port - - console.print(f"🌐 URL: [bold blue]http://{allocated_host}:{allocated_port}[/bold blue]") - console.print(f"📖 Docs: [link]http://{allocated_host}:{allocated_port}/docs[/link]") - - try: - - sync_service = get_middleware_sync() - sync_enabled = sync_service.is_sync_enabled() - api_key_set = bool(Config.get_api_key()) - - console.print(f"\n🔄 [bold]Middleware Sync Status:[/bold]") - if sync_enabled: - console.print(f" Status: [green]✅ ENABLED[/green]") - console.print(f" 📊 Local invocations will sync to middleware") - - # Test connection - try: - test_result = sync_service.test_connection() - if test_result.get("success"): - console.print(f" Connection: [green]✅ Connected to middleware[/green]") - else: - console.print(f" Connection: [red]❌ Failed to connect: {test_result.get('error', 'Unknown error')}[/red]") - except Exception as e: - if os.getenv('DISABLE_TRY_CATCH'): - raise - console.print(f" Connection: [red]❌ Connection test failed: {e}[/red]") - else: - console.print(f" Status: [yellow]⚠️ DISABLED[/yellow]") - if not api_key_set: - console.print(f" Reason: [yellow]API key not configured[/yellow]") - console.print(f" 💡 Setup: [cyan]runagent setup --api-key [/cyan]") - else: - user_disabled = not Config.get_user_config().get("local_sync_enabled", True) - if user_disabled: - console.print(f" Reason: [yellow]Disabled by user[/yellow]") - console.print(f" 💡 Enable: [cyan]runagent local-sync --enable[/cyan]") - console.print(f" 📊 Local invocations will only be stored locally") - - except Exception as e: - console.print(f"[dim]Note: Could not check middleware sync status: {e}[/dim]") - - # Start server (this will block) - server.start(debug=debug) - - except KeyboardInterrupt: - console.print("\n🛑 [yellow]Server stopped by user[/yellow]") - except Exception as e: - if os.getenv('DISABLE_TRY_CATCH'): - raise - console.print(f"❌ [red]Server error:[/red] {e}") - raise click.ClickException("Server failed to start") - - -@click.command( - context_settings=dict( - ignore_unknown_options=True, - allow_extra_args=True, - )) -@click.option("--id", "agent_id", help="Agent ID to run") -@click.option("--host", help="Host to connect to (use with --port)") -@click.option("--port", type=int, help="Port to connect to (use with --host)") -@click.option( - "--input", - "input_file", - type=click.Path(exists=True, file_okay=True, dir_okay=False, readable=True, path_type=Path), - help="Path to input JSON file" -) -@click.option("--local", is_flag=True, help="Run agent locally") -@click.option("--tag", required=True, help="Entrypoint tag to be used") -# @click.option("--generic-stream", is_flag=True, help="Use generic streaming mode") -@click.option("--timeout", type=int, help="Timeout in seconds") -@click.pass_context -def run(ctx, agent_id, host, port, input_file, local, tag, timeout): - """ - Run an agent with flexible configuration options - - Examples: - # Using agent ID with extra params - runagent run --agent-id my-agent --param1=value1 --param2=value2 - - # Using host/port with input file - runagent run --host localhost --port 8080 --input config.json - - # local agent - runagent run --id d33c497d-d3f5-462e-8ff4-c28d819b92d6 --tag minimal --local --message=something - - # remote agent - runagent run --id d33c497d-d3f5-462e-8ff4-c28d819b92d6 --tag minimal --message=something - """ - - # ============================================ - # VALIDATION 1: Either agent-id OR host/port - # ============================================ - agent_id_provided = agent_id is not None - host_port_provided = host is not None or port is not None - - if agent_id_provided and host_port_provided: - raise click.UsageError( - "Cannot specify both --agent-id and --host/--port. " - "Choose one approach." - ) - - if not agent_id_provided and not host_port_provided: - raise click.UsageError( - "Must specify either --agent-id or both --host and --port." - ) - - # If using host/port, both must be provided - if host_port_provided and (host is None or port is None): - raise click.UsageError( - "When using host/port, both --host and --port must be specified." - ) - - # ============================================ - # # VALIDATION 2: tag validation - # # ============================================ - if tag.endswith("_stream"): - console.print(f"❌ [bold red]Execution failed:[/bold red] Cannot use streaming Entrypoint tag `{tag}` through non-streaming endpoint.") - return - - - # ============================================ - # VALIDATION 3: Input file OR extra params - # ============================================ - - # Parse extra parameters from ctx.args - extra_params = {} - invalid_args = [] - - for arg in ctx.args: - if arg.startswith('--') and '=' in arg: - # Valid format: --key=value - key, value = arg[2:].split('=', 1) - extra_params[key] = value - else: - # Invalid format - invalid_args.append(arg) - - if invalid_args: - raise click.UsageError( - f"Invalid extra arguments: {invalid_args}. " - "Extra parameters must be in --key=value format." - ) - - # Check mutual exclusivity of input file and extra params - if input_file and extra_params: - raise click.UsageError( - "Cannot specify both --input file and extra parameters. " - "Use either --input config.json OR --param1=value1 --param2=value2" - ) - - if not input_file and not extra_params: - console.print("⚠️ No input file or extra parameters provided. Running with defaults.") - - # ============================================ - # DISPLAY CONFIGURATION - # ============================================ - - console.print("🚀 RunAgent Configuration:") - - # Connection info - if agent_id: - console.print(f" Agent ID: [cyan]{agent_id}[/cyan]") - else: - console.print(f" Host: [cyan]{host}[/cyan]") - console.print(f" Port: [cyan]{port}[/cyan]") - - # Tag - # mode = "Generic Streaming" if generic_stream else "Generic" - console.print(f" Tag: [magenta]{tag}[/magenta]") - - # Local execution - if local: - console.print(" Local: [green]Yes[/green]") - else: - console.print(" Local: [red]No(Deployed to RunAgent Cloud)[/red]") - - # Timeout - if timeout: - console.print(f" Timeout: [yellow]{timeout}s[/yellow]") - - # Input configuration - if input_file: - console.print(f" Input file: [blue]{input_file}[/blue]") - # Load and validate JSON file here - try: - import json - with open(input_file, 'r') as f: - input_params = json.load(f) - console.print(f" Config keys: [dim]{list(input_params.keys())}[/dim]") - except json.JSONDecodeError: - if os.getenv('DISABLE_TRY_CATCH'): - raise - raise click.ClickException(f"Invalid JSON in input file: {input_file}") - except Exception as e: - if os.getenv('DISABLE_TRY_CATCH'): - raise - raise click.ClickException(f"Error reading input file: {e}") - - elif extra_params: - console.print(" Extra parameters:") - for key, value in extra_params.items(): - # Try to parse value as JSON for complex types - # TODO: Will add type inference later - console.print(f" --{key} = [green]{value}[/green]") - input_params = extra_params - - else: - input_params = {} - - # ============================================ - # EXECUTION LOGIC - # ============================================ - - try: - ra_client = RunAgentClient( - agent_id=agent_id, - local=local, - host=host, - port=port, - entrypoint_tag=tag - ) - - if tag.endswith("_stream"): - for item in ra_client.run(**input_params): - console.print(item) - else: - result = ra_client.run(**input_params) - console.print(result) - - except Exception as e: - if os.getenv('DISABLE_TRY_CATCH'): - raise - # Display error with red ❌ symbol - console.print(f"❌ [bold red]Execution failed:[/bold red] {e}") - # Exit with error code 1 instead of raising ClickException to avoid duplicate message - import sys - sys.exit(1) - - -@click.command( - context_settings=dict( - ignore_unknown_options=True, - allow_extra_args=True, - )) -@click.option("--id", "agent_id", help="Agent ID to run") -@click.option("--host", help="Host to connect to (use with --port)") -@click.option("--port", type=int, help="Port to connect to (use with --host)") -@click.option( - "--input", - "input_file", - type=click.Path(exists=True, file_okay=True, dir_okay=False, readable=True, path_type=Path), - help="Path to input JSON file" -) -@click.option("--local", is_flag=True, help="Run agent locally") -@click.option("--tag", required=True, help="Entrypoint tag to be used") -@click.option("--timeout", type=int, help="Timeout in seconds") -@click.pass_context -def run_stream(ctx, agent_id, host, port, input_file, local, tag, timeout): - """ - Stream agent execution results in real-time. - - This command connects to an agent via WebSocket and streams the execution results - as they become available, providing real-time feedback. - - Examples: - # Local streaming agent - runagent run-stream --id d33c497d-d3f5-462e-8ff4-c28d819b92d6 --tag minimal_stream --local --message=something - - # Remote streaming agent - runagent run-stream --id d33c497d-d3f5-462e-8ff4-c28d819b92d6 --tag minimal_stream --message=something - - # With input file - runagent run-stream --id d33c497d-d3f5-462e-8ff4-c28d819b92d6 --tag minimal_stream --local --input config.json - """ - - # ============================================ - # PARAMETER PARSING - # ============================================ - - extra_params = {} - for item in ctx.args: - if '=' in item: - key, value = item.split('=', 1) - # Remove leading dashes - key = key.lstrip('-') - extra_params[key] = value - else: - # Handle boolean flags - key = item.lstrip('-') - extra_params[key] = True - - # ============================================ - # VALIDATION - # ============================================ - - # VALIDATION 1: Agent ID or host/port required - if not agent_id and not (host and port): - console.print(f"❌ [bold red]Execution failed:[/bold red] Either --id or both --host and --port are required") - import sys - sys.exit(1) - - # VALIDATION 2: tag validation for streaming - if not tag.endswith("_stream"): - console.print(f"❌ [bold red]Execution failed:[/bold red] Streaming command requires entrypoint tag ending with '_stream'. Got: {tag}") - import sys - sys.exit(1) - - # ============================================ - # DISPLAY CONFIGURATION - # ============================================ - - console.print("🚀 RunAgent Streaming Configuration:") - - # Connection info - if agent_id: - console.print(f" Agent ID: [cyan]{agent_id}[/cyan]") - else: - console.print(f" Host: [cyan]{host}[/cyan]") - console.print(f" Port: [cyan]{port}[/cyan]") - - # Tag - console.print(f" Tag: [magenta]{tag}[/magenta]") - - # Local execution - if local: - console.print(" Local: [green]Yes[/green]") - else: - console.print(" Local: [red]No (Deployed to RunAgent Cloud)[/red]") - - # Timeout - if timeout: - console.print(f" Timeout: [yellow]{timeout}s[/yellow]") - - # Input configuration - if input_file: - console.print(f" Input file: [blue]{input_file}[/blue]") - # Load and validate JSON file here - try: - import json - with open(input_file, 'r') as f: - input_params = json.load(f) - console.print(f" Config keys: [dim]{list(input_params.keys())}[/dim]") - except json.JSONDecodeError: - if os.getenv('DISABLE_TRY_CATCH'): - raise - console.print(f"❌ [bold red]Execution failed:[/bold red] Invalid JSON in input file: {input_file}") - import sys - sys.exit(1) - except Exception as e: - if os.getenv('DISABLE_TRY_CATCH'): - raise - console.print(f"❌ [bold red]Execution failed:[/bold red] Error reading input file: {e}") - import sys - sys.exit(1) - - elif extra_params: - console.print(" Extra parameters:") - for key, value in extra_params.items(): - console.print(f" --{key} = {value}") - input_params = extra_params - - else: - input_params = {} - - # ============================================ - # EXECUTION LOGIC - # ============================================ - - try: - ra_client = RunAgentClient( - agent_id=agent_id, - local=local, - host=host, - port=port, - entrypoint_tag=tag - ) - - console.print(f"\n🔄 [bold]Starting streaming execution...[/bold]") - console.print(f"📡 [dim]Connected to agent via WebSocket[/dim]") - console.print(f"📤 [dim]Streaming results:[/dim]\n") - - # Stream the results - for chunk in ra_client.run_stream(**input_params): - console.print(chunk) - - except Exception as e: - if os.getenv('DISABLE_TRY_CATCH'): - raise - # Display error with red ❌ symbol - console.print(f"❌ [bold red]Streaming failed:[/bold red] {e}") - # Exit with error code 1 instead of raising ClickException to avoid duplicate message - import sys - sys.exit(1) - - -@click.group() -def db(): - """Database management and monitoring commands""" - pass - -@db.command() -@click.option("--cleanup-days", type=int, help="Clean up records older than N days") -@click.option("--agent-id", help="Show detailed info for specific agent") -@click.option("--capacity", is_flag=True, help="Show detailed capacity information") -def status(cleanup_days, agent_id, capacity): - """Show local database status and statistics (ENHANCED with invocation stats)""" - try: - sdk = RunAgent() - - if capacity: - # Show detailed capacity info - capacity_info = sdk.db_service.get_database_capacity_info() - - console.print(f"\n📊 [bold]Database Capacity Information[/bold]") - console.print( - f"Current: [cyan]{capacity_info.get('current_count', 0)}/5[/cyan] agents" - ) - console.print( - f"Remaining slots: [green]{capacity_info.get('remaining_slots', 0)}[/green]" - ) - - status = "🔴 FULL" if capacity_info.get("is_full") else "🟢 Available" - console.print(f"Status: {status}") - - agents = capacity_info.get("agents", []) - if agents: - console.print(f"\n📋 [bold]Deployed Agents (by age):[/bold]") - - # Create table for agents - table = Table(title="Agents by Deployment Age") - table.add_column("#", style="dim", width=3) - table.add_column("Status", width=6) - table.add_column("Agent ID", style="magenta", width=36) - table.add_column("Framework", style="green", width=12) - table.add_column("Deployed At", style="cyan", width=20) - table.add_column("Age Note", style="yellow", width=10) - - for i, agent in enumerate(agents): - status_icon = ( - "🟢" - if agent["status"] == "deployed" - else "🔴" if agent["status"] == "error" else "🟡" - ) - age_label = ( - "oldest" - if i == 0 - else "newest" if i == len(agents) - 1 else "" - ) - - table.add_row( - str(i+1), - status_icon, - agent['agent_id'], - agent['framework'], - agent['deployed_at'] or "Unknown", - age_label - ) - - console.print(table) - - if capacity_info.get("is_full"): - oldest = capacity_info.get("oldest_agent", {}) - console.print( - f"\n💡 [yellow]To deploy new agent, replace oldest:[/yellow]" - ) - console.print( - f" [cyan]runagent serve --folder --replace {oldest.get('agent_id', '')}[/cyan]" - ) - console.print( - f" [cyan]runagent delete --id {oldest.get('agent_id', '')}[/cyan]" - ) - - return - - if agent_id: - # Show agent-specific details including invocations - result = sdk.get_agent_info(agent_id, local=True) - if result.get("success"): - agent_data = result["agent_info"] - console.print(f"\n🔍 [bold]Agent Details: {agent_id}[/bold]") - console.print(f"Framework: [green]{agent_data.get('framework')}[/green]") - console.print(f"Status: [yellow]{agent_data.get('status')}[/yellow]") - console.print(f"Path: [blue]{agent_data.get('deployment_path')}[/blue]") - - # Show agent-specific invocation stats - agent_inv_stats = sdk.db_service.get_invocation_stats(agent_id=agent_id) - console.print(f"\n📊 [bold]Invocation Statistics for {agent_id}[/bold]") - console.print(f"Total: [cyan]{agent_inv_stats.get('total_invocations', 0)}[/cyan]") - console.print(f"Success Rate: [blue]{agent_inv_stats.get('success_rate', 0)}%[/blue]") - - return - - # Show general database stats - stats = sdk.db_service.get_database_stats() - capacity_info = sdk.db_service.get_database_capacity_info() - - console.print("\n📊 [bold]Local Database Status[/bold]") - - current_count = capacity_info.get("current_count", 0) - is_full = capacity_info.get("is_full", False) - status = "FULL" if is_full else "OK" - console.print( - f"Agent Capacity: [cyan]{current_count}/5[/cyan] agents ([red]{status}[/red])" - if is_full - else f"Agent Capacity: [cyan]{current_count}/5[/cyan] agents ([green]{status}[/green])" - ) - - console.print(f"Total Agent Runs: [cyan]{stats.get('total_runs', 0)}[/cyan]") - console.print( - f"Database Size: [yellow]{stats.get('database_size_mb', 0)} MB[/yellow]" - ) - - # NEW: Show invocation statistics - overall_stats = sdk.db_service.get_invocation_stats() - - console.print(f"\n📊 [bold]Invocation Statistics[/bold]") - console.print(f"Total Invocations: [cyan]{overall_stats.get('total_invocations', 0)}[/cyan]") - console.print(f"Completed: [green]{overall_stats.get('completed_invocations', 0)}[/green]") - console.print(f"Failed: [red]{overall_stats.get('failed_invocations', 0)}[/red]") - console.print(f"Pending: [yellow]{overall_stats.get('pending_invocations', 0)}[/yellow]") - console.print(f"Success Rate: [blue]{overall_stats.get('success_rate', 0)}%[/blue]") - - if overall_stats.get('avg_execution_time_ms'): - avg_time = overall_stats['avg_execution_time_ms'] - if avg_time < 1000: - time_display = f"{avg_time:.1f}ms" - else: - time_display = f"{avg_time/1000:.2f}s" - console.print(f"Average Execution Time: [cyan]{time_display}[/cyan]") - - # Show agent status breakdown - status_counts = stats.get("agent_status_counts", {}) - if status_counts: - console.print("\n📈 [bold]Agent Status Breakdown:[/bold]") - for status, count in status_counts.items(): - console.print(f" [cyan]{status}[/cyan]: {count}") - - # List agents in table format - agents = sdk.db_service.list_agents() - - if agents: - console.print(f"\n📋 [bold]Deployed Agents:[/bold]") - - # Create table for better formatting - table = Table(title=f"Local Agents ({len(agents)} total)") - table.add_column("Status", width=8) - table.add_column("Files", width=6) - table.add_column("Agent ID", style="magenta", width=36) - table.add_column("Framework", style="green", width=12) - table.add_column("Host:Port", style="blue", width=15) - table.add_column("Runs", style="cyan", width=6) - table.add_column("Status", style="yellow", width=10) - - for agent in agents: - status_icon = ( - "🟢" - if agent["status"] == "deployed" - else "🔴" if agent["status"] == "error" else "🟡" - ) - exists_icon = "📁" if agent.get("exists") else "❌" - - table.add_row( - status_icon, - exists_icon, - agent['agent_id'], - agent['framework'], - f"{agent.get('host', 'N/A')}:{agent.get('port', 'N/A')}", - str(agent.get('run_count', 0)), - agent['status'] - ) - - console.print(table) - - # Show recent invocations - recent_invocations = sdk.db_service.list_invocations(limit=5) - if recent_invocations: - console.print(f"\n📋 [bold]Recent Invocations:[/bold]") - for inv in recent_invocations: - status_color = "green" if inv['status'] == "completed" else "red" if inv['status'] == "failed" else "yellow" - console.print(f" • {inv['invocation_id'][:12]}... [{status_color}]{inv['status']}[/{status_color}] ({inv.get('entrypoint_tag', 'N/A')})") - - console.print(f"\n💡 [bold]Database Commands:[/bold]") - console.print(f" • [cyan]runagent db invocations[/cyan] - Show all invocations") - console.print(f" • [cyan]runagent db invocation [/cyan] - Show specific invocation") - console.print(f" • [cyan]runagent db cleanup[/cyan] - Clean up old records") - console.print(f" • [cyan]runagent db status --agent-id [/cyan] - Agent-specific info") - console.print(f" • [cyan]runagent db status --capacity[/cyan] - Capacity management info") - - # Cleanup if requested (keep existing logic) - if cleanup_days: - console.print(f"\n🧹 Cleaning up records older than {cleanup_days} days...") - cleanup_result = sdk.cleanup_local_database(cleanup_days) - if cleanup_result.get("success"): - console.print(f"✅ [green]{cleanup_result.get('message')}[/green]") - else: - console.print(f"❌ [red]{cleanup_result.get('error')}[/red]") - - except Exception as e: - if os.getenv('DISABLE_TRY_CATCH'): - raise - console.print(f"❌ [red]Database status error:[/red] {e}") - raise click.ClickException("Failed to get database status") - - -@db.command() -@click.option("--agent-id", help="Filter by specific agent ID") -@click.option("--status", type=click.Choice(["pending", "completed", "failed"]), help="Filter by status") -@click.option("--limit", type=int, default=20, help="Maximum number of invocations to show") -@click.option("--format", "output_format", type=click.Choice(["table", "json"]), default="table", help="Output format") -def invocations(agent_id, status, limit, output_format): - """Show agent invocation history and statistics""" - try: - sdk = RunAgent() - - # Get invocations - invocations_list = sdk.db_service.list_invocations( - agent_id=agent_id, - status=status, - limit=limit - ) - - if output_format == "json": - console.print(json.dumps(invocations_list, indent=2)) - return - - if not invocations_list: - console.print("📭 [yellow]No invocations found[/yellow]") - if agent_id: - console.print(f" • Agent ID: {agent_id}") - if status: - console.print(f" • Status: {status}") - return - - # Show statistics first - if agent_id: - stats = sdk.db_service.get_invocation_stats(agent_id=agent_id) - else: - stats = sdk.db_service.get_invocation_stats() - - console.print(f"\n📊 [bold]Invocation Statistics[/bold]") - if agent_id: - console.print(f" Agent ID: [magenta]{agent_id}[/magenta]") - console.print(f" Total: [cyan]{stats.get('total_invocations', 0)}[/cyan]") - console.print(f" Completed: [green]{stats.get('completed_invocations', 0)}[/green]") - console.print(f" Failed: [red]{stats.get('failed_invocations', 0)}[/red]") - console.print(f" Pending: [yellow]{stats.get('pending_invocations', 0)}[/yellow]") - console.print(f" Success Rate: [blue]{stats.get('success_rate', 0)}%[/blue]") - if stats.get('avg_execution_time_ms'): - console.print(f" Avg Execution Time: [cyan]{stats.get('avg_execution_time_ms', 0):.1f}ms[/cyan]") - - # Show invocations table - console.print(f"\n📋 [bold]Recent Invocations (showing {len(invocations_list)} of {limit} max)[/bold]") - - table = Table(title="Agent Invocations") - table.add_column("Invocation", style="dim", width=12) - table.add_column("Agent", style="magenta", width=12) - table.add_column("Entrypoint", style="green", width=12) - table.add_column("Status", width=10) - table.add_column("Duration", style="cyan", width=10) - table.add_column("Started", style="dim", width=16) - table.add_column("SDK", style="yellow", width=10) - - for inv in invocations_list: - # Status with color - status_text = inv['status'] - if status_text == "completed": - status_display = f"[green]{status_text}[/green]" - elif status_text == "failed": - status_display = f"[red]{status_text}[/red]" - else: - status_display = f"[yellow]{status_text}[/yellow]" - - # Duration calculation - duration_display = "N/A" - if inv.get('execution_time_ms'): - if inv['execution_time_ms'] < 1000: - duration_display = f"{inv['execution_time_ms']:.0f}ms" - else: - duration_display = f"{inv['execution_time_ms']/1000:.1f}s" - - # Format timestamp - started_display = "N/A" - if inv.get('request_timestamp'): - try: - from datetime import datetime - dt = datetime.fromisoformat(inv['request_timestamp'].replace('Z', '+00:00')) - started_display = dt.strftime('%m-%d %H:%M:%S') - except: - started_display = inv['request_timestamp'][:16] - - table.add_row( - inv['invocation_id'][:8] + "...", - inv['agent_id'][:8] + "...", - inv.get('entrypoint_tag', 'N/A')[:12], - status_display, - duration_display, - started_display, - inv.get('sdk_type', 'unknown')[:10] - ) - - console.print(table) - - # Show usage tips - console.print(f"\n💡 [dim]Usage tips:[/dim]") - console.print(f" • View specific invocation: [cyan]runagent db invocation [/cyan]") - console.print(f" • Filter by agent: [cyan]runagent db invocations --agent-id [/cyan]") - console.print(f" • Filter by status: [cyan]runagent db invocations --status completed[/cyan]") - console.print(f" • JSON output: [cyan]runagent db invocations --format json[/cyan]") - - except Exception as e: - if os.getenv('DISABLE_TRY_CATCH'): - raise - console.print(f"❌ [red]Error getting invocations:[/red] {e}") - raise click.ClickException("Failed to get invocations") - - -@db.command() -@click.argument("invocation_id") -@click.option("--format", "output_format", type=click.Choice(["table", "json"]), default="table", help="Output format") -def invocation(invocation_id, output_format): - """Show detailed information about a specific invocation""" - try: - sdk = RunAgent() - - invocation = sdk.db_service.get_invocation(invocation_id) - - if not invocation: - console.print(f"❌ [red]Invocation {invocation_id} not found[/red]") - - # Show available invocations - console.print("\n💡 Recent invocations:") - recent = sdk.db_service.list_invocations(limit=5) - for inv in recent: - console.print(f" • {inv['invocation_id']} ({inv['status']})") - - raise click.ClickException("Invocation not found") - - if output_format == "json": - console.print(json.dumps(invocation, indent=2)) - return - - # Display detailed information - console.print(f"\n🔍 [bold]Invocation Details[/bold]") - console.print(f" Invocation ID: [bold magenta]{invocation['invocation_id']}[/bold magenta]") - console.print(f" Agent ID: [bold cyan]{invocation['agent_id']}[/bold cyan]") - console.print(f" Entrypoint: [green]{invocation.get('entrypoint_tag', 'N/A')}[/green]") - console.print(f" SDK Type: [yellow]{invocation.get('sdk_type', 'unknown')}[/yellow]") - - # Status with color - status = invocation['status'] - if status == "completed": - status_display = f"[green]{status}[/green]" - elif status == "failed": - status_display = f"[red]{status}[/red]" - else: - status_display = f"[yellow]{status}[/yellow]" - console.print(f" Status: {status_display}") - - # Timing information - console.print(f"\n⏱️ [bold]Timing Information[/bold]") - if invocation.get('request_timestamp'): - console.print(f" Started: [cyan]{invocation['request_timestamp']}[/cyan]") - if invocation.get('response_timestamp'): - console.print(f" Completed: [cyan]{invocation['response_timestamp']}[/cyan]") - if invocation.get('execution_time_ms'): - exec_time = invocation['execution_time_ms'] - if exec_time < 1000: - time_display = f"{exec_time:.1f}ms" - else: - time_display = f"{exec_time/1000:.2f}s" - console.print(f" Duration: [green]{time_display}[/green]") - - # Input data - console.print(f"\n📥 [bold]Input Data[/bold]") - if invocation.get('input_data'): - input_str = json.dumps(invocation['input_data'], indent=2) - if len(input_str) > 500: - console.print(f" [dim]{input_str[:500]}...\n (truncated - use --format json for full data)[/dim]") - else: - console.print(f" [dim]{input_str}[/dim]") - else: - console.print(" [dim]No input data[/dim]") - - # Output data or error - if invocation['status'] == 'failed' and invocation.get('error_detail'): - console.print(f"\n❌ [bold red]Error Details[/bold red]") - console.print(f" [red]{invocation['error_detail']}[/red]") - elif invocation.get('output_data'): - console.print(f"\n📤 [bold]Output Data[/bold]") - output_str = json.dumps(invocation['output_data'], indent=2) - if len(output_str) > 500: - console.print(f" [dim]{output_str[:500]}...\n (truncated - use --format json for full data)[/dim]") - else: - console.print(f" [dim]{output_str}[/dim]") - - # Client info - if invocation.get('client_info'): - console.print(f"\n🔧 [bold]Client Information[/bold]") - client_str = json.dumps(invocation['client_info'], indent=2) - console.print(f" [dim]{client_str}[/dim]") - - except Exception as e: - if os.getenv('DISABLE_TRY_CATCH'): - raise - console.print(f"❌ [red]Error getting invocation details:[/red] {e}") - raise click.ClickException("Failed to get invocation details") - - -@db.command() -@click.option("--days", type=int, default=30, help="Clean up invocations older than N days") -@click.option("--agent-runs", is_flag=True, help="Also clean up old agent_runs records") -@click.option("--yes", is_flag=True, help="Skip confirmation") -def cleanup(days, agent_runs, yes): - """Clean up old database records""" - try: - sdk = RunAgent() - - # Get count of records to be cleaned - all_invocations = sdk.db_service.list_invocations(limit=1000) - - # Filter by date (simple approximation for preview) - from datetime import datetime, timedelta - cutoff_date = datetime.now() - timedelta(days=days) - - old_invocations_count = len([ - inv for inv in all_invocations - if inv.get('request_timestamp') and - datetime.fromisoformat(inv['request_timestamp'].replace('Z', '+00:00')) < cutoff_date - ]) - - console.print(f"🧹 [yellow]Cleanup Preview (older than {days} days):[/yellow]") - console.print(f" • Invocations: {old_invocations_count} records") - - if agent_runs: - console.print(f" • Agent runs: Will be cleaned too") - - if old_invocations_count == 0: - console.print(f"✅ [green]No records found older than {days} days[/green]") - return - - if not yes: - if not click.confirm(f"⚠️ This will permanently delete {old_invocations_count} invocation records. Continue?"): - console.print("Cleanup cancelled.") - return - - # Perform cleanup - deleted_invocations = sdk.db_service.cleanup_old_invocations(days_old=days) - - console.print(f"✅ [green]Cleaned up {deleted_invocations} old invocation records[/green]") - - if agent_runs: - deleted_runs = sdk.cleanup_local_database(days) - if deleted_runs.get("success"): - console.print(f"✅ [green]Also cleaned up old agent runs[/green]") - - # Show updated stats - stats = sdk.db_service.get_invocation_stats() - console.print(f"📊 Remaining invocations: [cyan]{stats.get('total_invocations', 0)}[/cyan]") - - except Exception as e: - if os.getenv('DISABLE_TRY_CATCH'): - raise - console.print(f"❌ [red]Error cleaning up records:[/red] {e}") - raise click.ClickException("Cleanup failed") - - -@click.command() -@click.option("--status", is_flag=True, help="Show sync status") -@click.option("--test", is_flag=True, help="Test middleware connection") -def local_sync(status, test): - """Manage local agent sync with middleware - ENHANCED""" - try: - sdk = RunAgent() - - if not sdk.config.is_configured(): - console.print("❌ [red]RunAgent not configured[/red]") - console.print("💡 Run: [cyan]runagent setup --api-key [/cyan]") - raise click.ClickException("Setup required") - - from runagent.sdk.deployment.middleware_sync import MiddlewareSyncService - sync_service = MiddlewareSyncService(sdk.config) - - if status or (not test): - # Show detailed sync status - console.print("\n📡 [bold]Middleware Sync Status[/bold]") - console.print("=" * 40) - - # API Key status - if sync_service.api_key: - console.print("🔑 [green]API Key: CONFIGURED[/green]") - console.print(f" Key: [dim]{sync_service.api_key[:16]}...[/dim]") - else: - console.print("🔑 [red]API Key: NOT CONFIGURED[/red]") - - # Base URL - console.print(f"🌐 Base URL: [blue]{sync_service.config.base_url}[/blue]") - - # Authentication status - if sync_service.auth_validated: - console.print("🔐 [green]Authentication: VALID[/green]") - else: - console.print("🔐 [red]Authentication: INVALID[/red]") - - # Overall sync status - if sync_service.sync_enabled: - console.print("✅ [green]Sync Status: ENABLED[/green]") - console.print(" Local agent runs will sync to middleware") - else: - console.print("❌ [red]Sync Status: DISABLED[/red]") - console.print("⚠️ Local agents will only be stored locally") - - if not sync_service.sync_enabled: - console.print("\n [yellow]To enable sync:[/yellow]") - console.print(" 1. Get API key from middleware dashboard") - console.print(" 2. Run: [cyan]runagent setup --api-key [/cyan]") - - if test: - console.print("\n [bold]Testing Connection...[/bold]") - - if not sync_service.api_key: - console.print("❌ [red]No API key configured[/red]") - raise click.ClickException("API key required for testing") - - # Test basic connection - console.print("1. Testing basic connectivity...") - connection_result = sync_service._test_middleware_connection() - if connection_result: - console.print(" ✅ [green]Basic connection: SUCCESS[/green]") - else: - console.print(" ❌ [red]Basic connection: FAILED[/red]") - raise click.ClickException("Cannot connect to middleware") - - # Test authentication - console.print("2. Testing authentication...") - auth_result = sync_service._test_supabase_authentication() - if auth_result: - console.print(" ✅ [green]Authentication: SUCCESS[/green]") - else: - console.print(" ❌ [red]Authentication: FAILED[/red]") - console.print(" Check your API key") - raise click.ClickException("Authentication failed") - - # Test user info endpoint - console.print("3. Testing user info endpoint...") - try: - response = sync_service.rest_client.http.get("/users/auth-user-info", timeout=10) - if response.status_code == 200: - user_data = response.json() - user_info = user_data.get("user", {}) - console.print(" ✅ [green]User info: SUCCESS[/green]") - console.print(f" 👤 Email: [cyan]{user_info.get('email', 'Unknown')}[/cyan]") - if user_info.get('id'): - console.print(f" 🆔 User ID: [dim]{user_info.get('id')}[/dim]") - else: - console.print(f" ❌ [red]User info: FAILED (HTTP {response.status_code})[/red]") - except Exception as e: - console.print(f" ❌ [red]User info: ERROR ({e})[/red]") - - console.print("\n✅ [bold green]All tests passed! Middleware sync is working.[/bold green]") - - except Exception as e: - if os.getenv('DISABLE_TRY_CATCH'): - raise - console.print(f"❌ [red]Local sync error: {e}[/red]") - raise click.ClickException("Local sync command failed") - - -# Add this simplified logs command to the db group in runagent/cli/commands.py - -@db.command() -@click.option("--agent-id", help="Filter by specific agent ID") -@click.option("--limit", type=int, default=100, help="Maximum number of logs to show") -@click.option("--format", "output_format", type=click.Choice(["table", "json"]), default="table", help="Output format") -def logs(agent_id, limit, output_format): - """Show all agent logs (no filtering)""" - try: - sdk = RunAgent() - - if agent_id: - # Show logs for specific agent - logs = sdk.db_service.get_agent_logs(agent_id=agent_id, limit=limit) - - if not logs: - console.print("📭 [yellow]No logs found[/yellow]") - console.print(f" • Agent ID: {agent_id}") - return - - if output_format == "json": - console.print(json.dumps(logs, indent=2)) - return - - console.print(f"\n📋 [bold]Agent Logs: {agent_id}[/bold]") - - table = Table(title=f"All Agent Logs (showing {len(logs)} entries)") - table.add_column("Time", style="dim", width=16) - table.add_column("Level", width=8) - table.add_column("Message", style="white", width=80) - table.add_column("Execution", style="cyan", width=12) - - for log in logs: - # Format timestamp - time_str = "N/A" - if log.get('created_at'): - try: - from datetime import datetime - dt = datetime.fromisoformat(log['created_at']) - time_str = dt.strftime('%m-%d %H:%M:%S') - except: - time_str = log['created_at'][:16] - - # Color code log levels - level = log.get('log_level', 'INFO') - if level == 'ERROR' or level == 'CRITICAL': - level_display = f"[red]{level}[/red]" - elif level == 'WARNING': - level_display = f"[yellow]{level}[/yellow]" - elif level == 'DEBUG': - level_display = f"[dim]{level}[/dim]" - else: - level_display = f"[green]{level}[/green]" - - # Don't truncate messages - show full log - message = log.get('message', '') - - # Show execution ID if available - exec_id = log.get('execution_id', '') - exec_display = exec_id[:8] + "..." if exec_id else "" - - table.add_row(time_str, level_display, message, exec_display) - - console.print(table) - - else: - # Show log summary for all agents - agents = sdk.db_service.list_agents() - - if not agents: - console.print("📭 [yellow]No agents found[/yellow]") - return - - console.print(f"\n📊 [bold]Agent Log Summary[/bold]") - - table = Table(title="Log Counts by Agent") - table.add_column("Agent ID", style="magenta", width=36) - table.add_column("Framework", style="green", width=12) - table.add_column("Total Logs", style="cyan", width=10) - table.add_column("Errors", style="red", width=8) - table.add_column("Last Log", style="dim", width=16) - - for agent in agents[:10]: # Show first 10 agents - agent_logs = sdk.db_service.get_agent_logs(agent['agent_id'], limit=1000) - error_logs = [log for log in agent_logs if log.get('log_level') in ['ERROR', 'CRITICAL']] - - last_log_time = "Never" - if agent_logs: - try: - from datetime import datetime - dt = datetime.fromisoformat(agent_logs[0]['created_at']) - last_log_time = dt.strftime('%m-%d %H:%M') - except: - last_log_time = "Recent" - - table.add_row( - agent['agent_id'], - agent['framework'], - str(len(agent_logs)), - str(len(error_logs)), - last_log_time - ) - - console.print(table) - - console.print(f"\n💡 [bold]Usage tips:[/bold]") - console.print(f" • View agent logs: [cyan]runagent db logs --agent-id [/cyan]") - console.print(f" • JSON output: [cyan]runagent db logs --agent-id --format json[/cyan]") - console.print(f" • More logs: [cyan]runagent db logs --agent-id --limit 500[/cyan]") - - except Exception as e: - if os.getenv('DISABLE_TRY_CATCH'): - raise - console.print(f"❌ [red]Error getting logs:[/red] {e}") - raise click.ClickException("Failed to get logs") - - -@db.command() -@click.option("--days", type=int, default=7, help="Clean up logs older than N days") -@click.option("--yes", is_flag=True, help="Skip confirmation") -def cleanup_logs(days, yes): - """Clean up old agent logs""" - try: - sdk = RunAgent() - - if not yes: - if not click.confirm(f"⚠️ This will delete logs older than {days} days for ALL agents. Continue?"): - console.print("Cleanup cancelled.") - return - - deleted_count = sdk.db_service.cleanup_old_logs(days_old=days) - console.print(f"✅ [green]Cleaned up {deleted_count} old log entries[/green]") - - except Exception as e: - if os.getenv('DISABLE_TRY_CATCH'): - raise - console.print(f"❌ [red]Error cleaning up logs:[/red] {e}") - raise click.ClickException("Log cleanup failed") \ No newline at end of file diff --git a/runagent/cli/commands/config.py b/runagent/cli/commands/config.py new file mode 100644 index 0000000..70fcafc --- /dev/null +++ b/runagent/cli/commands/config.py @@ -0,0 +1,633 @@ +""" +CLI commands that use the restructured SDK internally. +""" +import os +import json +import uuid + +from pathlib import Path + +import click +from rich.console import Console +from rich.table import Table + +from runagent import RunAgent +from runagent.sdk.exceptions import ( # RunAgentError,; ConnectionError + AuthenticationError, + TemplateError, + ValidationError, +) +from runagent.client.client import RunAgentClient +from runagent.sdk.server.local_server import LocalServer +from runagent.utils.agent import detect_framework +from runagent.utils.animation import show_subtle_robotic_runner, show_quick_runner +from runagent.utils.config import Config +from runagent.sdk.deployment.middleware_sync import get_middleware_sync +from runagent.cli.utils import add_framework_options, get_selected_framework +from runagent.utils.enums.framework import Framework +console = Console() + + +def format_error_message(error_info): + """Format error information from API responses""" + if isinstance(error_info, dict) and "message" in error_info: + # New format with ErrorDetail object + error_message = error_info.get("message", "Unknown error") + error_code = error_info.get("code", "UNKNOWN_ERROR") + return f"[{error_code}] {error_message}" + else: + # Fallback to old format for backward compatibility + return str(error_info) if error_info else "Unknown error" + + +@click.group(invoke_without_command=True) +@click.option("--set-api-key", help="Set API key directly (e.g., runagent config --set-api-key YOUR_KEY)") +@click.option("--set-base-url", help="Set base URL directly (e.g., runagent config --set-base-url https://api.example.com)") +@click.pass_context +def config(ctx, set_api_key, set_base_url): + """ + Manage RunAgent configuration + + \b + Interactive mode (for humans): + $ runagent config + + \b + Direct flags (for scripts/agents): + $ runagent config --set-api-key YOUR_KEY + $ runagent config --set-base-url https://api.example.com + + \b + Subcommands: + $ runagent config status + $ runagent config reset + """ + + # Handle direct flag options + if set_api_key: + _set_api_key_direct(set_api_key) + return + + if set_base_url: + _set_base_url_direct(set_base_url) + return + + # If no subcommand and no flags, show interactive menu + if ctx.invoked_subcommand is None: + show_interactive_config_menu() + + +def _set_api_key_direct(api_key: str): + """Set API key directly (for --set-api-key flag) with validation""" + from rich.panel import Panel + from rich.status import Status + from runagent.constants import DEFAULT_BASE_URL + + if not api_key or not api_key.strip(): + console.print(Panel( + "[red]❌ API key cannot be empty[/red]", + title="[bold red]Error[/bold red]", + border_style="red" + )) + raise click.ClickException("Invalid API key") + + # Validate and fetch user info + try: + sdk = RunAgent() + base_url = Config.get_base_url() or DEFAULT_BASE_URL + + with Status("[bold cyan]Validating credentials...", spinner="dots"): + sdk.configure(api_key=api_key, base_url=base_url, save=True) + + # Get user info + user_config = Config.get_user_config() + + # Build success message + success_msg = ( + "[bold green]✅ API key updated successfully![/bold green]\n\n" + f"[dim]User:[/dim] [cyan]{user_config.get('user_email', 'N/A')}[/cyan]\n" + f"[dim]Tier:[/dim] [yellow]{user_config.get('user_tier', 'N/A')}[/yellow]" + ) + + # Add project if available + if user_config.get('active_project_name'): + success_msg += f"\n[dim]Project:[/dim] [green]{user_config.get('active_project_name')}[/green]" + + console.print(Panel( + success_msg, + title="[bold green]Success[/bold green]", + border_style="green" + )) + + except AuthenticationError as e: + console.print(Panel( + f"[red]❌ Authentication failed[/red]\n\n" + f"[dim]Error:[/dim] {str(e)}\n\n" + "[yellow]Please check your API key and try again[/yellow]", + title="[bold red]Validation Error[/bold red]", + border_style="red" + )) + raise click.ClickException("Authentication failed") + except Exception as e: + if os.getenv('DISABLE_TRY_CATCH'): + raise + console.print(Panel( + f"[red]❌ Failed to save API key[/red]\n\n" + f"[dim]Error:[/dim] {str(e)}", + title="[bold red]Error[/bold red]", + border_style="red" + )) + raise click.ClickException("Failed to save configuration") + + +def _set_base_url_direct(base_url: str): + """Set base URL directly (for --set-base-url flag)""" + from rich.panel import Panel + + # Validate URL format + if not base_url.startswith(('http://', 'https://')): + base_url = f"https://{base_url}" + + success = Config.set_base_url(base_url) + + if success: + console.print(Panel( + f"[bold green]✅ Base URL updated successfully![/bold green]\n\n" + f"[dim]New URL:[/dim] [cyan]{base_url}[/cyan]", + title="[bold green]Success[/bold green]", + border_style="green" + )) + else: + console.print(Panel( + "[red]❌ Failed to save base URL[/red]", + title="[bold red]Error[/bold red]", + border_style="red" + )) + raise click.ClickException("Failed to save configuration") + + +def show_interactive_config_menu(): + """Show interactive configuration menu""" + try: + from rich.panel import Panel + from runagent.cli.branding import print_header + import inquirer + + print_header("Configuration") + + questions = [ + inquirer.List( + 'config_option', + message="What would you like to configure?", + choices=[ + ('🔑 API Key', 'api_key'), + ('🌐 Base URL', 'base_url'), + ('📁 Active Project', 'project'), + ('🔄 Sync Settings', 'sync'), + ('📊 View Status', 'status'), + ('🔃 Reset Configuration', 'reset'), + ], + carousel=True + ), + ] + + answers = inquirer.prompt(questions) + if not answers: + console.print("[dim]Configuration cancelled.[/dim]") + return + + option = answers['config_option'] + + # Route to appropriate handler + if option == 'api_key': + _interactive_set_api_key() + elif option == 'base_url': + _interactive_set_base_url() + elif option == 'project': + _interactive_set_project() + elif option == 'sync': + _interactive_sync_settings() + elif option == 'status': + _show_config_status() + elif option == 'reset': + _interactive_reset_config() + + except Exception as e: + if os.getenv('DISABLE_TRY_CATCH'): + raise + console.print(f"[red]Error:[/red] {e}") + + +def _interactive_set_api_key(): + """Interactive API key setup with validation""" + from rich.prompt import Prompt + from rich.panel import Panel + from rich.status import Status + from runagent.constants import DEFAULT_BASE_URL + + api_key = Prompt.ask("[cyan]Enter your API key[/cyan]", password=True) + + if not api_key or not api_key.strip(): + console.print(Panel( + "[red]❌ API key cannot be empty[/red]", + title="[bold red]Error[/bold red]", + border_style="red" + )) + return + + # Validate and fetch user info + try: + sdk = RunAgent() + base_url = Config.get_base_url() or DEFAULT_BASE_URL + + with Status("[bold cyan]Validating credentials...", spinner="dots"): + sdk.configure(api_key=api_key, base_url=base_url, save=True) + + # Get user info + user_config = Config.get_user_config() + + # Build success message + success_msg = ( + "[bold green]✅ API key updated successfully![/bold green]\n\n" + f"[dim]User:[/dim] [cyan]{user_config.get('user_email', 'N/A')}[/cyan]\n" + f"[dim]Tier:[/dim] [yellow]{user_config.get('user_tier', 'N/A')}[/yellow]" + ) + + # Add project if available + if user_config.get('active_project_name'): + success_msg += f"\n[dim]Project:[/dim] [green]{user_config.get('active_project_name')}[/green]" + + console.print(Panel( + success_msg, + title="[bold green]Success[/bold green]", + border_style="green" + )) + + except AuthenticationError as e: + console.print(Panel( + f"[red]❌ Authentication failed[/red]\n\n" + f"[dim]Error:[/dim] {str(e)}\n\n" + "[yellow]Please check your API key and try again[/yellow]", + title="[bold red]Validation Error[/bold red]", + border_style="red" + )) + except Exception as e: + if os.getenv('DISABLE_TRY_CATCH'): + raise + console.print(Panel( + f"[red]❌ Failed to save API key[/red]\n\n" + f"[dim]Error:[/dim] {str(e)}", + title="[bold red]Error[/bold red]", + border_style="red" + )) + + +def _interactive_set_base_url(): + """Interactive base URL setup""" + from rich.prompt import Prompt + from rich.panel import Panel + from runagent.constants import DEFAULT_BASE_URL + + console.print(f"[dim]Current: {Config.get_base_url()}[/dim]") + console.print(f"[dim]Default: {DEFAULT_BASE_URL}[/dim]\n") + + base_url = Prompt.ask( + "[cyan]Enter base URL[/cyan]", + default=DEFAULT_BASE_URL + ) + + if not base_url.startswith(('http://', 'https://')): + base_url = f"https://{base_url}" + + success = Config.set_base_url(base_url) + + if success: + console.print(Panel( + f"[bold green]✅ Base URL updated successfully![/bold green]\n\n" + f"[dim]New URL:[/dim] [cyan]{base_url}[/cyan]", + title="[bold green]Success[/bold green]", + border_style="green" + )) + else: + console.print(Panel( + "[red]❌ Failed to save base URL[/red]", + title="[bold red]Error[/bold red]", + border_style="red" + )) + + +def _interactive_sync_settings(): + """Interactive sync settings configuration""" + try: + from rich.panel import Panel + import inquirer + + # Get current status + user_config = Config.get_user_config() + current_status = user_config.get('local_sync_enabled', True) + + # Show current status + if current_status: + status_text = "[green]Currently: ENABLED[/green]" + else: + status_text = "[red]Currently: DISABLED[/red]" + + console.print(f"\n📡 Middleware Sync {status_text}\n") + + # Ask what to do + questions = [ + inquirer.List( + 'sync_action', + message="Select sync preference", + choices=[ + ('✅ Enable Sync (sync local runs to middleware)', 'enable'), + ('❌ Disable Sync (local only)', 'disable'), + ], + default=('✅ Enable Sync (sync local runs to middleware)', 'enable') if current_status else ('❌ Disable Sync (local only)', 'disable'), + carousel=True + ), + ] + + answers = inquirer.prompt(questions) + if not answers: + console.print("[dim]Sync configuration cancelled.[/dim]") + return + + action = answers['sync_action'] + + # Set the preference + new_status = (action == 'enable') + Config.set_user_config('local_sync_enabled', new_status) + + if new_status: + console.print(Panel( + "[bold green]✅ Middleware sync enabled![/bold green]\n\n" + "[dim]Local agent runs will now sync to middleware.[/dim]\n" + "[dim]Requires valid API key.[/dim]", + title="[bold green]Success[/bold green]", + border_style="green" + )) + else: + console.print(Panel( + "[bold yellow]⚠️ Middleware sync disabled[/bold yellow]\n\n" + "[dim]Local agents will only store data locally.[/dim]\n" + "[dim]Your runs won't appear in the middleware dashboard.[/dim]", + title="[bold]Sync Disabled[/bold]", + border_style="yellow" + )) + + except Exception as e: + if os.getenv('DISABLE_TRY_CATCH'): + raise + console.print(f"[red]Error:[/red] {e}") + + +def _interactive_set_project(): + """Interactive project selection from API""" + try: + from rich.panel import Panel + from rich.status import Status + import inquirer + + # Get API key + api_key = Config.get_api_key() + if not api_key: + console.print(Panel( + "[red]❌ No API key configured[/red]\n\n" + "[dim]Run 'runagent setup' first[/dim]", + title="[bold red]Error[/bold red]", + border_style="red" + )) + return + + # Fetch projects from API + console.print("\n[cyan]📁 Fetching your projects...[/cyan]\n") + + from runagent.sdk.rest_client import RestClient + + with Status("[bold cyan]Loading projects...", spinner="dots"): + rest_client = RestClient( + api_key=api_key, + base_url=Config.get_base_url() + ) + + try: + response = rest_client.http.get("/projects?page=1&per_page=20&include_stats=false") + + if response.status_code != 200: + console.print(Panel( + f"[red]❌ Failed to fetch projects (Status: {response.status_code})[/red]", + title="[bold red]Error[/bold red]", + border_style="red" + )) + return + + projects_data = response.json() + + if not projects_data.get("success"): + console.print(Panel( + f"[red]❌ {projects_data.get('error', 'Failed to fetch projects')}[/red]", + title="[bold red]Error[/bold red]", + border_style="red" + )) + return + + projects = projects_data.get("data", {}).get("projects", []) + + if not projects: + console.print(Panel( + "[yellow]⚠️ No projects found[/yellow]\n\n" + "[dim]Create a project in the dashboard first[/dim]", + title="[bold]No Projects[/bold]", + border_style="yellow" + )) + return + + except Exception as e: + console.print(Panel( + f"[red]❌ Error fetching projects: {str(e)}[/red]", + title="[bold red]Error[/bold red]", + border_style="red" + )) + return + + # Show project selection + current_project_id = Config.get_user_config().get('active_project_id') + + project_choices = [] + default_choice = None + + for project in projects: + project_id = project.get('id') + project_name = project.get('name', 'Unnamed') + is_default = project.get('is_default', False) + + # Mark current and default projects + label = f"📁 {project_name}" + if project_id == current_project_id: + label = f"✓ {label} [current]" + default_choice = (label, project_id) + elif is_default: + label = f"{label} [default]" + + choice_tuple = (label, project_id) + project_choices.append(choice_tuple) + + if not default_choice and is_default: + default_choice = choice_tuple + + questions = [ + inquirer.List( + 'project', + message="Select active project", + choices=project_choices, + default=default_choice, + carousel=True + ), + ] + + answers = inquirer.prompt(questions) + if not answers: + console.print("[dim]Project selection cancelled.[/dim]") + return + + selected_project_id = answers['project'] + + # Find selected project details + selected_project = next( + (p for p in projects if p.get('id') == selected_project_id), + None + ) + + if not selected_project: + console.print("[red]Error: Project not found[/red]") + return + + # Save to database + Config.set_user_config('active_project_id', selected_project_id) + Config.set_user_config('active_project_name', selected_project.get('name')) + + console.print(Panel( + f"[bold green]✅ Active project updated![/bold green]\n\n" + f"[dim]Project:[/dim] [cyan]{selected_project.get('name')}[/cyan]", + title="[bold green]Success[/bold green]", + border_style="green" + )) + + except Exception as e: + if os.getenv('DISABLE_TRY_CATCH'): + raise + console.print(f"[red]Error:[/red] {e}") + + +def _show_config_status(): + """Show configuration status (helper for interactive menu and status command)""" + from rich.panel import Panel + from rich.table import Table + + user_config = Config.get_user_config() + api_key = Config.get_api_key() + base_url = Config.get_base_url() + + # Create status table + table = Table(show_header=False, box=None, padding=(0, 2)) + table.add_column("Setting", style="dim") + table.add_column("Value", style="cyan") + + # API Key status + if api_key: + masked_key = api_key[:8] + "..." + api_key[-4:] if len(api_key) > 12 else "***" + table.add_row("🔑 API Key", f"[green]✓[/green] {masked_key}") + else: + table.add_row("🔑 API Key", "[red]✗ Not set[/red]") + + # Base URL + table.add_row("🌐 Base URL", base_url or "[yellow]Using default[/yellow]") + + # User info + if user_config.get('user_email'): + table.add_row("✉️ Email", user_config.get('user_email')) + + if user_config.get('user_tier'): + table.add_row("🎯 Tier", user_config.get('user_tier')) + + # Active project + if user_config.get('active_project_name'): + table.add_row("📁 Active Project", user_config.get('active_project_name')) + + # Sync status + sync_enabled = user_config.get('local_sync_enabled', True) + if sync_enabled: + table.add_row("🔄 Middleware Sync", "[green]✓ Enabled[/green]") + else: + table.add_row("🔄 Middleware Sync", "[yellow]⚠ Disabled[/yellow]") + + console.print(Panel( + table, + title="[bold cyan]RunAgent Configuration[/bold cyan]", + border_style="cyan" + )) + + # Show helpful info + console.print("\n[dim]💡 Use arrow keys in interactive mode: 'runagent config'[/dim]") + console.print("[dim]💡 Direct flags for automation: 'runagent config --set-api-key '[/dim]\n") + + +def _interactive_reset_config(): + """Interactive reset configuration (helper for interactive menu)""" + from rich.prompt import Confirm + from rich.panel import Panel + + console.print("[yellow]⚠️ This will remove all your configuration including API key[/yellow]") + if not Confirm.ask("\n[bold]Are you sure you want to reset?[/bold]", default=False): + console.print("[dim]Reset cancelled.[/dim]") + return + + sdk = RunAgent() + sdk.config.clear() + + console.print(Panel( + "[bold green]✅ Configuration reset successfully![/bold green]\n\n" + "[dim]Run 'runagent setup' to configure again.[/dim]", + title="[bold green]Success[/bold green]", + border_style="green" + )) + + + + +@config.command("status") +def config_status_cmd(): + """Show current configuration status""" + _show_config_status() + + +@config.command("reset") +@click.option("--yes", is_flag=True, help="Skip confirmation") +def config_reset_cmd(yes): + """Reset configuration to defaults""" + if yes: + _reset_config_without_prompt() + else: + _interactive_reset_config() + + +def _reset_config_without_prompt(): + """Reset config without confirmation (for --yes flag)""" + from rich.panel import Panel + + try: + sdk = RunAgent() + sdk.config.clear() + + console.print(Panel( + "[bold green]✅ Configuration reset successfully![/bold green]\n\n" + "[dim]Run 'runagent setup' to configure again.[/dim]", + title="[bold green]Success[/bold green]", + border_style="green" + )) + except Exception as e: + if os.getenv('DISABLE_TRY_CATCH'): + raise + console.print(f"[red]Error:[/red] {e}") + raise click.ClickException("Reset failed") diff --git a/runagent/cli/commands/db.py b/runagent/cli/commands/db.py new file mode 100644 index 0000000..6e391ef --- /dev/null +++ b/runagent/cli/commands/db.py @@ -0,0 +1,659 @@ +""" +CLI commands that use the restructured SDK internally. +""" +import os +import json +import uuid + +from pathlib import Path + +import click +from rich.console import Console +from rich.table import Table + +from runagent import RunAgent +from runagent.sdk.exceptions import ( # RunAgentError,; ConnectionError + AuthenticationError, + TemplateError, + ValidationError, +) +from runagent.client.client import RunAgentClient +from runagent.sdk.server.local_server import LocalServer +from runagent.utils.agent import detect_framework +from runagent.utils.animation import show_subtle_robotic_runner, show_quick_runner +from runagent.utils.config import Config +from runagent.sdk.deployment.middleware_sync import get_middleware_sync +from runagent.cli.utils import add_framework_options, get_selected_framework +from runagent.utils.enums.framework import Framework +console = Console() + + +def format_error_message(error_info): + """Format error information from API responses""" + if isinstance(error_info, dict) and "message" in error_info: + # New format with ErrorDetail object + error_message = error_info.get("message", "Unknown error") + error_code = error_info.get("code", "UNKNOWN_ERROR") + return f"[{error_code}] {error_message}" + else: + # Fallback to old format for backward compatibility + return str(error_info) if error_info else "Unknown error" + + +# ============================================================================ +# Config Command Group +# ============================================================================ + +@click.group() +def db(): + """Database management and monitoring commands""" + pass + +@db.command() +@click.option("--cleanup-days", type=int, help="Clean up records older than N days") +@click.option("--agent-id", help="Show detailed info for specific agent") +@click.option("--capacity", is_flag=True, help="Show detailed capacity information") +def status(cleanup_days, agent_id, capacity): + """Show local database status and statistics (ENHANCED with invocation stats)""" + try: + sdk = RunAgent() + + if capacity: + # Show detailed capacity info + capacity_info = sdk.db_service.get_database_capacity_info() + + console.print(f"\n📊 [bold]Database Capacity Information[/bold]") + console.print( + f"Current: [cyan]{capacity_info.get('current_count', 0)}/5[/cyan] agents" + ) + console.print( + f"Remaining slots: [green]{capacity_info.get('remaining_slots', 0)}[/green]" + ) + + status = "🔴 FULL" if capacity_info.get("is_full") else "🟢 Available" + console.print(f"Status: {status}") + + agents = capacity_info.get("agents", []) + if agents: + console.print(f"\n📋 [bold]Deployed Agents (by age):[/bold]") + + # Create table for agents + table = Table(title="Agents by Deployment Age") + table.add_column("#", style="dim", width=3) + table.add_column("Status", width=6) + table.add_column("Agent ID", style="magenta", width=36) + table.add_column("Framework", style="green", width=12) + table.add_column("Deployed At", style="cyan", width=20) + table.add_column("Age Note", style="yellow", width=10) + + for i, agent in enumerate(agents): + status_icon = ( + "🟢" + if agent["status"] == "deployed" + else "🔴" if agent["status"] == "error" else "🟡" + ) + age_label = ( + "oldest" + if i == 0 + else "newest" if i == len(agents) - 1 else "" + ) + + table.add_row( + str(i+1), + status_icon, + agent['agent_id'], + agent['framework'], + agent['deployed_at'] or "Unknown", + age_label + ) + + console.print(table) + + if capacity_info.get("is_full"): + oldest = capacity_info.get("oldest_agent", {}) + console.print( + f"\n💡 [yellow]To deploy new agent, replace oldest:[/yellow]" + ) + console.print( + f" [cyan]runagent serve --folder --replace {oldest.get('agent_id', '')}[/cyan]" + ) + console.print( + f" [cyan]runagent delete --id {oldest.get('agent_id', '')}[/cyan]" + ) + + return + + if agent_id: + # Show agent-specific details including invocations + result = sdk.get_agent_info(agent_id, local=True) + if result.get("success"): + agent_data = result["agent_info"] + console.print(f"\n🔍 [bold]Agent Details: {agent_id}[/bold]") + console.print(f"Framework: [green]{agent_data.get('framework')}[/green]") + console.print(f"Status: [yellow]{agent_data.get('status')}[/yellow]") + console.print(f"Path: [blue]{agent_data.get('deployment_path')}[/blue]") + + # Show agent-specific invocation stats + agent_inv_stats = sdk.db_service.get_invocation_stats(agent_id=agent_id) + console.print(f"\n📊 [bold]Invocation Statistics for {agent_id}[/bold]") + console.print(f"Total: [cyan]{agent_inv_stats.get('total_invocations', 0)}[/cyan]") + console.print(f"Success Rate: [blue]{agent_inv_stats.get('success_rate', 0)}%[/blue]") + + return + + # Show general database stats + stats = sdk.db_service.get_database_stats() + capacity_info = sdk.db_service.get_database_capacity_info() + + console.print("\n📊 [bold]Local Database Status[/bold]") + + current_count = capacity_info.get("current_count", 0) + is_full = capacity_info.get("is_full", False) + status = "FULL" if is_full else "OK" + console.print( + f"Agent Capacity: [cyan]{current_count}/5[/cyan] agents ([red]{status}[/red])" + if is_full + else f"Agent Capacity: [cyan]{current_count}/5[/cyan] agents ([green]{status}[/green])" + ) + + console.print(f"Total Agent Runs: [cyan]{stats.get('total_runs', 0)}[/cyan]") + console.print( + f"Database Size: [yellow]{stats.get('database_size_mb', 0)} MB[/yellow]" + ) + + # NEW: Show invocation statistics + overall_stats = sdk.db_service.get_invocation_stats() + + console.print(f"\n📊 [bold]Invocation Statistics[/bold]") + console.print(f"Total Invocations: [cyan]{overall_stats.get('total_invocations', 0)}[/cyan]") + console.print(f"Completed: [green]{overall_stats.get('completed_invocations', 0)}[/green]") + console.print(f"Failed: [red]{overall_stats.get('failed_invocations', 0)}[/red]") + console.print(f"Pending: [yellow]{overall_stats.get('pending_invocations', 0)}[/yellow]") + console.print(f"Success Rate: [blue]{overall_stats.get('success_rate', 0)}%[/blue]") + + if overall_stats.get('avg_execution_time_ms'): + avg_time = overall_stats['avg_execution_time_ms'] + if avg_time < 1000: + time_display = f"{avg_time:.1f}ms" + else: + time_display = f"{avg_time/1000:.2f}s" + console.print(f"Average Execution Time: [cyan]{time_display}[/cyan]") + + # Show agent status breakdown + status_counts = stats.get("agent_status_counts", {}) + if status_counts: + console.print("\n📈 [bold]Agent Status Breakdown:[/bold]") + for status, count in status_counts.items(): + console.print(f" [cyan]{status}[/cyan]: {count}") + + # List agents in table format + agents = sdk.db_service.list_agents() + + if agents: + console.print(f"\n📋 [bold]Deployed Agents:[/bold]") + + # Create table for better formatting + table = Table(title=f"Local Agents ({len(agents)} total)") + table.add_column("Status", width=8) + table.add_column("Files", width=6) + table.add_column("Agent ID", style="magenta", width=36) + table.add_column("Framework", style="green", width=12) + table.add_column("Host:Port", style="blue", width=15) + table.add_column("Runs", style="cyan", width=6) + table.add_column("Status", style="yellow", width=10) + + for agent in agents: + status_icon = ( + "🟢" + if agent["status"] == "deployed" + else "🔴" if agent["status"] == "error" else "🟡" + ) + exists_icon = "📁" if agent.get("exists") else "❌" + + table.add_row( + status_icon, + exists_icon, + agent['agent_id'], + agent['framework'], + f"{agent.get('host', 'N/A')}:{agent.get('port', 'N/A')}", + str(agent.get('run_count', 0)), + agent['status'] + ) + + console.print(table) + + # Show recent invocations + recent_invocations = sdk.db_service.list_invocations(limit=5) + if recent_invocations: + console.print(f"\n📋 [bold]Recent Invocations:[/bold]") + for inv in recent_invocations: + status_color = "green" if inv['status'] == "completed" else "red" if inv['status'] == "failed" else "yellow" + console.print(f" • {inv['invocation_id'][:12]}... [{status_color}]{inv['status']}[/{status_color}] ({inv.get('entrypoint_tag', 'N/A')})") + + console.print(f"\n💡 [bold]Database Commands:[/bold]") + console.print(f" • [cyan]runagent db invocations[/cyan] - Show all invocations") + console.print(f" • [cyan]runagent db invocation [/cyan] - Show specific invocation") + console.print(f" • [cyan]runagent db cleanup[/cyan] - Clean up old records") + console.print(f" • [cyan]runagent db status --agent-id [/cyan] - Agent-specific info") + console.print(f" • [cyan]runagent db status --capacity[/cyan] - Capacity management info") + + # Cleanup if requested (keep existing logic) + if cleanup_days: + console.print(f"\n🧹 Cleaning up records older than {cleanup_days} days...") + cleanup_result = sdk.cleanup_local_database(cleanup_days) + if cleanup_result.get("success"): + console.print(f"✅ [green]{cleanup_result.get('message')}[/green]") + else: + console.print(f"❌ [red]{cleanup_result.get('error')}[/red]") + + except Exception as e: + if os.getenv('DISABLE_TRY_CATCH'): + raise + console.print(f"❌ [red]Database status error:[/red] {e}") + raise click.ClickException("Failed to get database status") + + +@db.command() +@click.option("--agent-id", help="Filter by specific agent ID") +@click.option("--status", type=click.Choice(["pending", "completed", "failed"]), help="Filter by status") +@click.option("--limit", type=int, default=20, help="Maximum number of invocations to show") +@click.option("--format", "output_format", type=click.Choice(["table", "json"]), default="table", help="Output format") +def invocations(agent_id, status, limit, output_format): + """Show agent invocation history and statistics""" + try: + sdk = RunAgent() + + # Get invocations + invocations_list = sdk.db_service.list_invocations( + agent_id=agent_id, + status=status, + limit=limit + ) + + if output_format == "json": + console.print(json.dumps(invocations_list, indent=2)) + return + + if not invocations_list: + console.print("📭 [yellow]No invocations found[/yellow]") + if agent_id: + console.print(f" • Agent ID: {agent_id}") + if status: + console.print(f" • Status: {status}") + return + + # Show statistics first + if agent_id: + stats = sdk.db_service.get_invocation_stats(agent_id=agent_id) + else: + stats = sdk.db_service.get_invocation_stats() + + console.print(f"\n📊 [bold]Invocation Statistics[/bold]") + if agent_id: + console.print(f" Agent ID: [magenta]{agent_id}[/magenta]") + console.print(f" Total: [cyan]{stats.get('total_invocations', 0)}[/cyan]") + console.print(f" Completed: [green]{stats.get('completed_invocations', 0)}[/green]") + console.print(f" Failed: [red]{stats.get('failed_invocations', 0)}[/red]") + console.print(f" Pending: [yellow]{stats.get('pending_invocations', 0)}[/yellow]") + console.print(f" Success Rate: [blue]{stats.get('success_rate', 0)}%[/blue]") + if stats.get('avg_execution_time_ms'): + console.print(f" Avg Execution Time: [cyan]{stats.get('avg_execution_time_ms', 0):.1f}ms[/cyan]") + + # Show invocations table + console.print(f"\n📋 [bold]Recent Invocations (showing {len(invocations_list)} of {limit} max)[/bold]") + + table = Table(title="Agent Invocations") + table.add_column("Invocation", style="dim", width=12) + table.add_column("Agent", style="magenta", width=12) + table.add_column("Entrypoint", style="green", width=12) + table.add_column("Status", width=10) + table.add_column("Duration", style="cyan", width=10) + table.add_column("Started", style="dim", width=16) + table.add_column("SDK", style="yellow", width=10) + + for inv in invocations_list: + # Status with color + status_text = inv['status'] + if status_text == "completed": + status_display = f"[green]{status_text}[/green]" + elif status_text == "failed": + status_display = f"[red]{status_text}[/red]" + else: + status_display = f"[yellow]{status_text}[/yellow]" + + # Duration calculation + duration_display = "N/A" + if inv.get('execution_time_ms'): + if inv['execution_time_ms'] < 1000: + duration_display = f"{inv['execution_time_ms']:.0f}ms" + else: + duration_display = f"{inv['execution_time_ms']/1000:.1f}s" + + # Format timestamp + started_display = "N/A" + if inv.get('request_timestamp'): + try: + from datetime import datetime + dt = datetime.fromisoformat(inv['request_timestamp'].replace('Z', '+00:00')) + started_display = dt.strftime('%m-%d %H:%M:%S') + except: + started_display = inv['request_timestamp'][:16] + + table.add_row( + inv['invocation_id'][:8] + "...", + inv['agent_id'][:8] + "...", + inv.get('entrypoint_tag', 'N/A')[:12], + status_display, + duration_display, + started_display, + inv.get('sdk_type', 'unknown')[:10] + ) + + console.print(table) + + # Show usage tips + console.print(f"\n💡 [dim]Usage tips:[/dim]") + console.print(f" • View specific invocation: [cyan]runagent db invocation [/cyan]") + console.print(f" • Filter by agent: [cyan]runagent db invocations --agent-id [/cyan]") + console.print(f" • Filter by status: [cyan]runagent db invocations --status completed[/cyan]") + console.print(f" • JSON output: [cyan]runagent db invocations --format json[/cyan]") + + except Exception as e: + if os.getenv('DISABLE_TRY_CATCH'): + raise + console.print(f"❌ [red]Error getting invocations:[/red] {e}") + raise click.ClickException("Failed to get invocations") + + +@db.command() +@click.argument("invocation_id") +@click.option("--format", "output_format", type=click.Choice(["table", "json"]), default="table", help="Output format") +def invocation(invocation_id, output_format): + """Show detailed information about a specific invocation""" + try: + sdk = RunAgent() + + invocation = sdk.db_service.get_invocation(invocation_id) + + if not invocation: + console.print(f"❌ [red]Invocation {invocation_id} not found[/red]") + + # Show available invocations + console.print("\n💡 Recent invocations:") + recent = sdk.db_service.list_invocations(limit=5) + for inv in recent: + console.print(f" • {inv['invocation_id']} ({inv['status']})") + + raise click.ClickException("Invocation not found") + + if output_format == "json": + console.print(json.dumps(invocation, indent=2)) + return + + # Display detailed information + console.print(f"\n🔍 [bold]Invocation Details[/bold]") + console.print(f" Invocation ID: [bold magenta]{invocation['invocation_id']}[/bold magenta]") + console.print(f" Agent ID: [bold cyan]{invocation['agent_id']}[/bold cyan]") + console.print(f" Entrypoint: [green]{invocation.get('entrypoint_tag', 'N/A')}[/green]") + console.print(f" SDK Type: [yellow]{invocation.get('sdk_type', 'unknown')}[/yellow]") + + # Status with color + status = invocation['status'] + if status == "completed": + status_display = f"[green]{status}[/green]" + elif status == "failed": + status_display = f"[red]{status}[/red]" + else: + status_display = f"[yellow]{status}[/yellow]" + console.print(f" Status: {status_display}") + + # Timing information + console.print(f"\n⏱️ [bold]Timing Information[/bold]") + if invocation.get('request_timestamp'): + console.print(f" Started: [cyan]{invocation['request_timestamp']}[/cyan]") + if invocation.get('response_timestamp'): + console.print(f" Completed: [cyan]{invocation['response_timestamp']}[/cyan]") + if invocation.get('execution_time_ms'): + exec_time = invocation['execution_time_ms'] + if exec_time < 1000: + time_display = f"{exec_time:.1f}ms" + else: + time_display = f"{exec_time/1000:.2f}s" + console.print(f" Duration: [green]{time_display}[/green]") + + # Input data + console.print(f"\n📥 [bold]Input Data[/bold]") + if invocation.get('input_data'): + input_str = json.dumps(invocation['input_data'], indent=2) + if len(input_str) > 500: + console.print(f" [dim]{input_str[:500]}...\n (truncated - use --format json for full data)[/dim]") + else: + console.print(f" [dim]{input_str}[/dim]") + else: + console.print(" [dim]No input data[/dim]") + + # Output data or error + if invocation['status'] == 'failed' and invocation.get('error_detail'): + console.print(f"\n❌ [bold red]Error Details[/bold red]") + console.print(f" [red]{invocation['error_detail']}[/red]") + elif invocation.get('output_data'): + console.print(f"\n📤 [bold]Output Data[/bold]") + output_str = json.dumps(invocation['output_data'], indent=2) + if len(output_str) > 500: + console.print(f" [dim]{output_str[:500]}...\n (truncated - use --format json for full data)[/dim]") + else: + console.print(f" [dim]{output_str}[/dim]") + + # Client info + if invocation.get('client_info'): + console.print(f"\n🔧 [bold]Client Information[/bold]") + client_str = json.dumps(invocation['client_info'], indent=2) + console.print(f" [dim]{client_str}[/dim]") + + except Exception as e: + if os.getenv('DISABLE_TRY_CATCH'): + raise + console.print(f"❌ [red]Error getting invocation details:[/red] {e}") + raise click.ClickException("Failed to get invocation details") + + +@db.command() +@click.option("--days", type=int, default=30, help="Clean up invocations older than N days") +@click.option("--agent-runs", is_flag=True, help="Also clean up old agent_runs records") +@click.option("--yes", is_flag=True, help="Skip confirmation") +def cleanup(days, agent_runs, yes): + """Clean up old database records""" + try: + sdk = RunAgent() + + # Get count of records to be cleaned + all_invocations = sdk.db_service.list_invocations(limit=1000) + + # Filter by date (simple approximation for preview) + from datetime import datetime, timedelta + cutoff_date = datetime.now() - timedelta(days=days) + + old_invocations_count = len([ + inv for inv in all_invocations + if inv.get('request_timestamp') and + datetime.fromisoformat(inv['request_timestamp'].replace('Z', '+00:00')) < cutoff_date + ]) + + console.print(f"🧹 [yellow]Cleanup Preview (older than {days} days):[/yellow]") + console.print(f" • Invocations: {old_invocations_count} records") + + if agent_runs: + console.print(f" • Agent runs: Will be cleaned too") + + if old_invocations_count == 0: + console.print(f"✅ [green]No records found older than {days} days[/green]") + return + + if not yes: + if not click.confirm(f"⚠️ This will permanently delete {old_invocations_count} invocation records. Continue?"): + console.print("Cleanup cancelled.") + return + + # Perform cleanup + deleted_invocations = sdk.db_service.cleanup_old_invocations(days_old=days) + + console.print(f"✅ [green]Cleaned up {deleted_invocations} old invocation records[/green]") + + if agent_runs: + deleted_runs = sdk.cleanup_local_database(days) + if deleted_runs.get("success"): + console.print(f"✅ [green]Also cleaned up old agent runs[/green]") + + # Show updated stats + stats = sdk.db_service.get_invocation_stats() + console.print(f"📊 Remaining invocations: [cyan]{stats.get('total_invocations', 0)}[/cyan]") + + except Exception as e: + if os.getenv('DISABLE_TRY_CATCH'): + raise + console.print(f"❌ [red]Error cleaning up records:[/red] {e}") + raise click.ClickException("Cleanup failed") + + +# local-sync command removed - sync settings now managed via 'runagent config' +# Use: runagent config > Select "🔄 Sync Settings" + + +# Add this simplified logs command to the db group in runagent/cli/commands.py + +@db.command() +@click.option("--agent-id", help="Filter by specific agent ID") +@click.option("--limit", type=int, default=100, help="Maximum number of logs to show") +@click.option("--format", "output_format", type=click.Choice(["table", "json"]), default="table", help="Output format") +def logs(agent_id, limit, output_format): + """Show all agent logs (no filtering)""" + try: + sdk = RunAgent() + + if agent_id: + # Show logs for specific agent + logs = sdk.db_service.get_agent_logs(agent_id=agent_id, limit=limit) + + if not logs: + console.print("📭 [yellow]No logs found[/yellow]") + console.print(f" • Agent ID: {agent_id}") + return + + if output_format == "json": + console.print(json.dumps(logs, indent=2)) + return + + console.print(f"\n📋 [bold]Agent Logs: {agent_id}[/bold]") + + table = Table(title=f"All Agent Logs (showing {len(logs)} entries)") + table.add_column("Time", style="dim", width=16) + table.add_column("Level", width=8) + table.add_column("Message", style="white", width=80) + table.add_column("Execution", style="cyan", width=12) + + for log in logs: + # Format timestamp + time_str = "N/A" + if log.get('created_at'): + try: + from datetime import datetime + dt = datetime.fromisoformat(log['created_at']) + time_str = dt.strftime('%m-%d %H:%M:%S') + except: + time_str = log['created_at'][:16] + + # Color code log levels + level = log.get('log_level', 'INFO') + if level == 'ERROR' or level == 'CRITICAL': + level_display = f"[red]{level}[/red]" + elif level == 'WARNING': + level_display = f"[yellow]{level}[/yellow]" + elif level == 'DEBUG': + level_display = f"[dim]{level}[/dim]" + else: + level_display = f"[green]{level}[/green]" + + # Don't truncate messages - show full log + message = log.get('message', '') + + # Show execution ID if available + exec_id = log.get('execution_id', '') + exec_display = exec_id[:8] + "..." if exec_id else "" + + table.add_row(time_str, level_display, message, exec_display) + + console.print(table) + + else: + # Show log summary for all agents + agents = sdk.db_service.list_agents() + + if not agents: + console.print("📭 [yellow]No agents found[/yellow]") + return + + console.print(f"\n📊 [bold]Agent Log Summary[/bold]") + + table = Table(title="Log Counts by Agent") + table.add_column("Agent ID", style="magenta", width=36) + table.add_column("Framework", style="green", width=12) + table.add_column("Total Logs", style="cyan", width=10) + table.add_column("Errors", style="red", width=8) + table.add_column("Last Log", style="dim", width=16) + + for agent in agents[:10]: # Show first 10 agents + agent_logs = sdk.db_service.get_agent_logs(agent['agent_id'], limit=1000) + error_logs = [log for log in agent_logs if log.get('log_level') in ['ERROR', 'CRITICAL']] + + last_log_time = "Never" + if agent_logs: + try: + from datetime import datetime + dt = datetime.fromisoformat(agent_logs[0]['created_at']) + last_log_time = dt.strftime('%m-%d %H:%M') + except: + last_log_time = "Recent" + + table.add_row( + agent['agent_id'], + agent['framework'], + str(len(agent_logs)), + str(len(error_logs)), + last_log_time + ) + + console.print(table) + + console.print(f"\n💡 [bold]Usage tips:[/bold]") + console.print(f" • View agent logs: [cyan]runagent db logs --agent-id [/cyan]") + console.print(f" • JSON output: [cyan]runagent db logs --agent-id --format json[/cyan]") + console.print(f" • More logs: [cyan]runagent db logs --agent-id --limit 500[/cyan]") + + except Exception as e: + if os.getenv('DISABLE_TRY_CATCH'): + raise + console.print(f"❌ [red]Error getting logs:[/red] {e}") + raise click.ClickException("Failed to get logs") + + +@db.command() +@click.option("--days", type=int, default=7, help="Clean up logs older than N days") +@click.option("--yes", is_flag=True, help="Skip confirmation") +def cleanup_logs(days, yes): + """Clean up old agent logs""" + try: + sdk = RunAgent() + + if not yes: + if not click.confirm(f"⚠️ This will delete logs older than {days} days for ALL agents. Continue?"): + console.print("Cleanup cancelled.") + return + + deleted_count = sdk.db_service.cleanup_old_logs(days_old=days) + console.print(f"✅ [green]Cleaned up {deleted_count} old log entries[/green]") + + except Exception as e: + if os.getenv('DISABLE_TRY_CATCH'): + raise + console.print(f"❌ [red]Error cleaning up logs:[/red] {e}") + raise click.ClickException("Log cleanup failed") \ No newline at end of file diff --git a/runagent/cli/commands/delete.py b/runagent/cli/commands/delete.py new file mode 100644 index 0000000..ae60e0d --- /dev/null +++ b/runagent/cli/commands/delete.py @@ -0,0 +1,121 @@ +""" +CLI commands that use the restructured SDK internally. +""" +import os +import json +import uuid + +from pathlib import Path + +import click +from rich.console import Console +from rich.table import Table + +from runagent import RunAgent +from runagent.sdk.exceptions import ( # RunAgentError,; ConnectionError + AuthenticationError, + TemplateError, + ValidationError, +) +from runagent.client.client import RunAgentClient +from runagent.sdk.server.local_server import LocalServer +from runagent.utils.agent import detect_framework +from runagent.utils.animation import show_subtle_robotic_runner, show_quick_runner +from runagent.utils.config import Config +from runagent.sdk.deployment.middleware_sync import get_middleware_sync +from runagent.cli.utils import add_framework_options, get_selected_framework +from runagent.utils.enums.framework import Framework +console = Console() + + +def format_error_message(error_info): + """Format error information from API responses""" + if isinstance(error_info, dict) and "message" in error_info: + # New format with ErrorDetail object + error_message = error_info.get("message", "Unknown error") + error_code = error_info.get("code", "UNKNOWN_ERROR") + return f"[{error_code}] {error_message}" + else: + # Fallback to old format for backward compatibility + return str(error_info) if error_info else "Unknown error" + + +# ============================================================================ +# Config Command Group +# ============================================================================ + + +@click.command() +@click.option("--id", "agent_id", required=True, help="Agent ID to delete") +@click.option("--yes", is_flag=True, help="Skip confirmation") +def delete(agent_id, yes): + """Delete an agent from the local database""" + try: + from runagent.cli.branding import print_header + print_header("Delete Agent") + + sdk = RunAgent() + + # Get agent info first + agent = sdk.db_service.get_agent(agent_id) + if not agent: + console.print(f"❌ [red]Agent {agent_id} not found in database[/red]") + + # Show available agents + console.print("\n💡 Available agents:") + agents = sdk.db_service.list_agents() + if agents: + table = Table(title="Available Agents") + table.add_column("Agent ID", style="magenta") + table.add_column("Framework", style="green") + table.add_column("Status", style="yellow") + table.add_column("Deployed At", style="dim") + + for agent in agents[:10]: # Show first 10 + table.add_row( + agent['agent_id'][:8] + "...", + agent['framework'], + agent['status'], + agent['deployed_at'] or "Unknown" + ) + console.print(table) + else: + console.print(" No agents found in database") + + raise click.ClickException("Agent not found") + + # Show agent details + console.print(f"\n🔍 [yellow]Agent to be deleted:[/yellow]") + console.print(f" Agent ID: [bold magenta]{agent['agent_id']}[/bold magenta]") + console.print(f" Framework: [green]{agent['framework']}[/green]") + console.print(f" Path: [blue]{agent['agent_path']}[/blue]") + console.print(f" Status: [yellow]{agent['status']}[/yellow]") + console.print(f" Deployed: [dim]{agent['deployed_at']}[/dim]") + console.print(f" Total Runs: [cyan]{agent['run_count']}[/cyan]") + + # Confirmation + if not yes: + if not click.confirm("\n⚠️ This will permanently delete the agent from the database. Continue?"): + console.print("Deletion cancelled.") + return + + # Delete the agent + result = sdk.db_service.force_delete_agent(agent_id) + + if result["success"]: + console.print(f"\n✅ [green]Agent {agent_id} deleted successfully![/green]") + + # Show updated capacity + capacity_info = sdk.db_service.get_database_capacity_info() + console.print(f"📊 Updated capacity: [cyan]{capacity_info.get('current_count', 0)}/5[/cyan] agents") + else: + console.print(f"❌ [red]Failed to delete agent:[/red] {format_error_message(result.get('error'))}") + import sys + sys.exit(1) + + except Exception as e: + if os.getenv('DISABLE_TRY_CATCH'): + raise + console.print(f"❌ [red]Delete error:[/red] {e}") + import sys + sys.exit(1) diff --git a/runagent/cli/commands/deploy.py b/runagent/cli/commands/deploy.py new file mode 100644 index 0000000..af8d06b --- /dev/null +++ b/runagent/cli/commands/deploy.py @@ -0,0 +1,108 @@ +""" +CLI commands that use the restructured SDK internally. +""" +import os +import json +import uuid + +from pathlib import Path + +import click +from rich.console import Console +from rich.table import Table + +from runagent import RunAgent +from runagent.sdk.exceptions import ( # RunAgentError,; ConnectionError + AuthenticationError, + TemplateError, + ValidationError, +) +from runagent.client.client import RunAgentClient +from runagent.sdk.server.local_server import LocalServer +from runagent.utils.agent import detect_framework +from runagent.utils.animation import show_subtle_robotic_runner, show_quick_runner +from runagent.utils.config import Config +from runagent.sdk.deployment.middleware_sync import get_middleware_sync +from runagent.cli.utils import add_framework_options, get_selected_framework +from runagent.utils.enums.framework import Framework +console = Console() + + +def format_error_message(error_info): + """Format error information from API responses""" + if isinstance(error_info, dict) and "message" in error_info: + # New format with ErrorDetail object + error_message = error_info.get("message", "Unknown error") + error_code = error_info.get("code", "UNKNOWN_ERROR") + return f"[{error_code}] {error_message}" + else: + # Fallback to old format for backward compatibility + return str(error_info) if error_info else "Unknown error" + + +# ============================================================================ +# Config Command Group +# ============================================================================ + + +@click.command() +@click.argument( + "path", + type=click.Path( + exists=True, + file_okay=False, + dir_okay=True, + readable=True, + resolve_path=True, + path_type=Path, + ), + default=".", +) +def deploy(path: Path): + """Deploy agent (upload + start) to remote server""" + + try: + from runagent.cli.branding import print_header + print_header("Deploy Agent") + + sdk = RunAgent() + + # Check authentication + if not sdk.is_configured(): + console.print( + "❌ [red]Not authenticated.[/red] Run [cyan]'runagent setup --api-key '[/cyan] first" + ) + raise click.ClickException("Authentication required") + + # Validate folder + if not Path(path).exists(): + raise click.ClickException(f"Folder not found: {path}") + + console.print(f"🎯 [bold]Deploying agent (upload + start)...[/bold]") + console.print(f"📁 Source: [cyan]{path}[/cyan]") + + # Deploy agent (framework auto-detected) + result = sdk.deploy_remote(folder=str(path)) + + if result.get("success"): + console.print(f"\n✅ [green]Deployment successful![/green]") + console.print(f"🆔 Agent ID: [bold magenta]{result.get('agent_id')}[/bold magenta]") + console.print(f"🌐 Endpoint: [link]{result.get('endpoint')}[/link]") + else: + console.print(f"❌ [red]Deployment failed:[/red] {format_error_message(result.get('error'))}") + import sys + sys.exit(1) + + except AuthenticationError as e: + if os.getenv('DISABLE_TRY_CATCH'): + raise + console.print(f"❌ [red]Authentication error:[/red] {e}") + import sys + sys.exit(1) + except Exception as e: + if os.getenv('DISABLE_TRY_CATCH'): + raise + console.print(f"❌ [red]Deployment error:[/red] {e}") + import sys + sys.exit(1) + diff --git a/runagent/cli/commands/init.py b/runagent/cli/commands/init.py new file mode 100644 index 0000000..8550b74 --- /dev/null +++ b/runagent/cli/commands/init.py @@ -0,0 +1,374 @@ +""" +CLI commands that use the restructured SDK internally. +""" +import os +import json +import uuid + +from pathlib import Path + +import click +from rich.console import Console +from rich.table import Table + +from runagent import RunAgent +from runagent.sdk.exceptions import ( # RunAgentError,; ConnectionError + AuthenticationError, + TemplateError, + ValidationError, +) +from runagent.client.client import RunAgentClient +from runagent.sdk.server.local_server import LocalServer +from runagent.utils.agent import detect_framework +from runagent.utils.animation import show_subtle_robotic_runner, show_quick_runner +from runagent.utils.config import Config +from runagent.sdk.deployment.middleware_sync import get_middleware_sync +from runagent.cli.utils import add_framework_options, get_selected_framework +from runagent.utils.enums.framework import Framework +console = Console() + + +def format_error_message(error_info): + """Format error information from API responses""" + if isinstance(error_info, dict) and "message" in error_info: + # New format with ErrorDetail object + error_message = error_info.get("message", "Unknown error") + error_code = error_info.get("code", "UNKNOWN_ERROR") + return f"[{error_code}] {error_message}" + else: + # Fallback to old format for backward compatibility + return str(error_info) if error_info else "Unknown error" + + +# ============================================================================ +# Config Command Group +# ============================================================================ + + +@click.command() +@click.option("--template", help="Template variant (default, advanced, etc.) - for non-interactive") +@click.option("--blank", is_flag=True, help="Start from blank template - for non-interactive") +@click.option("--name", help="Agent name - for non-interactive") +@click.option("--description", help="Agent description - for non-interactive") +@click.option("--overwrite", is_flag=True, help="Overwrite existing folder") +@add_framework_options # Adds framework flags for non-interactive +@click.argument( + "path", + type=click.Path( + file_okay=False, + dir_okay=True, + readable=True, + resolve_path=True, + path_type=Path, + ), + default=".", + required=False, +) +def init(template, blank, name, description, overwrite, path, **kwargs): + """ + Initialize a new RunAgent project + + \b + Interactive mode (default - recommended): + $ runagent init + + \b + Non-interactive with template: + $ runagent init --framework langgraph --template advanced --name "My Agent" --description "Does XYZ" ./my-agent + + \b + Non-interactive blank: + $ runagent init --blank --name "Custom Agent" --description "My custom implementation" + """ + + try: + from runagent.cli.branding import print_header + from rich.prompt import Prompt + from rich.panel import Panel + import inquirer + + print_header("Initialize Project") + + sdk = RunAgent() + + # Determine if interactive mode + selected_framework = get_selected_framework(kwargs) + has_required_non_interactive = ( + (selected_framework or blank) and name and description + ) + is_interactive = not has_required_non_interactive + + # Variables to collect + agent_name = name + agent_description = description + use_blank = blank + framework = selected_framework + selected_template = template or "default" + + if is_interactive: + # Step 1: Choose blank or template + console.print("[bold cyan]How would you like to start?[/bold cyan]\n") + + start_questions = [ + inquirer.List( + 'start_type', + message="Select starting point", + choices=[ + ('📦 From Template (recommended)', 'template'), + ('📄 Blank Project (advanced)', 'blank'), + ], + default=('📦 From Template (recommended)', 'template'), + carousel=True + ), + ] + + start_answer = inquirer.prompt(start_questions) + if not start_answer: + console.print("[dim]Initialization cancelled.[/dim]") + return + + use_blank = (start_answer['start_type'] == 'blank') + + # Step 2: If template, select framework and template + if not use_blank: + # Select framework + console.print("\n[bold]Select framework:[/bold]\n") + selectable_frameworks = Framework.get_selectable_frameworks() + + framework_choices = [] + for fw in selectable_frameworks: + category_emoji = "🐍" if fw.is_pythonic() else "🌐" if fw.is_webhook() else "❓" + label = f"{category_emoji} {fw.value} ({fw.category})" + framework_choices.append((label, fw)) + + fw_questions = [ + inquirer.List( + 'framework', + message="Choose framework", + choices=framework_choices, + carousel=True + ), + ] + + fw_answer = inquirer.prompt(fw_questions) + if not fw_answer: + console.print("[dim]Initialization cancelled.[/dim]") + return + + framework = fw_answer['framework'] + + # Select template for chosen framework + console.print(f"\n[bold]Select template for {framework.value}:[/bold]") + + # Fetch templates with real progress feedback + from rich.status import Status + import time + + fetch_start = time.time() + + with Status( + "[cyan]Fetching available templates...[/cyan]", + console=console, + spinner="dots" + ) as status: + clone_start = time.time() + status.update("[cyan]Cloning template repository...[/cyan]") + + templates = sdk.list_templates(framework.value) + clone_time = time.time() - clone_start + + status.update(f"[cyan]Templates fetched ({clone_time:.1f}s)[/cyan]") + template_list = templates.get(framework.value, ["default"]) + + fetch_time = time.time() - fetch_start + + console.print(f"[dim]✓ Found {len(template_list)} template(s) in {fetch_time:.1f}s[/dim]") + + # Auto-select if only one template available + if len(template_list) == 1: + selected_template = template_list[0] + console.print(f"[dim]→ Using template: {selected_template}[/dim]\n") + else: + # Show dropdown for multiple templates + console.print() + template_choices = [(f"🧱 {tmpl}", tmpl) for tmpl in template_list] + + tmpl_questions = [ + inquirer.List( + 'template', + message="Choose template", + choices=template_choices, + carousel=True + ), + ] + + tmpl_answer = inquirer.prompt(tmpl_questions) + if not tmpl_answer: + console.print("[dim]Initialization cancelled.[/dim]") + return + + selected_template = tmpl_answer['template'] + else: + # Blank project uses default framework + framework = Framework.DEFAULT + selected_template = "default" + + # Step 3: Get agent name and description (for both blank and template) + console.print("\n[bold]Agent Details:[/bold]\n") + + agent_name = Prompt.ask( + "[cyan]Agent name[/cyan]", + default="my-agent" + ) + + agent_description = Prompt.ask( + "[cyan]Agent description[/cyan]", + default="My AI agent" + ) + + # Step 4: Get path (default based on agent name) + console.print() + # Convert agent name to valid directory name (replace spaces with hyphens, lowercase) + default_path = agent_name.lower().replace(" ", "-").replace("_", "-") + path_input = Prompt.ask( + "[cyan]Project path[/cyan]", + default=default_path + ) + path = Path(path_input) + + # Ensure framework is set + if not framework: + framework = Framework.DEFAULT + + # Validate framework if it came from string input + if isinstance(framework, str): + try: + framework = Framework.from_string(framework) + except ValueError as e: + raise click.UsageError(str(e)) + + # Use the path as the project location + project_path = path.resolve() + + # Ensure the path exists + project_path.parent.mkdir(parents=True, exist_ok=True) + + # Show configuration summary + console.print(Panel( + f"[bold]Project Configuration:[/bold]\n\n" + f"[dim]Name:[/dim] [cyan]{agent_name}[/cyan]\n" + f"[dim]Description:[/dim] [white]{agent_description}[/white]\n" + f"[dim]Framework:[/dim] [magenta]{framework.value}[/magenta]\n" + f"[dim]Template:[/dim] [yellow]{selected_template}[/yellow]\n" + f"[dim]Path:[/dim] [blue]{project_path}[/blue]", + title="[bold cyan]Creating Agent[/bold cyan]", + border_style="cyan" + )) + + # Initialize project + success = sdk.init_project( + folder_path=project_path, + framework=framework.value, + template=selected_template, + overwrite=overwrite + ) + + if not success: + raise Exception("Project initialization failed") + + # Update config file with name and description + try: + import warnings + from datetime import datetime + + # Suppress Pydantic datetime warnings during config update + warnings.filterwarnings('ignore', category=UserWarning, module='pydantic') + + config_path = project_path / "runagent.config.json" + if config_path.exists(): + with open(config_path, 'r') as f: + config_data = json.load(f) + + config_data['name'] = agent_name + config_data['description'] = agent_description + + # Fix created_at format if it exists and is a string in wrong format + if 'created_at' in config_data and isinstance(config_data['created_at'], str): + try: + # Try to parse and convert to ISO format + dt = datetime.strptime(config_data['created_at'], "%Y-%m-%d %H:%M:%S") + config_data['created_at'] = dt.isoformat() + except: + # If parsing fails, use current time in ISO format + config_data['created_at'] = datetime.now().isoformat() + + with open(config_path, 'w') as f: + json.dump(config_data, f, indent=2) + + console.print("\n[dim]✓ Updated agent name and description in config[/dim]") + except Exception as e: + console.print(f"[yellow]⚠️ Could not update config: {e}[/yellow]") + + # Success message + relative_path = project_path.relative_to(Path.cwd()) if project_path != Path.cwd() else Path(".") + + console.print(Panel( + f"[bold green]✅ Agent '{agent_name}' created successfully![/bold green]\n\n" + f"[dim]Location:[/dim] [cyan]{relative_path}[/cyan]\n" + f"[dim]Framework:[/dim] [magenta]{framework.value}[/magenta]", + title="[bold green]Success[/bold green]", + border_style="green" + )) + + # Simple next steps + console.print("\n💡 [bold]Next Steps:[/bold]") + if relative_path != Path("."): + console.print(f" 1️⃣ [cyan]cd {relative_path}[/cyan]") + console.print(f" 2️⃣ Install dependencies: [cyan]pip install -r requirements.txt[/cyan]") + console.print(f" 3️⃣ Serve locally: [cyan]runagent serve .[/cyan]") + else: + console.print(f" 1️⃣ Install dependencies: [cyan]pip install -r requirements.txt[/cyan]") + console.print(f" 2️⃣ Serve locally: [cyan]runagent serve .[/cyan]") + + except TemplateError as e: + if os.getenv('DISABLE_TRY_CATCH'): + raise + console.print(Panel( + f"[bold red]Template Error[/bold red]\n\n" + f"{str(e)}\n\n" + f"[dim]Please check that the selected framework and template are valid.[/dim]", + title="[bold red]❌ Failed[/bold red]", + border_style="red" + )) + import sys + sys.exit(1) + except FileExistsError as e: + if os.getenv('DISABLE_TRY_CATCH'): + raise + + # Extract just the path from the error message + path_match = str(e).split("'") + folder_path = path_match[1] if len(path_match) > 1 else "the specified path" + + console.print(Panel( + f"[bold yellow]Directory Already Exists[/bold yellow]\n\n" + f"[dim]Path:[/dim] [cyan]{folder_path}[/cyan]\n\n" + f"The directory already exists and is not empty.\n\n" + f"[bold]Options:[/bold]\n" + f" • Choose a different path\n" + f" • Use [cyan]--overwrite[/cyan] flag to replace existing files\n" + f" • Remove the directory manually", + title="[bold yellow]⚠️ Path Conflict[/bold yellow]", + border_style="yellow" + )) + import sys + sys.exit(1) + except click.UsageError: + # Re-raise UsageError as-is for proper click handling + raise + except Exception as e: + if os.getenv('DISABLE_TRY_CATCH'): + raise + console.print(f"❌ [red]Initialization error:[/red] {e}") + raise click.ClickException("Project initialization failed") + diff --git a/runagent/cli/commands/run.py b/runagent/cli/commands/run.py new file mode 100644 index 0000000..1417769 --- /dev/null +++ b/runagent/cli/commands/run.py @@ -0,0 +1,235 @@ +""" +CLI commands that use the restructured SDK internally. +""" +import os +import json +import uuid + +from pathlib import Path + +import click +from rich.console import Console +from rich.table import Table + +from runagent import RunAgent +from runagent.sdk.exceptions import ( # RunAgentError,; ConnectionError + AuthenticationError, + TemplateError, + ValidationError, +) +from runagent.client.client import RunAgentClient +from runagent.sdk.server.local_server import LocalServer +from runagent.utils.agent import detect_framework +from runagent.utils.animation import show_subtle_robotic_runner, show_quick_runner +from runagent.utils.config import Config +from runagent.sdk.deployment.middleware_sync import get_middleware_sync +from runagent.cli.utils import add_framework_options, get_selected_framework +from runagent.utils.enums.framework import Framework +console = Console() + + +def format_error_message(error_info): + """Format error information from API responses""" + if isinstance(error_info, dict) and "message" in error_info: + # New format with ErrorDetail object + error_message = error_info.get("message", "Unknown error") + error_code = error_info.get("code", "UNKNOWN_ERROR") + return f"[{error_code}] {error_message}" + else: + # Fallback to old format for backward compatibility + return str(error_info) if error_info else "Unknown error" + + +# ============================================================================ +# Config Command Group +# ============================================================================ + + + +@click.command( + context_settings=dict( + ignore_unknown_options=True, + allow_extra_args=True, + )) +@click.option("--id", "agent_id", help="Agent ID to run") +@click.option("--host", help="Host to connect to (use with --port)") +@click.option("--port", type=int, help="Port to connect to (use with --host)") +@click.option( + "--input", + "input_file", + type=click.Path(exists=True, file_okay=True, dir_okay=False, readable=True, path_type=Path), + help="Path to input JSON file" +) +@click.option("--local", is_flag=True, help="Run agent locally") +@click.option("--tag", required=True, help="Entrypoint tag to be used") +# @click.option("--generic-stream", is_flag=True, help="Use generic streaming mode") +@click.option("--timeout", type=int, help="Timeout in seconds") +@click.pass_context +def run(ctx, agent_id, host, port, input_file, local, tag, timeout): + """ + Run an agent with flexible configuration options + + Examples: + # Using agent ID with extra params + runagent run --agent-id my-agent --param1=value1 --param2=value2 + + # Using host/port with input file + runagent run --host localhost --port 8080 --input config.json + + # local agent + runagent run --id d33c497d-d3f5-462e-8ff4-c28d819b92d6 --tag minimal --local --message=something + + # remote agent + runagent run --id d33c497d-d3f5-462e-8ff4-c28d819b92d6 --tag minimal --message=something + """ + from runagent.cli.branding import print_header + print_header("Run Agent") + + # ============================================ + # VALIDATION 1: Either agent-id OR host/port + # ============================================ + agent_id_provided = agent_id is not None + host_port_provided = host is not None or port is not None + + if agent_id_provided and host_port_provided: + raise click.UsageError( + "Cannot specify both --agent-id and --host/--port. " + "Choose one approach." + ) + + if not agent_id_provided and not host_port_provided: + raise click.UsageError( + "Must specify either --agent-id or both --host and --port." + ) + + # If using host/port, both must be provided + if host_port_provided and (host is None or port is None): + raise click.UsageError( + "When using host/port, both --host and --port must be specified." + ) + + # ============================================ + # # VALIDATION 2: tag validation + # # ============================================ + if tag.endswith("_stream"): + console.print(f"❌ [bold red]Execution failed:[/bold red] Cannot use streaming Entrypoint tag `{tag}` through non-streaming endpoint.") + return + + + # ============================================ + # VALIDATION 3: Input file OR extra params + # ============================================ + + # Parse extra parameters from ctx.args + extra_params = {} + invalid_args = [] + + for arg in ctx.args: + if arg.startswith('--') and '=' in arg: + # Valid format: --key=value + key, value = arg[2:].split('=', 1) + extra_params[key] = value + else: + # Invalid format + invalid_args.append(arg) + + if invalid_args: + raise click.UsageError( + f"Invalid extra arguments: {invalid_args}. " + "Extra parameters must be in --key=value format." + ) + + # Check mutual exclusivity of input file and extra params + if input_file and extra_params: + raise click.UsageError( + "Cannot specify both --input file and extra parameters. " + "Use either --input config.json OR --param1=value1 --param2=value2" + ) + + if not input_file and not extra_params: + console.print("⚠️ No input file or extra parameters provided. Running with defaults.") + + # ============================================ + # DISPLAY CONFIGURATION + # ============================================ + + console.print("🚀 RunAgent Configuration:") + + # Connection info + if agent_id: + console.print(f" Agent ID: [cyan]{agent_id}[/cyan]") + else: + console.print(f" Host: [cyan]{host}[/cyan]") + console.print(f" Port: [cyan]{port}[/cyan]") + + # Tag + # mode = "Generic Streaming" if generic_stream else "Generic" + console.print(f" Tag: [magenta]{tag}[/magenta]") + + # Local execution + if local: + console.print(" Local: [green]Yes[/green]") + else: + console.print(" Local: [red]No(Deployed to RunAgent Cloud)[/red]") + + # Timeout + if timeout: + console.print(f" Timeout: [yellow]{timeout}s[/yellow]") + + # Input configuration + if input_file: + console.print(f" Input file: [blue]{input_file}[/blue]") + # Load and validate JSON file here + try: + import json + with open(input_file, 'r') as f: + input_params = json.load(f) + console.print(f" Config keys: [dim]{list(input_params.keys())}[/dim]") + except json.JSONDecodeError: + if os.getenv('DISABLE_TRY_CATCH'): + raise + raise click.ClickException(f"Invalid JSON in input file: {input_file}") + except Exception as e: + if os.getenv('DISABLE_TRY_CATCH'): + raise + raise click.ClickException(f"Error reading input file: {e}") + + elif extra_params: + console.print(" Extra parameters:") + for key, value in extra_params.items(): + # Try to parse value as JSON for complex types + # TODO: Will add type inference later + console.print(f" --{key} = [green]{value}[/green]") + input_params = extra_params + + else: + input_params = {} + + # ============================================ + # EXECUTION LOGIC + # ============================================ + + try: + ra_client = RunAgentClient( + agent_id=agent_id, + local=local, + host=host, + port=port, + entrypoint_tag=tag + ) + + if tag.endswith("_stream"): + for item in ra_client.run(**input_params): + console.print(item) + else: + result = ra_client.run(**input_params) + console.print(result) + + except Exception as e: + if os.getenv('DISABLE_TRY_CATCH'): + raise + # Display error with red ❌ symbol + console.print(f"❌ [bold red]Execution failed:[/bold red] {e}") + # Exit with error code 1 instead of raising ClickException to avoid duplicate message + import sys + sys.exit(1) diff --git a/runagent/cli/commands/run_stream.py b/runagent/cli/commands/run_stream.py new file mode 100644 index 0000000..65f4982 --- /dev/null +++ b/runagent/cli/commands/run_stream.py @@ -0,0 +1,204 @@ +""" +CLI commands that use the restructured SDK internally. +""" +import os +import json +import uuid + +from pathlib import Path + +import click +from rich.console import Console +from rich.table import Table + +from runagent import RunAgent +from runagent.sdk.exceptions import ( # RunAgentError,; ConnectionError + AuthenticationError, + TemplateError, + ValidationError, +) +from runagent.client.client import RunAgentClient +from runagent.sdk.server.local_server import LocalServer +from runagent.utils.agent import detect_framework +from runagent.utils.animation import show_subtle_robotic_runner, show_quick_runner +from runagent.utils.config import Config +from runagent.sdk.deployment.middleware_sync import get_middleware_sync +from runagent.cli.utils import add_framework_options, get_selected_framework +from runagent.utils.enums.framework import Framework +console = Console() + + +def format_error_message(error_info): + """Format error information from API responses""" + if isinstance(error_info, dict) and "message" in error_info: + # New format with ErrorDetail object + error_message = error_info.get("message", "Unknown error") + error_code = error_info.get("code", "UNKNOWN_ERROR") + return f"[{error_code}] {error_message}" + else: + # Fallback to old format for backward compatibility + return str(error_info) if error_info else "Unknown error" + + +# ============================================================================ +# Config Command Group +# ============================================================================ + + +@click.command( + context_settings=dict( + ignore_unknown_options=True, + allow_extra_args=True, + )) +@click.option("--id", "agent_id", help="Agent ID to run") +@click.option("--host", help="Host to connect to (use with --port)") +@click.option("--port", type=int, help="Port to connect to (use with --host)") +@click.option( + "--input", + "input_file", + type=click.Path(exists=True, file_okay=True, dir_okay=False, readable=True, path_type=Path), + help="Path to input JSON file" +) +@click.option("--local", is_flag=True, help="Run agent locally") +@click.option("--tag", required=True, help="Entrypoint tag to be used") +@click.option("--timeout", type=int, help="Timeout in seconds") +@click.pass_context +def run_stream(ctx, agent_id, host, port, input_file, local, tag, timeout): + """ + Stream agent execution results in real-time. + + This command connects to an agent via WebSocket and streams the execution results + as they become available, providing real-time feedback. + + Examples: + # Local streaming agent + runagent run-stream --id d33c497d-d3f5-462e-8ff4-c28d819b92d6 --tag minimal_stream --local --message=something + + # Remote streaming agent + runagent run-stream --id d33c497d-d3f5-462e-8ff4-c28d819b92d6 --tag minimal_stream --message=something + + # With input file + runagent run-stream --id d33c497d-d3f5-462e-8ff4-c28d819b92d6 --tag minimal_stream --local --input config.json + """ + from runagent.cli.branding import print_header + print_header("Stream Agent Output") + + # ============================================ + # PARAMETER PARSING + # ============================================ + + extra_params = {} + for item in ctx.args: + if '=' in item: + key, value = item.split('=', 1) + # Remove leading dashes + key = key.lstrip('-') + extra_params[key] = value + else: + # Handle boolean flags + key = item.lstrip('-') + extra_params[key] = True + + # ============================================ + # VALIDATION + # ============================================ + + # VALIDATION 1: Agent ID or host/port required + if not agent_id and not (host and port): + console.print(f"❌ [bold red]Execution failed:[/bold red] Either --id or both --host and --port are required") + import sys + sys.exit(1) + + # VALIDATION 2: tag validation for streaming + if not tag.endswith("_stream"): + console.print(f"❌ [bold red]Execution failed:[/bold red] Streaming command requires entrypoint tag ending with '_stream'. Got: {tag}") + import sys + sys.exit(1) + + # ============================================ + # DISPLAY CONFIGURATION + # ============================================ + + console.print("🚀 RunAgent Streaming Configuration:") + + # Connection info + if agent_id: + console.print(f" Agent ID: [cyan]{agent_id}[/cyan]") + else: + console.print(f" Host: [cyan]{host}[/cyan]") + console.print(f" Port: [cyan]{port}[/cyan]") + + # Tag + console.print(f" Tag: [magenta]{tag}[/magenta]") + + # Local execution + if local: + console.print(" Local: [green]Yes[/green]") + else: + console.print(" Local: [red]No (Deployed to RunAgent Cloud)[/red]") + + # Timeout + if timeout: + console.print(f" Timeout: [yellow]{timeout}s[/yellow]") + + # Input configuration + if input_file: + console.print(f" Input file: [blue]{input_file}[/blue]") + # Load and validate JSON file here + try: + import json + with open(input_file, 'r') as f: + input_params = json.load(f) + console.print(f" Config keys: [dim]{list(input_params.keys())}[/dim]") + except json.JSONDecodeError: + if os.getenv('DISABLE_TRY_CATCH'): + raise + console.print(f"❌ [bold red]Execution failed:[/bold red] Invalid JSON in input file: {input_file}") + import sys + sys.exit(1) + except Exception as e: + if os.getenv('DISABLE_TRY_CATCH'): + raise + console.print(f"❌ [bold red]Execution failed:[/bold red] Error reading input file: {e}") + import sys + sys.exit(1) + + elif extra_params: + console.print(" Extra parameters:") + for key, value in extra_params.items(): + console.print(f" --{key} = {value}") + input_params = extra_params + + else: + input_params = {} + + # ============================================ + # EXECUTION LOGIC + # ============================================ + + try: + ra_client = RunAgentClient( + agent_id=agent_id, + local=local, + host=host, + port=port, + entrypoint_tag=tag + ) + + console.print(f"\n🔄 [bold]Starting streaming execution...[/bold]") + console.print(f"📡 [dim]Connected to agent via WebSocket[/dim]") + console.print(f"📤 [dim]Streaming results:[/dim]\n") + + # Stream the results + for chunk in ra_client.run_stream(**input_params): + console.print(chunk) + + except Exception as e: + if os.getenv('DISABLE_TRY_CATCH'): + raise + # Display error with red ❌ symbol + console.print(f"❌ [bold red]Streaming failed:[/bold red] {e}") + # Exit with error code 1 instead of raising ClickException to avoid duplicate message + import sys + sys.exit(1) + diff --git a/runagent/cli/commands/serve.py b/runagent/cli/commands/serve.py new file mode 100644 index 0000000..72a7843 --- /dev/null +++ b/runagent/cli/commands/serve.py @@ -0,0 +1,222 @@ +""" +CLI commands that use the restructured SDK internally. +""" +import os +import json +import uuid + +from pathlib import Path + +import click +from rich.console import Console +from rich.table import Table + +from runagent import RunAgent +from runagent.sdk.exceptions import ( # RunAgentError,; ConnectionError + AuthenticationError, + TemplateError, + ValidationError, +) +from runagent.client.client import RunAgentClient +from runagent.sdk.server.local_server import LocalServer +from runagent.utils.agent import detect_framework +from runagent.utils.animation import show_subtle_robotic_runner, show_quick_runner +from runagent.utils.config import Config +from runagent.sdk.deployment.middleware_sync import get_middleware_sync +from runagent.cli.utils import add_framework_options, get_selected_framework +from runagent.utils.enums.framework import Framework +console = Console() + + +def format_error_message(error_info): + """Format error information from API responses""" + if isinstance(error_info, dict) and "message" in error_info: + # New format with ErrorDetail object + error_message = error_info.get("message", "Unknown error") + error_code = error_info.get("code", "UNKNOWN_ERROR") + return f"[{error_code}] {error_message}" + else: + # Fallback to old format for backward compatibility + return str(error_info) if error_info else "Unknown error" + + +# ============================================================================ +# Config Command Group +# ============================================================================ + + + +@click.command() +@click.option("--port", type=int, help="Preferred port (auto-allocated if unavailable)") +@click.option("--host", default="127.0.0.1", help="Host to bind server to") +@click.option("--debug", is_flag=True, help="Run server in debug mode") +@click.option("--replace", help="Replace existing agent with this agent ID") +@click.option("--no-animation", is_flag=True, help="Skip startup animation") +@click.option("--animation-style", + type=click.Choice(["field", "ascii", "minimal", "quick"]), + default="field", + help="Animation style") +@click.argument( + "path", + type=click.Path( + exists=True, + file_okay=False, + dir_okay=True, + readable=True, + resolve_path=True, + path_type=Path, + ), + default=".", +) +def serve(port, host, debug, replace, no_animation, animation_style, path): + """Start local FastAPI server with subtle robotic runner animation""" + + try: + from runagent.cli.branding import print_header + print_header("Serve Agent Locally") + + # Show subtle startup animation + if not no_animation: + console.print("\n") + + if animation_style == "quick": + show_quick_runner(duration=1.5) + else: + show_subtle_robotic_runner(duration=2.0, style=animation_style) + + sdk = RunAgent() + + # Handle replace operation + if replace: + console.print(f"🔄 [yellow]Replacing agent: {replace}[/yellow]") + + # Check if the agent to replace exists + existing_agent = sdk.db_service.get_agent(replace) + if not existing_agent: + console.print(f"⚠️ [yellow]Agent {replace} not found in database[/yellow]") + console.print("💡 Available agents:") + agents = sdk.db_service.list_agents() + for agent in agents[:5]: # Show first 5 + console.print(f" • {agent['agent_id']} ({agent['framework']})") + raise click.ClickException("Agent to replace not found") + + # Generate new agent ID + import uuid + new_agent_id = str(uuid.uuid4()) + + # Get currently used ports to avoid conflicts + used_ports = [] + all_agents = sdk.db_service.list_agents() + for agent in all_agents: + if agent.get('port') and agent['agent_id'] != replace: # Exclude the agent being replaced + used_ports.append(agent['port']) + + # Allocate host and port + from runagent.utils.port import PortManager + if port and PortManager.is_port_available(host, port): + allocated_host = host + allocated_port = port + console.print(f"🎯 Using specified address: [blue]{allocated_host}:{allocated_port}[/blue]") + else: + allocated_host, allocated_port = PortManager.allocate_unique_address(used_ports) + console.print(f"🔌 Auto-allocated address: [blue]{allocated_host}:{allocated_port}[/blue]") + + # Use the existing replace_agent method with proper port allocation + result = sdk.db_service.replace_agent( + old_agent_id=replace, + new_agent_id=new_agent_id, + agent_path=str(path), + host=allocated_host, + port=allocated_port, # Ensure port is not None + framework=detect_framework(path).value, + ) + + if not result["success"]: + raise click.ClickException(f"Failed to replace agent: {result['error']}") + + console.print(f"✅ [green]Agent replaced successfully![/green]") + console.print(f"🆔 New Agent ID: [bold magenta]{new_agent_id}[/bold magenta]") + console.print(f"🔌 Address: [bold blue]{allocated_host}:{allocated_port}[/bold blue]") + + # Create server with the new agent ID and allocated host/port + from runagent.sdk.db import DBService + db_service = DBService() + + server = LocalServer( + db_service=db_service, + agent_id=new_agent_id, + agent_path=path, + port=allocated_port, + host=allocated_host, + ) + else: + # Normal operation - check capacity if not replacing + capacity_info = sdk.db_service.get_database_capacity_info() + if capacity_info["is_full"] and not replace: + console.print("❌ [red]Database is full![/red]") + oldest_agent = capacity_info.get("oldest_agent", {}) + if oldest_agent: + console.print(f"💡 [yellow]Suggested commands:[/yellow]") + console.print(f" Replace: [cyan]runagent serve {path} --replace {oldest_agent.get('agent_id', '')}[/cyan]") + console.print(f" Delete: [cyan]runagent delete --id {oldest_agent.get('agent_id', '')}[/cyan]") + raise click.ClickException("Database at capacity. Use --replace or use 'runagent delete' to free space.") + + console.print("⚡ [bold]Starting local server with auto port allocation...[/bold]") + + # Use the existing LocalServer.from_path method + server = LocalServer.from_path(path, port=port, host=host) + + # Common server startup code + allocated_host = server.host + allocated_port = server.port + + console.print(f"🌐 URL: [bold blue]http://{allocated_host}:{allocated_port}[/bold blue]") + console.print(f"📖 Docs: [link]http://{allocated_host}:{allocated_port}/docs[/link]") + + try: + + sync_service = get_middleware_sync() + sync_enabled = sync_service.is_sync_enabled() + api_key_set = bool(Config.get_api_key()) + + console.print(f"\n🔄 [bold]Middleware Sync Status:[/bold]") + if sync_enabled: + console.print(f" Status: [green]✅ ENABLED[/green]") + console.print(f" 📊 Local invocations will sync to middleware") + + # Test connection + try: + test_result = sync_service.test_connection() + if test_result.get("success"): + console.print(f" Connection: [green]✅ Connected to middleware[/green]") + else: + console.print(f" Connection: [red]❌ Failed to connect: {test_result.get('error', 'Unknown error')}[/red]") + except Exception as e: + if os.getenv('DISABLE_TRY_CATCH'): + raise + console.print(f" Connection: [red]❌ Connection test failed: {e}[/red]") + else: + console.print(f" Status: [yellow]⚠️ DISABLED[/yellow]") + if not api_key_set: + console.print(f" Reason: [yellow]API key not configured[/yellow]") + console.print(f" 💡 Setup: [cyan]runagent setup --api-key [/cyan]") + else: + user_disabled = not Config.get_user_config().get("local_sync_enabled", True) + if user_disabled: + console.print(f" Reason: [yellow]Disabled by user[/yellow]") + console.print(f" 💡 Enable: [cyan]runagent local-sync --enable[/cyan]") + console.print(f" 📊 Local invocations will only be stored locally") + + except Exception as e: + console.print(f"[dim]Note: Could not check middleware sync status: {e}[/dim]") + + # Start server (this will block) + server.start(debug=debug) + + except KeyboardInterrupt: + console.print("\n🛑 [yellow]Server stopped by user[/yellow]") + except Exception as e: + if os.getenv('DISABLE_TRY_CATCH'): + raise + console.print(f"❌ [red]Server error:[/red] {e}") + raise click.ClickException("Server failed to start") diff --git a/runagent/cli/commands/setup.py b/runagent/cli/commands/setup.py new file mode 100644 index 0000000..2741a7c --- /dev/null +++ b/runagent/cli/commands/setup.py @@ -0,0 +1,238 @@ +""" +CLI commands that use the restructured SDK internally. +""" +import os +import json +import uuid + +from pathlib import Path + +import click +from rich.console import Console +from rich.table import Table + +from runagent import RunAgent +from runagent.sdk.exceptions import ( # RunAgentError,; ConnectionError + AuthenticationError, + TemplateError, + ValidationError, +) +from runagent.client.client import RunAgentClient +from runagent.sdk.server.local_server import LocalServer +from runagent.utils.agent import detect_framework +from runagent.utils.animation import show_subtle_robotic_runner, show_quick_runner +from runagent.utils.config import Config +from runagent.sdk.deployment.middleware_sync import get_middleware_sync +from runagent.cli.utils import add_framework_options, get_selected_framework +from runagent.utils.enums.framework import Framework +console = Console() + + +@click.command() +@click.option("--again", is_flag=True, help="Reconfigure even if already setup") +def setup(again): + """ + Setup RunAgent authentication + + \b + First-time setup: + $ runagent setup + + \b + Reconfigure: + $ runagent setup --again + + \b + Change specific settings later: + $ runagent config set-api-key + $ runagent config set-base-url + """ + try: + from runagent.cli.branding import print_setup_banner + from rich.prompt import Prompt, Confirm + from rich.panel import Panel + + sdk = RunAgent() + api_key = Config.get_api_key() + + # Check if already configured + if api_key and not again: + config_status = sdk.get_config_status() + user_email = config_status.get('user_info', {}).get('email', 'N/A') + + console.print(Panel( + "[bold cyan]✅ RunAgent is already configured![/bold cyan]\n\n" + f"[dim]User:[/dim] [green]{user_email}[/green]\n" + f"[dim]Base URL:[/dim] [cyan]{config_status.get('base_url')}[/cyan]\n\n" + "[dim]To reconfigure, run:[/dim] [white]runagent setup --again[/white]\n" + "[dim]To view config:[/dim] [white]runagent config status[/white]", + title="[bold]Already Setup[/bold]", + border_style="cyan" + )) + return + + # Show welcome banner for new setup + if not api_key or again: + if not api_key: + print_setup_banner() + else: + console.print("\n[bold cyan]🔄 Reconfiguring RunAgent[/bold cyan]\n") + + # Show setup method options with arrow-key selection + console.print("[bold cyan]Choose your setup method:[/bold cyan]\n") + + import inquirer + + questions = [ + inquirer.List( + 'setup_method', + message="Select setup method", + choices=[ + ('🪄 Express Setup (Browser login - Coming Soon!)', 'express'), + ('🔑 Manual Setup (Enter API key)', 'manual'), + ], + default=('🔑 Manual Setup (Enter API key)', 'manual'), + carousel=True + ), + ] + + answers = inquirer.prompt(questions) + if not answers: + console.print("[dim]Setup cancelled.[/dim]") + return + + choice = answers['setup_method'] + + if choice == "express": + # Express setup - coming soon + console.print(Panel( + "[bold cyan]🚀 Express Setup - Coming Soon![/bold cyan]\n\n" + "This feature will allow you to authenticate via your browser.\n\n" + "[dim]For now, please use Manual Setup[/dim]\n\n" + "📚 [link=https://docs.runagent.dev/setup]Learn more[/link]", + title="[bold]Feature Preview[/bold]", + border_style="cyan" + )) + + if not Confirm.ask("\n[bold]Continue with Manual Setup?[/bold]", default=True): + console.print("[dim]Setup cancelled.[/dim]") + return + + # Manual setup - prompt for API key + console.print("\n[bold white]📝 Manual Setup[/bold white]\n") + api_key = Prompt.ask( + "[cyan]Enter your API key[/cyan]", + password=True + ) + + if not api_key or not api_key.strip(): + console.print(Panel( + "[red]❌ API key cannot be empty[/red]", + title="[bold red]Error[/bold red]", + border_style="red" + )) + raise click.ClickException("Invalid API key") + + console.print("\n🔑 [cyan]Configuring RunAgent...[/cyan]") + + # Configure SDK with validation + try: + from rich.status import Status + from runagent.constants import DEFAULT_BASE_URL + + # Use default base URL from constants + base_url = Config.get_base_url() or DEFAULT_BASE_URL + + with Status("[bold cyan]Validating credentials...", spinner="dots", console=console) as status: + sdk.configure(api_key=api_key, base_url=base_url, save=True) + + console.print(Panel( + "[bold green]✅ Setup completed successfully![/bold green]\n\n" + "[dim]Your credentials have been saved securely.[/dim]", + title="[bold green]Success[/bold green]", + border_style="green" + )) + except AuthenticationError as auth_err: + if os.getenv('DISABLE_TRY_CATCH'): + raise + console.print(f"❌ [red]Authentication failed:[/red] {auth_err}") + + # Provide specific troubleshooting based on error message + error_msg = str(auth_err).lower() + console.print("\n💡 [yellow]Troubleshooting:[/yellow]") + + if "invalid api key" in error_msg or "not authenticated" in error_msg: + console.print(" • Check that your API key is correct") + console.print(" • Verify the API key is not expired") + console.print(" • Ensure you have access to the middleware") + elif "connection" in error_msg or "timeout" in error_msg: + console.print(" • Check your internet connection") + console.print(" • Verify the middleware server is accessible") + from runagent.constants import DEFAULT_BASE_URL + display_url = base_url if 'base_url' in locals() else DEFAULT_BASE_URL + console.print(f" • Trying to connect to: {display_url}") + else: + console.print(" • Check your API key and network connection") + console.print(" • Contact support if the issue persists") + + raise click.ClickException("Authentication failed") + + # Show user information (from cached data) + config_status = sdk.get_config_status() + user_info = config_status.get('user_info', {}) + + if user_info and user_info.get('email'): + from rich.panel import Panel + from rich.table import Table + + # Create info table + info_table = Table(show_header=False, box=None, padding=(0, 2)) + info_table.add_column("", style="dim", no_wrap=True) + info_table.add_column("", style="cyan") + + info_table.add_row("✉️ Email", user_info.get('email')) + info_table.add_row("🎯 Tier", user_info.get('tier', 'Free')) + + # Show active project + user_config = Config.get_user_config() + active_project = user_config.get('active_project_name') + if active_project: + info_table.add_row("📁 Active Project", active_project) + + console.print(Panel( + info_table, + title="[bold]👤 User Information[/bold]", + border_style="cyan" + )) + + # Show sync status (simplified) + console.print("\n🔄 [bold]Middleware Sync Status:[/bold]") + try: + from runagent.sdk.deployment.middleware_sync import MiddlewareSyncService + sync_service = MiddlewareSyncService(sdk.config) + + if sync_service.is_sync_enabled(): + console.print(" Status: [green]✅ ENABLED[/green]") + console.print(" 📊 Local agent runs will sync to middleware") + else: + console.print(" Status: [yellow]⚠️ DISABLED[/yellow]") + console.print(" 📊 Only local storage will be used") + + except Exception as e: + console.print(f" Status: [yellow]Unknown - {e}[/yellow]") + + # Show next steps - Simple workflow + console.print("\n💡 [bold]Next Steps:[/bold]") + console.print(" 1️⃣ Initialize a new agent: [cyan]runagent init[/cyan]") + console.print(" 2️⃣ Serve it locally: [cyan]runagent serve [/cyan]") + console.print(" 3️⃣ Invoke your agent: [cyan]runagent run --id --tag [/cyan]") + + except AuthenticationError: + # Already handled above + raise + except Exception as e: + if os.getenv('DISABLE_TRY_CATCH'): + raise + console.print(f"❌ [red]Setup error:[/red] {e}") + raise click.ClickException("Setup failed") + diff --git a/runagent/cli/commands/start.py b/runagent/cli/commands/start.py new file mode 100644 index 0000000..f292669 --- /dev/null +++ b/runagent/cli/commands/start.py @@ -0,0 +1,101 @@ +""" +CLI commands that use the restructured SDK internally. +""" +import os +import json +import uuid + +from pathlib import Path + +import click +from rich.console import Console +from rich.table import Table + +from runagent import RunAgent +from runagent.sdk.exceptions import ( # RunAgentError,; ConnectionError + AuthenticationError, + TemplateError, + ValidationError, +) +from runagent.client.client import RunAgentClient +from runagent.sdk.server.local_server import LocalServer +from runagent.utils.agent import detect_framework +from runagent.utils.animation import show_subtle_robotic_runner, show_quick_runner +from runagent.utils.config import Config +from runagent.sdk.deployment.middleware_sync import get_middleware_sync +from runagent.cli.utils import add_framework_options, get_selected_framework +from runagent.utils.enums.framework import Framework +console = Console() + + +def format_error_message(error_info): + """Format error information from API responses""" + if isinstance(error_info, dict) and "message" in error_info: + # New format with ErrorDetail object + error_message = error_info.get("message", "Unknown error") + error_code = error_info.get("code", "UNKNOWN_ERROR") + return f"[{error_code}] {error_message}" + else: + # Fallback to old format for backward compatibility + return str(error_info) if error_info else "Unknown error" + + +# ============================================================================ +# Config Command Group +# ============================================================================ + +@click.command() +@click.option("--id", "agent_id", required=True, help="Agent ID to start") +@click.option("--config", help="JSON configuration for deployment") +def start(agent_id, config): + """Start an uploaded agent on remote server""" + + try: + from runagent.cli.branding import print_header + print_header("Start Remote Agent") + + sdk = RunAgent() + + # Check authentication + if not sdk.is_configured(): + console.print( + "❌ [red]Not authenticated.[/red] Run [cyan]'runagent setup --api-key '[/cyan] first" + ) + raise click.ClickException("Authentication required") + + # Parse config + config_dict = {} + if config: + try: + config_dict = json.loads(config) + except json.JSONDecodeError: + if os.getenv('DISABLE_TRY_CATCH'): + raise + raise click.ClickException("Invalid JSON in config parameter") + + console.print(f"🚀 [bold]Starting agent...[/bold]") + console.print(f"🆔 Agent ID: [magenta]{agent_id}[/magenta]") + + # Start agent + result = sdk.start_remote_agent(agent_id, config_dict) + + if result.get("success"): + console.print(f"\n✅ [green]Agent started successfully![/green]") + console.print(f"🌐 Endpoint: [link]{result.get('endpoint')}[/link]") + else: + console.print(f"❌ [red]Start failed:[/red] {format_error_message(result.get('error'))}") + import sys + sys.exit(1) + + except AuthenticationError as e: + if os.getenv('DISABLE_TRY_CATCH'): + raise + console.print(f"❌ [red]Authentication error:[/red] {e}") + import sys + sys.exit(1) + except Exception as e: + if os.getenv('DISABLE_TRY_CATCH'): + raise + console.print(f"❌ [red]Start error:[/red] {e}") + import sys + sys.exit(1) diff --git a/runagent/cli/commands/teardown.py b/runagent/cli/commands/teardown.py new file mode 100644 index 0000000..95ff351 --- /dev/null +++ b/runagent/cli/commands/teardown.py @@ -0,0 +1,127 @@ +""" +CLI commands that use the restructured SDK internally. +""" +import os +import json +import uuid + +from pathlib import Path + +import click +from rich.console import Console +from rich.table import Table + +from runagent import RunAgent +from runagent.sdk.exceptions import ( # RunAgentError,; ConnectionError + AuthenticationError, + TemplateError, + ValidationError, +) +from runagent.client.client import RunAgentClient +from runagent.sdk.server.local_server import LocalServer +from runagent.utils.agent import detect_framework +from runagent.utils.animation import show_subtle_robotic_runner, show_quick_runner +from runagent.utils.config import Config +from runagent.sdk.deployment.middleware_sync import get_middleware_sync +from runagent.cli.utils import add_framework_options, get_selected_framework +from runagent.utils.enums.framework import Framework +console = Console() + + +def format_error_message(error_info): + """Format error information from API responses""" + if isinstance(error_info, dict) and "message" in error_info: + # New format with ErrorDetail object + error_message = error_info.get("message", "Unknown error") + error_code = error_info.get("code", "UNKNOWN_ERROR") + return f"[{error_code}] {error_message}" + else: + # Fallback to old format for backward compatibility + return str(error_info) if error_info else "Unknown error" + + +# ============================================================================ +# Config Command Group +# ============================================================================ + + +@click.command() +@click.option("--yes", is_flag=True, help="Skip confirmation") +def teardown(yes): + """Complete teardown - Remove RunAgent configuration AND database""" + try: + from runagent.cli.branding import print_header + from rich.panel import Panel + from rich.prompt import Confirm + from runagent.constants import LOCAL_CACHE_DIRECTORY, DATABASE_FILE_NAME + from pathlib import Path + + print_header("Complete Teardown") + + sdk = RunAgent() + + if not yes: + config_status = sdk.get_config_status() + db_stats = sdk.db_service.get_database_stats() + + # Show what will be deleted + console.print(Panel( + "[bold red]⚠️ COMPLETE TEARDOWN[/bold red]\n\n" + "This will permanently delete:\n" + " • All configuration (API key, user info, settings)\n" + " • Complete database (all agents, runs, logs, history)\n" + " • All local agent data\n\n" + "[yellow]This action CANNOT be undone![/yellow]", + title="[bold red]Warning[/bold red]", + border_style="red" + )) + + console.print("\n📊 [bold]Current data:[/bold]") + if config_status.get("configured"): + console.print(f" User: [cyan]{config_status.get('user_info', {}).get('email', 'N/A')}[/cyan]") + console.print(f" Total agents: [yellow]{db_stats.get('total_agents', 0)}[/yellow]") + console.print(f" Total runs: [yellow]{db_stats.get('total_runs', 0)}[/yellow]") + console.print(f" Database size: [yellow]{db_stats.get('database_size_mb', 0)} MB[/yellow]\n") + + if not Confirm.ask( + "[bold red]Are you absolutely sure you want to proceed?[/bold red]", + default=False + ): + console.print("[dim]Teardown cancelled.[/dim]") + return + + # Clear configuration from database + sdk.config.clear() + + # Close database connections + sdk.db_service.close() + + # Delete database file + db_path = Path(LOCAL_CACHE_DIRECTORY) / DATABASE_FILE_NAME + if db_path.exists(): + db_path.unlink() + console.print(f"🗑️ [dim]Deleted database: {db_path}[/dim]") + + # Delete legacy JSON file if exists + json_file = Path(LOCAL_CACHE_DIRECTORY) / "user_data.json" + if json_file.exists(): + json_file.unlink() + console.print(f"🗑️ [dim]Deleted legacy config: {json_file}[/dim]") + + console.print(Panel( + "[bold green]✅ RunAgent teardown completed successfully![/bold green]\n\n" + "All configuration and data have been removed.\n\n" + "[dim]To start fresh, run:[/dim] [cyan]runagent setup[/cyan]", + title="[bold green]Complete[/bold green]", + border_style="green" + )) + + except Exception as e: + if os.getenv('DISABLE_TRY_CATCH'): + raise + console.print(Panel( + f"[red]❌ Teardown error:[/red] {str(e)}", + title="[bold red]Error[/bold red]", + border_style="red" + )) + raise click.ClickException("Teardown failed") diff --git a/runagent/cli/commands/upload.py b/runagent/cli/commands/upload.py new file mode 100644 index 0000000..c885f64 --- /dev/null +++ b/runagent/cli/commands/upload.py @@ -0,0 +1,110 @@ +""" +CLI commands that use the restructured SDK internally. +""" +import os +import json +import uuid + +from pathlib import Path + +import click +from rich.console import Console +from rich.table import Table + +from runagent import RunAgent +from runagent.sdk.exceptions import ( # RunAgentError,; ConnectionError + AuthenticationError, + TemplateError, + ValidationError, +) +from runagent.client.client import RunAgentClient +from runagent.sdk.server.local_server import LocalServer +from runagent.utils.agent import detect_framework +from runagent.utils.animation import show_subtle_robotic_runner, show_quick_runner +from runagent.utils.config import Config +from runagent.sdk.deployment.middleware_sync import get_middleware_sync +from runagent.cli.utils import add_framework_options, get_selected_framework +from runagent.utils.enums.framework import Framework +console = Console() + + +def format_error_message(error_info): + """Format error information from API responses""" + if isinstance(error_info, dict) and "message" in error_info: + # New format with ErrorDetail object + error_message = error_info.get("message", "Unknown error") + error_code = error_info.get("code", "UNKNOWN_ERROR") + return f"[{error_code}] {error_message}" + else: + # Fallback to old format for backward compatibility + return str(error_info) if error_info else "Unknown error" + + +# ============================================================================ +# Config Command Group +# ============================================================================ + + +@click.command() +@click.argument( + "path", + type=click.Path( + exists=True, + file_okay=False, + dir_okay=True, + readable=True, + resolve_path=True, + path_type=Path, + ), + default=".", +) +def upload(path: Path): + """Upload agent to remote server""" + + try: + from runagent.cli.branding import print_header + print_header("Upload Agent") + + sdk = RunAgent() + + # Check authentication + if not sdk.is_configured(): + console.print( + "❌ [red]Not authenticated.[/red] Run [cyan]'runagent setup --api-key '[/cyan] first" + ) + raise click.ClickException("Authentication required") + + # Validate folder + if not Path(path).exists(): + raise click.ClickException(f"Folder not found: {path}") + + console.print(f"📤 [bold]Uploading agent...[/bold]") + console.print(f"📁 Source: [cyan]{path}[/cyan]") + + # Upload agent (framework auto-detected) + result = sdk.upload_agent(folder=path) + + if result.get("success"): + agent_id = result["agent_id"] + console.print(f"\n✅ [green]Upload successful![/green]") + console.print(f"🆔 Agent ID: [bold magenta]{agent_id}[/bold magenta]") + console.print(f"\n💡 [bold]Next step:[/bold]") + console.print(f"[cyan]runagent start --id {agent_id}[/cyan]") + else: + console.print(f"❌ [red]Upload failed:[/red] {format_error_message(result.get('error'))}") + import sys + sys.exit(1) + + except AuthenticationError as e: + if os.getenv('DISABLE_TRY_CATCH'): + raise + console.print(f"❌ [red]Authentication error:[/red] {e}") + import sys + sys.exit(1) + except Exception as e: + if os.getenv('DISABLE_TRY_CATCH'): + raise + console.print(f"❌ [red]Upload error:[/red] {e}") + import sys + sys.exit(1) + diff --git a/runagent/cli/main.py b/runagent/cli/main.py index 240e30a..4737586 100644 --- a/runagent/cli/main.py +++ b/runagent/cli/main.py @@ -1,28 +1,79 @@ +import os import click +import warnings +from rich.console import Console from . import commands +from .branding import print_logo -@click.group() -@click.option('--version', is_flag=True, expose_value=False, is_eager=True, callback=commands.print_version, help='Show version information') -def runagent(): +from .commands.setup import setup as setup_cmd +from .commands.config import config as config_cmd +from .commands.teardown import teardown as teardown_cmd +from .commands.init import init as init_cmd +from .commands.upload import upload as upload_cmd +from .commands.start import start as start_cmd +from .commands.deploy import deploy as deploy_cmd +from .commands.serve import serve as serve_cmd +from .commands.run import run as run_cmd +from .commands.run_stream import run_stream as run_stream_cmd +from .commands.delete import delete as delete_cmd +from .commands.db import db as db_cmd + +if not os.getenv('DISABLE_TRY_CATCH'): + warnings.filterwarnings( + "ignore", + message=".*Pydantic serializer warnings.*" + ) + +def show_help_with_logo(ctx, param, value): + """Custom help callback that shows logo before help text""" + if value and not ctx.resilient_parsing: + print_logo(show_tagline=True, brand_color="cyan") + click.echo(ctx.get_help()) + ctx.exit() + +console = Console() + + +def print_version(ctx, param, value): + """Custom version callback with colored output""" + if not value or ctx.resilient_parsing: + return + try: + from runagent.__version__ import __version__ + from runagent.cli.branding import print_compact_logo + print_compact_logo(brand_color="cyan") + console.print(f"\n[bold white]Version:[/bold white] [bold cyan]{__version__}[/bold cyan]") + console.print(f"[dim]Deploy and manage AI agents with ease 🚀[/dim]\n") + except ImportError: + console.print("[red]runagent version unknown[/red]") + ctx.exit() + + +@click.group(invoke_without_command=True) +@click.option('--help', '-h', is_flag=True, expose_value=False, is_eager=True, callback=show_help_with_logo, help='Show this message and exit') +@click.option('--version', is_flag=True, expose_value=False, is_eager=True, callback=print_version, help='Show version information') +@click.pass_context +def runagent(ctx): """RunAgent CLI - Deploy and manage AI agents easily""" - pass - -runagent.add_command(commands.version) -runagent.add_command(commands.setup) -runagent.add_command(commands.teardown) -runagent.add_command(commands.init) -runagent.add_command(commands.template) -runagent.add_command(commands.upload) -runagent.add_command(commands.start) -runagent.add_command(commands.deploy) -runagent.add_command(commands.serve) -runagent.add_command(commands.run) -runagent.add_command(commands.run_stream) -runagent.add_command(commands.delete) -runagent.add_command(commands.db) -runagent.add_command(commands.local_sync) + # Show logo when no subcommand is provided + if ctx.invoked_subcommand is None: + print_logo(show_tagline=True, brand_color="cyan") + click.echo(ctx.get_help()) + +runagent.add_command(setup_cmd) +runagent.add_command(config_cmd) +runagent.add_command(teardown_cmd) +runagent.add_command(init_cmd) +runagent.add_command(upload_cmd) +runagent.add_command(start_cmd) +runagent.add_command(deploy_cmd) +runagent.add_command(serve_cmd) +runagent.add_command(run_cmd) +runagent.add_command(run_stream_cmd) +runagent.add_command(delete_cmd) +runagent.add_command(db_cmd) if __name__ == "__main__": runagent() \ No newline at end of file diff --git a/runagent/client/client.py b/runagent/client/client.py index f0c1341..561dc66 100644 --- a/runagent/client/client.py +++ b/runagent/client/client.py @@ -56,6 +56,7 @@ def run(self, *input_args, **input_kwargs): response = self.rest_client.run_agent( self.agent_id, self.entrypoint_tag, input_args=input_args, input_kwargs=input_kwargs ) + # print(f"response#######################################: {response}") if response.get("success"): # Handle new response format with nested data if "data" in response and "result_data" in response["data"]: diff --git a/runagent/constants.py b/runagent/constants.py index cc3ce1b..c85d5ee 100644 --- a/runagent/constants.py +++ b/runagent/constants.py @@ -16,16 +16,16 @@ # Environment Variables ENV_RUNAGENT_API_KEY = "RUNAGENT_API_KEY" -# UPDATED: Change default port to match your middleware (8333) -ENV_RUNAGENT_BASE_URL = "http://20.84.81.110:8333/" +ENV_RUNAGENT_BASE_URL = "RUNAGENT_BASE_URL" ENV_LOCAL_CACHE_DIRECTORY = "RUNAGENT_CACHE_DIR" ENV_RUNAGENT_LOGGING_LEVEL = "RUNAGENT_LOGGING_LEVEL" # Local Configuration LOCAL_CACHE_DIRECTORY_PATH = "~/.runagent" -USER_DATA_FILE_NAME = "user_data.json" -DEFAULT_BASE_URL = "http://20.84.81.110:8333/" +DEFAULT_BASE_URL = "https://runagent-middleware-v2.onrender.com/" AGENT_CONFIG_FILE_NAME = "runagent.config.json" +DATABASE_FILE_NAME = "runagent_local.db" +DEFAULT_TIMEOUT_SECONDS = 300 # 5 minutes default timeout # Rest of the file remains the same... _cache_dir = os.environ.get(ENV_LOCAL_CACHE_DIRECTORY) diff --git a/runagent/sdk/config.py b/runagent/sdk/config.py index ff4e51e..c514278 100644 --- a/runagent/sdk/config.py +++ b/runagent/sdk/config.py @@ -12,7 +12,6 @@ ENV_RUNAGENT_API_KEY, ENV_RUNAGENT_BASE_URL, LOCAL_CACHE_DIRECTORY, - USER_DATA_FILE_NAME, ) from .exceptions import AuthenticationError, ValidationError @@ -48,22 +47,43 @@ def __init__( self._config["base_url"] = base_url def _get_default_config_path(self) -> Path: - """Get default config file path""" + """Get default config file path (legacy - for migration only)""" config_dir = Path(LOCAL_CACHE_DIRECTORY) config_dir.mkdir(exist_ok=True) - return config_dir / USER_DATA_FILE_NAME + return config_dir / "user_data.json" # Legacy file def _load_config(self) -> t.Dict[str, t.Any]: - """Load configuration from all sources""" + """Load configuration from database and environment variables""" config = {} - # 1. Load from config file - if self.config_file.exists(): - try: - with open(self.config_file, "r") as f: - config.update(json.load(f)) - except (json.JSONDecodeError, IOError): - pass + # 1. Load from database + try: + from .db import DBService + db_service = DBService() + db_config = db_service.get_all_user_metadata() + + if db_config: + config.update(db_config) + elif self.config_file.exists(): + # One-time migration from JSON file if database is empty + try: + with open(self.config_file, "r") as f: + json_config = json.load(f) + config.update(json_config) + + # Migrate to database + if json_config: + for key, value in json_config.items(): + db_service.set_user_metadata(key, value) + + # Backup old file + backup_file = self.config_file.with_suffix('.json.backup') + self.config_file.rename(backup_file) + except (json.JSONDecodeError, IOError): + pass + except Exception: + # If database fails, just continue with empty config + pass # 2. Override with environment variables if os.getenv(ENV_RUNAGENT_API_KEY): @@ -77,13 +97,18 @@ def _load_config(self) -> t.Dict[str, t.Any]: return config def save_config(self) -> bool: - """Save current configuration to file""" + """Save current configuration to database""" try: - self.config_file.parent.mkdir(exist_ok=True) - with open(self.config_file, "w") as f: - json.dump(self._config, f, indent=2) - return True - except (IOError, OSError): + from .db import DBService + db_service = DBService() + + success = True + for key, value in self._config.items(): + if not db_service.set_user_metadata(key, value): + success = False + + return success + except Exception: return False def setup( @@ -146,7 +171,11 @@ def _test_authentication(self) -> t.Dict[str, t.Any]: # Test connection using the token validation endpoint api_key = self._config.get("api_key") - response = client.http.post(f"/tokens/validate?token={api_key}", timeout=10) + response = client.http.post( + f"/tokens/validate?token={api_key}", + data={}, # Send empty body (required by endpoint) + timeout=10 + ) if response.status_code == 200: token_data = response.json() @@ -158,7 +187,9 @@ def _test_authentication(self) -> t.Dict[str, t.Any]: user_info = { "email": data.get("user_email"), "user_id": data.get("user_id"), - "tier": data.get("user_tier", "Free") + "tier": data.get("user_tier", "Free"), + "active_project_name": data.get("default_project_name"), + "active_project_id": data.get("default_project_id"), } # Store user info for later display @@ -166,6 +197,8 @@ def _test_authentication(self) -> t.Dict[str, t.Any]: "user_email": user_info["email"], "user_id": user_info["user_id"], "user_tier": user_info["tier"], + "active_project_id": user_info["active_project_id"], + "active_project_name": user_info["active_project_name"], "auth_validated": True }) diff --git a/runagent/sdk/db.py b/runagent/sdk/db.py index be291b9..06ebc8f 100644 --- a/runagent/sdk/db.py +++ b/runagent/sdk/db.py @@ -24,7 +24,7 @@ from sqlalchemy.orm import relationship, sessionmaker from sqlalchemy.sql import func -from runagent.constants import LOCAL_CACHE_DIRECTORY +from runagent.constants import LOCAL_CACHE_DIRECTORY, DATABASE_FILE_NAME from runagent.utils.port import PortManager @@ -164,6 +164,22 @@ class AgentLog(Base): Index("idx_agent_logs_level", "log_level"), ) + +class UserMetadata(Base): + """User metadata model for storing user configuration as key-value pairs""" + + __tablename__ = "user_metadata" + + key = Column(String, primary_key=True) + value = Column(Text, nullable=False) # Store JSON-serialized values + created_at = Column(DateTime, default=func.current_timestamp()) + updated_at = Column( + DateTime, default=func.current_timestamp(), onupdate=func.current_timestamp() + ) + + # Indexes + __table_args__ = (Index("idx_user_metadata_key", "key"),) + class DBManager: """Low-level database manager for SQLAlchemy operations""" @@ -175,7 +191,7 @@ def __init__(self, db_path: Path = None): db_path: Path to the SQLite database file """ if db_path is None: - db_path = Path(LOCAL_CACHE_DIRECTORY) / "runagent_local.db" + db_path = Path(LOCAL_CACHE_DIRECTORY) / DATABASE_FILE_NAME self.db_path = db_path self.engine = None @@ -2037,3 +2053,149 @@ def cleanup_old_logs(self, days_old: int = 7) -> int: session.rollback() console.print(f"Error cleaning up old logs: {e}") return 0 + + # User Metadata Methods + def set_user_metadata(self, key: str, value: Any) -> bool: + """ + Set user metadata key-value pair + + Args: + key: Metadata key (e.g., 'api_key', 'base_url', 'user_email') + value: Metadata value (will be JSON-serialized) + + Returns: + True if successful, False otherwise + """ + with self.db_manager.get_session() as session: + try: + # Serialize value to JSON + value_json = json.dumps(value) + + # Check if key exists + metadata = session.query(UserMetadata).filter( + UserMetadata.key == key + ).first() + + if metadata: + # Update existing + metadata.value = value_json + metadata.updated_at = func.current_timestamp() + else: + # Create new + metadata = UserMetadata( + key=key, + value=value_json + ) + session.add(metadata) + + session.commit() + return True + except Exception as e: + session.rollback() + if os.getenv('DISABLE_TRY_CATCH'): + raise + console.print(f"Error setting user metadata: {e}") + return False + + def get_user_metadata(self, key: str, default: Any = None) -> Any: + """ + Get user metadata value by key + + Args: + key: Metadata key + default: Default value if key doesn't exist + + Returns: + Deserialized metadata value or default + """ + with self.db_manager.get_session() as session: + try: + metadata = session.query(UserMetadata).filter( + UserMetadata.key == key + ).first() + + if not metadata: + return default + + # Deserialize JSON value + return json.loads(metadata.value) + except Exception as e: + if os.getenv('DISABLE_TRY_CATCH'): + raise + console.print(f"Error getting user metadata: {e}") + return default + + def get_all_user_metadata(self) -> Dict[str, Any]: + """ + Get all user metadata as a dictionary + + Returns: + Dictionary with all metadata key-value pairs + """ + with self.db_manager.get_session() as session: + try: + metadata_records = session.query(UserMetadata).all() + + result = {} + for record in metadata_records: + try: + result[record.key] = json.loads(record.value) + except json.JSONDecodeError: + # If JSON parsing fails, store as string + result[record.key] = record.value + + return result + except Exception as e: + if os.getenv('DISABLE_TRY_CATCH'): + raise + console.print(f"Error getting all user metadata: {e}") + return {} + + def delete_user_metadata(self, key: str) -> bool: + """ + Delete user metadata by key + + Args: + key: Metadata key to delete + + Returns: + True if successful, False otherwise + """ + with self.db_manager.get_session() as session: + try: + metadata = session.query(UserMetadata).filter( + UserMetadata.key == key + ).first() + + if not metadata: + return False + + session.delete(metadata) + session.commit() + return True + except Exception as e: + session.rollback() + if os.getenv('DISABLE_TRY_CATCH'): + raise + console.print(f"Error deleting user metadata: {e}") + return False + + def clear_all_user_metadata(self) -> bool: + """ + Clear all user metadata + + Returns: + True if successful, False otherwise + """ + with self.db_manager.get_session() as session: + try: + session.query(UserMetadata).delete() + session.commit() + console.print("🧹 [green]Cleared all user metadata[/green]") + return True + except Exception as e: + session.rollback() + if os.getenv('DISABLE_TRY_CATCH'): + raise + console.print(f"Error clearing user metadata: {e}") + return False diff --git a/runagent/sdk/rest_client.py b/runagent/sdk/rest_client.py index 8b9a335..594364d 100644 --- a/runagent/sdk/rest_client.py +++ b/runagent/sdk/rest_client.py @@ -20,6 +20,7 @@ ) from runagent.utils.config import Config +from runagent.constants import DEFAULT_TIMEOUT_SECONDS from runagent.utils.agent_id import ( generate_agent_id, generate_agent_fingerprint, @@ -160,8 +161,8 @@ def _request( method=method.upper(), url=url, params=params, - json=data if data and not files else None, - data=None if data and not files else data, + json=data if data is not None and not files else None, + data=None if data is not None and not files else data, headers=request_headers if request_headers else None, files=files, timeout=timeout, @@ -825,7 +826,8 @@ def _start_agent_core(self, agent_id: str, config: Dict = None) -> Dict: payload = config or {} try: - response = self.http.post(f"/agents/{agent_id}/start", data=payload, timeout=60) + # Increased timeout to 5 minutes to allow for background processing + response = self.http.post(f"/agents/{agent_id}/start", data=payload, timeout=300) result = response.json() return self._process_start_result(result, agent_id) @@ -1137,7 +1139,7 @@ def run_agent( entrypoint_tag: str, input_args: list = None, input_kwargs: dict = None, - timeout_seconds: int = 60, + timeout_seconds: int = DEFAULT_TIMEOUT_SECONDS, async_execution: bool = False, ) -> Dict: """Execute an agent with given parameters""" diff --git a/runagent/sdk/socket_client.py b/runagent/sdk/socket_client.py index eadf2b8..8e3b149 100644 --- a/runagent/sdk/socket_client.py +++ b/runagent/sdk/socket_client.py @@ -10,7 +10,10 @@ class SocketClient: - """WebSocket client for agent streaming with both async and sync support""" + """WebSocket client for agent streaming with both async and sync support + + FIXED: Now properly handles cloud deployments with correct WSS URLs + """ def __init__( self, @@ -19,32 +22,75 @@ def __init__( api_prefix: Optional[str] = "/api/v1", is_local: Optional[bool] = True ): - if not base_socket_url: - base_url = Config.get_base_url() - base_url = base_url.lstrip("http://").lstrip("https://") - base_socket_url = f"ws://{base_url}" - self.is_local = is_local - self.base_socket_url = base_socket_url.rstrip("/") + api_prefix self.api_key = api_key or Config.get_api_key() self.serializer = CoreSerializer() + # FIXED: Handle cloud vs local URL construction + if base_socket_url: + # Use provided URL + self.base_socket_url = base_socket_url.rstrip("/") + api_prefix + else: + if is_local: + # Local: Use localhost + base_url = "ws://127.0.0.1:8450" + self.base_socket_url = base_url + api_prefix + else: + # Cloud: Convert HTTP(S) base URL to WS(S) + base_url = Config.get_base_url() + + # Convert https:// to wss:// or http:// to ws:// + if base_url.startswith("https://"): + ws_base = base_url.replace("https://", "wss://") + elif base_url.startswith("http://"): + ws_base = base_url.replace("http://", "ws://") + else: + # No protocol, assume secure for cloud + ws_base = f"wss://{base_url}" + + self.base_socket_url = ws_base.rstrip("/") + api_prefix + + print(f"[DEBUG] SocketClient initialized:") + print(f" - is_local: {self.is_local}") + print(f" - base_socket_url: {self.base_socket_url}") + print(f" - api_key: {'SET' if self.api_key else 'NOT SET'}") + async def run_stream_async(self, agent_id: str, entrypoint_tag: str, *input_args, **input_kwargs) -> AsyncIterator[Any]: """Stream agent execution results (async version)""" - uri = f"{self.base_socket_url}/agents/{agent_id}/run-stream?token={self.api_key}" - # if not self.is_local: - # uri = f"{uri}?token={self.api_key}" + # FIXED: Build proper cloud URL with query param auth + if self.is_local: + uri = f"{self.base_socket_url}/agents/{agent_id}/run-stream" + else: + # Cloud: Add token as query parameter + uri = f"{self.base_socket_url}/agents/{agent_id}/run-stream?token={self.api_key}" + + # print(f"[DEBUG] Connecting to: {uri}") + + # FIXED: Add proper headers for cloud authentication + extra_headers = {} + if not self.is_local and self.api_key: + extra_headers["Authorization"] = f"Bearer {self.api_key}" - async with websockets.connect(uri) as websocket: + async with websockets.connect( + uri, + extra_headers=extra_headers if extra_headers else None, + ping_interval=20, + ping_timeout=60, + close_timeout=10, + max_size=10 * 1024 * 1024 + ) as websocket: # Send start stream request in the exact format required request_data = { "entrypoint_tag": entrypoint_tag, - "input_args": input_args, - "input_kwargs": input_kwargs, - "timeout_seconds": 60, + "input_args": list(input_args), # Ensure JSON serializable + "input_kwargs": dict(input_kwargs), # Ensure JSON serializable + "timeout_seconds": 600, "async_execution": False } + + print(f"[DEBUG] Sending request: {request_data}") + # Send the request as direct JSON await websocket.send(json.dumps(request_data)) @@ -53,18 +99,22 @@ async def run_stream_async(self, agent_id: str, entrypoint_tag: str, *input_args try: message = json.loads(raw_message) except json.JSONDecodeError: - continue # Skip invalid messages + print(f"[WARN] Invalid JSON message: {raw_message}") + continue message_type = message.get("type") if message_type == "error": - raise Exception(f"Stream error: {message.get('error')}") + error_msg = message.get('error') or message.get('detail', 'Unknown error') + raise Exception(f"Stream error: {error_msg}") elif message_type == "status": status = message.get("status") if status == "stream_completed": + print("[DEBUG] Stream completed") break elif status == "stream_started": - continue # Skip status messages + print("[DEBUG] Stream started") + continue elif message_type == "data": # Yield the actual chunk data yield message.get("content") @@ -73,19 +123,42 @@ def run_stream(self, agent_id: str, entrypoint_tag: str, input_args, input_kwarg """Stream agent execution results (sync version)""" from websockets.sync.client import connect - uri = f"{self.base_socket_url}/agents/{agent_id}/run-stream?token={self.api_key}" + # FIXED: Build proper cloud URL with query param auth + if self.is_local: + uri = f"{self.base_socket_url}/agents/{agent_id}/run-stream" + else: + # Cloud: Add token as query parameter + uri = f"{self.base_socket_url}/agents/{agent_id}/run-stream?token={self.api_key}" + + # print(f"[DEBUG] Connecting to: {uri}") - with connect(uri) as websocket: + # FIXED: Add proper headers for cloud authentication + extra_headers = {} + if not self.is_local and self.api_key: + extra_headers["Authorization"] = f"Bearer {self.api_key}" + + # Add proper timeout and keepalive settings + with connect( + uri, + additional_headers=extra_headers if extra_headers else None, + ping_interval=20, # Send ping every 20 seconds + ping_timeout=60, # Wait up to 60 seconds for pong + close_timeout=10, # Timeout for closing handshake + max_size=10 * 1024 * 1024, # 10MB max message size + open_timeout=30 # FIXED: Add connection timeout + ) as websocket: # Send start stream request in the exact format required request_data = { "entrypoint_tag": entrypoint_tag, - "input_args": input_args, - "input_kwargs": input_kwargs, - "timeout_seconds": 60, + "input_args": list(input_args) if input_args else [], + "input_kwargs": dict(input_kwargs) if input_kwargs else {}, + "timeout_seconds": 600, "async_execution": False } + print(f"[DEBUG] Sending request: {request_data}") + # Send the request as direct JSON websocket.send(json.dumps(request_data)) @@ -94,18 +167,22 @@ def run_stream(self, agent_id: str, entrypoint_tag: str, input_args, input_kwarg try: message = json.loads(raw_message) except json.JSONDecodeError: - continue # Skip invalid messages + print(f"[WARN] Invalid JSON message: {raw_message}") + continue message_type = message.get("type") if message_type == "error": - raise Exception(f"Stream error: {message.get('error')}") + error_msg = message.get('error') or message.get('detail', 'Unknown error') + raise Exception(f"Stream error: {error_msg}") elif message_type == "status": status = message.get("status") if status == "stream_completed": + print("[DEBUG] Stream completed") break elif status == "stream_started": - continue # Skip status messages + print("[DEBUG] Stream started") + continue elif message_type == "data": # Yield the actual chunk data - yield message.get("content") + yield message.get("content") \ No newline at end of file diff --git a/runagent/sdk/template_downloader.py b/runagent/sdk/template_downloader.py index 3947ddc..cbed3b0 100644 --- a/runagent/sdk/template_downloader.py +++ b/runagent/sdk/template_downloader.py @@ -5,6 +5,7 @@ import tempfile import typing as t from pathlib import Path +import requests import git from git import Repo @@ -29,7 +30,202 @@ def __init__(self, repo_url: str, branch: str = "main"): ''' self.repo_url = repo_url self.branch = branch - + + # Extract GitHub owner/repo for API usage + self.github_token = os.getenv("GITHUB_TOKEN") + if "github.com" in self.repo_url: + parts = self.repo_url.replace(".git", "").split("/") + self.github_owner = parts[-2] + self.github_repo = parts[-1] + self.use_github_api = True + else: + self.use_github_api = False + + def _github_api_get(self, path: str) -> dict: + """ + Make a GET request to GitHub API + + Args: + path: API path (e.g., "repos/owner/repo/contents/path") + + Returns: + JSON response + """ + url = f"https://api.github.com/{path}" + headers = {"Accept": "application/vnd.github.v3+json"} + + if self.github_token: + headers["Authorization"] = f"token {self.github_token}" + + params = {"ref": self.branch} + response = requests.get(url, headers=headers, params=params, timeout=10) + + if response.status_code == 200: + return response.json() + elif response.status_code == 403 and "rate limit" in response.text.lower(): + raise TemplateDownloadError( + "GitHub API rate limit exceeded. Set GITHUB_TOKEN environment variable for higher limits." + ) + else: + raise TemplateDownloadError( + f"GitHub API request failed: {response.status_code} - {response.text}" + ) + + def _download_github_folder_api(self, folder_path: str, local_dir: Path) -> None: + """ + Download a folder from GitHub using API (much faster than git clone) + + Args: + folder_path: Path to folder in repo (e.g., "templates/letta/default") + local_dir: Local directory to save files + """ + api_path = f"repos/{self.github_owner}/{self.github_repo}/contents/{folder_path}" + + try: + contents = self._github_api_get(api_path) + except TemplateDownloadError: + # Fall back to git clone if API fails + raise + + # Create local directory + local_dir.mkdir(parents=True, exist_ok=True) + + for item in contents: + if item['type'] == 'file': + # Download file directly + file_response = requests.get(item['download_url'], timeout=30) + + if file_response.status_code == 200: + local_file_path = local_dir / item['name'] + local_file_path.write_bytes(file_response.content) + else: + raise TemplateDownloadError(f"Failed to download file: {item['name']}") + + elif item['type'] == 'dir': + # Recursively download subdirectory + sub_folder = local_dir / item['name'] + self._download_github_folder_api(item['path'], sub_folder) + + def _list_github_folder_api(self, folder_path: str) -> list: + """ + List contents of a folder on GitHub using API (instant vs cloning) + + Args: + folder_path: Path to folder in repo + + Returns: + List of items in the folder + """ + api_path = f"repos/{self.github_owner}/{self.github_repo}/contents/{folder_path}" + return self._github_api_get(api_path) + + def _list_templates_via_api(self, prepath: str, framework_filter: str = None, debug_enabled: bool = False) -> t.Dict[str, t.List[str]]: + """ + List templates using GitHub API (much faster than git clone!) + + Args: + prepath: Pre-path before framework directory + framework_filter: Optional specific framework to scan + debug_enabled: Whether to log debug information + + Returns: + Dictionary mapping framework names to list of template names + """ + import time + import logging + logger = logging.getLogger(__name__) + + start_time = time.time() + if debug_enabled: + logger.info(f"[PERF] Using GitHub API to list templates") + + templates = {} + + # If framework filter specified, only scan that framework + if framework_filter: + framework_path = f"{prepath}/{framework_filter}" if prepath else framework_filter + try: + if debug_enabled: + logger.info(f"[PERF] Fetching templates for {framework_filter} via API") + + items = self._list_github_folder_api(framework_path) + templates[framework_filter] = [] + + for item in items: + if item['type'] == 'dir' and not item['name'].startswith('.'): + # Check if it has a config file (validate it's a template) + template_name = item['name'] + try: + config_check_path = f"{framework_path}/{template_name}" + template_contents = self._list_github_folder_api(config_check_path) + + # Check if runagent.config.json exists + has_config = any( + f['name'] in ['runagent.config.json', 'runagent.config.yaml', 'runagent.config.yml'] + for f in template_contents + ) + + if has_config: + templates[framework_filter].append(template_name) + if debug_enabled: + logger.debug(f"[PERF] ✓ {template_name} valid") + except Exception as e: + if debug_enabled: + logger.debug(f"[PERF] ✗ {template_name} skipped: {e}") + + api_time = time.time() - start_time + if debug_enabled: + logger.info(f"[PERF] API listing completed in {api_time:.2f}s - found {len(templates[framework_filter])} templates") + + return templates + + except Exception as e: + raise TemplateDownloadError(f"Failed to list templates via API: {e}") + + # Scan all frameworks + try: + if debug_enabled: + logger.info(f"[PERF] Fetching all frameworks via API") + + framework_items = self._list_github_folder_api(prepath if prepath else "") + + for framework_item in framework_items: + if framework_item['type'] == 'dir' and not framework_item['name'].startswith('.'): + framework_name = framework_item['name'] + templates[framework_name] = [] + + # List templates in this framework + try: + framework_path = f"{prepath}/{framework_name}" if prepath else framework_name + template_items = self._list_github_folder_api(framework_path) + + for template_item in template_items: + if template_item['type'] == 'dir' and not template_item['name'].startswith('.'): + template_name = template_item['name'] + try: + # Quick validation: check if config exists + template_contents = self._list_github_folder_api(f"{framework_path}/{template_name}") + has_config = any( + f['name'] in ['runagent.config.json', 'runagent.config.yaml', 'runagent.config.yml'] + for f in template_contents + ) + + if has_config: + templates[framework_name].append(template_name) + except Exception: + pass # Skip invalid templates + except Exception: + pass # Skip frameworks with errors + + api_time = time.time() - start_time + if debug_enabled: + logger.info(f"[PERF] API listing completed in {api_time:.2f}s") + + return templates + + except Exception as e: + raise TemplateDownloadError(f"Failed to list all templates via API: {e}") + def download_template( self, prepath: str, framework: str, template: str, target_folder: str ) -> None: @@ -51,7 +247,18 @@ def download_template( target_dir = Path(target_folder) target_dir.mkdir(parents=True, exist_ok=True) - # Use temporary directory for sparse checkout + # Use GitHub API if available (much faster!) + if self.use_github_api: + try: + self._download_github_folder_api(template_path, target_dir) + return + except Exception as e: + # Fall back to git clone if API fails + import logging + logger = logging.getLogger(__name__) + logger.warning(f"GitHub API download failed, falling back to git clone: {e}") + + # Fallback: Use git clone with sparse checkout with tempfile.TemporaryDirectory() as temp_dir: temp_path = Path(temp_dir) @@ -133,12 +340,52 @@ def _copy_directory_contents(self, source_dir: Path, target_dir: Path) -> None: # Copy file shutil.copy2(item, target_item) - def list_available_templates(self, prepath: str) -> t.Dict[str, t.List[str]]: + def _scan_framework_templates(self, framework_dir: Path, template_list: list, debug_enabled: bool = False): + """ + Helper to scan templates in a framework directory. + + Args: + framework_dir: Path to framework directory + template_list: List to append valid template names to + debug_enabled: Whether to log debug information + """ + import time + import logging + logger = logging.getLogger(__name__) + + for template_dir in framework_dir.iterdir(): + if template_dir.is_dir() and not template_dir.name.startswith("."): + # Verify this is a valid template + # Skip templates that fail validation (e.g., test directories without config) + template_name = template_dir.name + try: + validate_start = time.time() + if debug_enabled: + logger.debug(f"[PERF] Validating template: {template_name}") + is_valid, _ = validate_agent(template_dir) + validate_time = time.time() - validate_start + + if is_valid: + if debug_enabled: + logger.debug(f"[PERF] ✓ {template_name} valid ({validate_time:.3f}s)") + template_list.append(template_name) + else: + if debug_enabled: + logger.debug(f"[PERF] ✗ {template_name} invalid ({validate_time:.3f}s)") + except Exception as e: + if debug_enabled: + validate_time = time.time() - validate_start + logger.debug(f"[PERF] ✗ {template_name} error ({validate_time:.3f}s): {e}") + # Skip invalid templates silently (e.g., test dirs, incomplete templates) + pass + + def list_available_templates(self, prepath: str, framework_filter: str = None) -> t.Dict[str, t.List[str]]: """ List all available templates in the repository Args: prepath: Pre-path before framework directory + framework_filter: Optional specific framework to scan (much faster) Returns: Dictionary mapping framework names to list of template names @@ -146,17 +393,45 @@ def list_available_templates(self, prepath: str) -> t.Dict[str, t.List[str]]: Raises: TemplateDownloadError: If listing fails """ + import time + import logging + import os + logger = logging.getLogger(__name__) + + # Only show debug info if explicitly enabled + debug_enabled = os.getenv('RUNAGENT_DEBUG') == '1' + + # Use GitHub API if available (much faster!) + if self.use_github_api: + try: + return self._list_templates_via_api(prepath, framework_filter, debug_enabled) + except TemplateDownloadError as e: + if debug_enabled: + logger.warning(f"[PERF] GitHub API failed, falling back to git clone: {e}") + # Fall through to git clone method + + # Fallback: Use git clone method with tempfile.TemporaryDirectory(dir="/tmp") as temp_dir: temp_path = Path(temp_dir) try: # Shallow clone for listing + start_time = time.time() + if debug_enabled: + logger.info(f"[PERF] Starting git clone from {self.repo_url}") + repo = Repo.clone_from( self.repo_url, temp_path, branch=self.branch, depth=1 ) + + clone_time = time.time() - start_time + if debug_enabled: + logger.info(f"[PERF] Git clone completed in {clone_time:.2f}s") + templates = {} # Navigate to the prepath directory + scan_start = time.time() prepath_dir = temp_path / prepath if prepath else temp_path if not prepath_dir.exists(): @@ -164,29 +439,42 @@ def list_available_templates(self, prepath: str) -> t.Dict[str, t.List[str]]: f"Pre-path '{prepath}' not found in repository branch '{self.branch}'" ) - # Scan for framework directories + # If framework filter specified, only scan that framework's directory + if framework_filter: + if debug_enabled: + logger.info(f"[PERF] Scanning framework: {framework_filter}") + framework_dir = prepath_dir / framework_filter + if framework_dir.exists() and framework_dir.is_dir(): + templates[framework_filter] = [] + fw_start = time.time() + self._scan_framework_templates(framework_dir, templates[framework_filter], debug_enabled) + fw_time = time.time() - fw_start + if debug_enabled: + logger.info(f"[PERF] Scanned {framework_filter} in {fw_time:.2f}s - found {len(templates[framework_filter])} templates") + return templates + + # Scan all framework directories for framework_dir in prepath_dir.iterdir(): - if framework_dir.is_dir() and not framework_dir.name.startswith( - "." - ): + if framework_dir.is_dir() and not framework_dir.name.startswith("."): framework_name = framework_dir.name + if debug_enabled: + logger.info(f"[PERF] Scanning framework: {framework_name}") templates[framework_name] = [] - - # Scan for template directories - for template_dir in framework_dir.iterdir(): - if ( - template_dir.is_dir() - and not template_dir.name.startswith(".") - ): - # Verify this is a valid template (has main.py) - if validate_agent(template_dir): - templates[framework_name].append(template_dir.name) - + fw_start = time.time() + self._scan_framework_templates(framework_dir, templates[framework_name], debug_enabled) + fw_time = time.time() - fw_start + if debug_enabled: + logger.info(f"[PERF] Scanned {framework_name} in {fw_time:.2f}s - found {len(templates[framework_name])} templates") + + scan_time = time.time() - scan_start + if debug_enabled: + logger.info(f"[PERF] Total scanning time: {scan_time:.2f}s") return templates except git.exc.GitCommandError as e: if os.getenv('DISABLE_TRY_CATCH'): raise + raise TemplateDownloadError(f"Git error while listing templates: {e}") except Exception as e: if os.getenv('DISABLE_TRY_CATCH'): diff --git a/runagent/sdk/template_manager.py b/runagent/sdk/template_manager.py index 07d8241..fae8960 100644 --- a/runagent/sdk/template_manager.py +++ b/runagent/sdk/template_manager.py @@ -58,13 +58,11 @@ def list_available( Dictionary mapping framework names to template lists """ try: - templates = self.downloader.list_available_templates(TEMPLATE_PREPATH) - - if framework_filter: - if framework_filter in templates: - return {framework_filter: templates[framework_filter]} - else: - return {} + # Pass framework_filter to downloader for faster scanning + templates = self.downloader.list_available_templates( + TEMPLATE_PREPATH, + framework_filter=framework_filter + ) return templates except Exception as e: @@ -111,8 +109,8 @@ def init_template( ValidationError: If template is invalid FileExistsError: If folder exists and overwrite is False """ - # Validate template exists - available_templates = self.list_available() + # Validate template exists - only fetch for this specific framework + available_templates = self.list_available(framework_filter=framework) if framework not in available_templates: raise ValidationError( diff --git a/runagent/utils/config.py b/runagent/utils/config.py index 7ebb41b..192bdc9 100644 --- a/runagent/utils/config.py +++ b/runagent/utils/config.py @@ -10,7 +10,6 @@ ENV_RUNAGENT_API_KEY, ENV_RUNAGENT_BASE_URL, LOCAL_CACHE_DIRECTORY, - USER_DATA_FILE_NAME, ) @@ -69,27 +68,54 @@ def get_config(project_dir: str) -> t.Optional[t.Dict[str, t.Any]]: @staticmethod def get_user_config() -> t.Dict[str, t.Any]: """ - Get user configuration from {ENV_LOCAL_CACHE_DIRECTORY}/config.json + Get user configuration from database (user_metadata table) Returns: User configuration content """ - config_dir = Path.home() / LOCAL_CACHE_DIRECTORY - config_file = config_dir / USER_DATA_FILE_NAME - - if not config_file.exists(): - return {} - try: - with config_file.open("r") as f: - return json.load(f) + from runagent.sdk.db import DBService + db_service = DBService() + + # Get from database + metadata = db_service.get_all_user_metadata() + + # One-time migration: Check for old JSON file only if database is empty + if not metadata: + config_dir = Path.home() / LOCAL_CACHE_DIRECTORY + config_file = config_dir / "user_data.json" # Legacy file + + if config_file.exists(): + try: + with config_file.open("r") as f: + json_config = json.load(f) + + # Migrate to database + if json_config: + for key, value in json_config.items(): + db_service.set_user_metadata(key, value) + + # Backup and remove old JSON file + backup_file = config_file.with_suffix('.json.backup') + config_file.rename(backup_file) + + return json_config + except Exception: + if os.getenv('DISABLE_TRY_CATCH'): + raise + pass + + return metadata or {} except Exception: + if os.getenv('DISABLE_TRY_CATCH'): + raise + # If database fails, return empty (no fallback) return {} @staticmethod def set_user_config(key: str, value: t.Any) -> bool: """ - Set a value in the user configuration + Set a value in the user configuration (database-backed) Args: key: Configuration key @@ -98,31 +124,10 @@ def set_user_config(key: str, value: t.Any) -> bool: Returns: True if successful, False otherwise """ - config_dir = Path.home() / LOCAL_CACHE_DIRECTORY - config_dir.mkdir(exist_ok=True) - - config_file = config_dir / USER_DATA_FILE_NAME - - # Get existing config or create new - if config_file.exists(): - try: - with config_file.open("r") as f: - config = json.load(f) - except Exception: - if os.getenv('DISABLE_TRY_CATCH'): - raise - config = {} - else: - config = {} - - # Update config - config[key] = value - - # Write config try: - with config_file.open("w") as f: - json.dump(config, f, indent=2) - return True + from runagent.sdk.db import DBService + db_service = DBService() + return db_service.set_user_metadata(key, value) except Exception: if os.getenv('DISABLE_TRY_CATCH'): raise @@ -235,23 +240,18 @@ def get_deployment_info(agent_id: str) -> t.Optional[t.Dict[str, t.Any]]: with info_file.open("r") as f: return json.load(f) - # Add these methods to your Config class - @staticmethod def clear_user_config() -> bool: """ - Clear all user configuration + Clear all user configuration from database Returns: True if successful, False otherwise """ - config_dir = Path.home() / LOCAL_CACHE_DIRECTORY - config_file = config_dir / USER_DATA_FILE_NAME - try: - if config_file.exists(): - config_file.unlink() - return True + from runagent.sdk.db import DBService + db_service = DBService() + return db_service.clear_all_user_metadata() except Exception: if os.getenv('DISABLE_TRY_CATCH'): raise @@ -283,9 +283,6 @@ def get_config_status() -> t.Dict[str, t.Any]: "base_url": Config.get_base_url(), "user_email": config.get("email"), "user_name": config.get("name"), - "config_file_exists": ( - Path.home() / LOCAL_CACHE_DIRECTORY / USER_DATA_FILE_NAME - ).exists(), } @staticmethod diff --git a/templates/agno/default/runagent.config.json b/templates/agno/default/runagent.config.json index bfe717e..f9a9166 100644 --- a/templates/agno/default/runagent.config.json +++ b/templates/agno/default/runagent.config.json @@ -1,5 +1,5 @@ { - "agent_name": "Demo Agent", + "agent_name": "A Rust backend agent for Agno", "description": "A simple placeholder agent", "framework": "agno", "template": "default", diff --git a/templates/agno/default/simple_assistant.py b/templates/agno/default/simple_assistant.py index 6052041..1172b56 100644 --- a/templates/agno/default/simple_assistant.py +++ b/templates/agno/default/simple_assistant.py @@ -4,7 +4,7 @@ agent = Agent( model=OpenAIChat( - id="gpt-4o" + id="gpt-4o-mini" ), markdown=True ) @@ -18,9 +18,7 @@ def agent_print_response(prompt: str): response = agent.run(prompt) # Return structured data that can be serialized - return { - "content": response.content if hasattr(response, 'content') else str(response), - } + return response def agent_print_response_stream(prompt: str): """Streaming response that yields serializable chunks""" diff --git a/test_scripts/python/client_test_agno.py b/test_scripts/python/client_test_agno.py index 3a409af..0c8b9d9 100644 --- a/test_scripts/python/client_test_agno.py +++ b/test_scripts/python/client_test_agno.py @@ -1,9 +1,9 @@ # from runagent import RunAgentClient # ra = RunAgentClient( -# agent_id="27f68f00-e8cd-4965-9b91-fac501e132e3", +# agent_id="71b31b58-c2d6-49ab-b564-d72b1a449df7", # entrypoint_tag="agno_print_response", -# local=True +# local=False # ) @@ -17,9 +17,9 @@ from runagent import RunAgentClient ra = RunAgentClient( - agent_id="27f68f00-e8cd-4965-9b91-fac501e132e3", + agent_id="c12d3486-cbf6-4d3a-b58b-e5a9c0dfe311", entrypoint_tag="agno_print_response_stream", - local=True + local=False ) for chunk in ra.run( diff --git a/test_scripts/rust/test_agno/Cargo.toml b/test_scripts/rust/test_agno/Cargo.toml index d7d2e8a..5cf216a 100644 --- a/test_scripts/rust/test_agno/Cargo.toml +++ b/test_scripts/rust/test_agno/Cargo.toml @@ -5,7 +5,7 @@ edition = "2021" [dependencies] # Assuming you're using the runagent Rust SDK -runagent = { path = "/home/riamdriad5/runagent/runagent/runagent-rust/runagent" } +runagent = { path = "/home/azureuser/runagent/runagent-rust/runagent" } # Required dependencies tokio = { version = "1.0", features = ["full"] } diff --git a/test_scripts/rust/test_agno/src/main.rs b/test_scripts/rust/test_agno/src/main.rs index 48b636b..e43e920 100644 --- a/test_scripts/rust/test_agno/src/main.rs +++ b/test_scripts/rust/test_agno/src/main.rs @@ -5,8 +5,7 @@ use serde_json::json; async fn main() -> Result<(), Box> { println!("🧪 Testing agno Agent with Rust SDK"); - // Replace with the actual agent ID from `runagent serve` - let agent_id = "aacf274a-32b8-497d-85a4-aa8597686c40"; + let agent_id = "f9415c35-6c01-4f76-a9d5-ef2e11c08cbb"; // Test: Non-streaming execution println!("\n🚀 Testing Non-Streaming Execution"); @@ -16,7 +15,7 @@ async fn main() -> Result<(), Box> { let client = RunAgentClient::new( agent_id, "agno_print_response", - true, // local = true + false, // local = true // Some("127.0.0.1"), // Some(8452) // Use the port from your server output ).await?; @@ -24,12 +23,12 @@ async fn main() -> Result<(), Box> { // println!("🔗 Connected to agent at 127.0.0.1:8452"); let response = client.run_with_args( - &[json!("Write a report on NVDA")], // positional args + &[json!("Write small paragraph on how i met your mother tv series")], // positional args &[] // no keyword args ).await?; println!("✅ Response received:"); - println!("{}", serde_json::to_string_pretty(&response)?); + println!("{}",(&response)); println!("\n✅ Test completed successfully!"); @@ -40,30 +39,30 @@ async fn main() -> Result<(), Box> { -use runagent::client::RunAgentClient; -use serde_json::json; -use futures::StreamExt; +// use runagent::client::RunAgentClient; +// use serde_json::json; +// use futures::StreamExt; -#[tokio::main] -async fn main() -> Result<(), Box> { - let agent_id = "d31336a7-7c02-43cb-a906-6019b06a1249"; +// #[tokio::main] +// async fn main() -> Result<(), Box> { +// let agent_id = "f9415c35-6c01-4f76-a9d5-ef2e11c08cbb"; - println!("🌊 ag2 Streaming Test"); - let client = RunAgentClient::new(agent_id, "agno_print_response_stream", true).await?; +// println!("🌊 ag2 Streaming Test"); +// let client = RunAgentClient::new(agent_id, "agno_print_response_stream", false).await?; - let mut stream = client.run_stream(&[ - ("prompt", json!("Tell me about solar system")) - ]).await?; +// let mut stream = client.run_stream(&[ +// ("prompt", json!("How many planets are there in the solar system?")) +// ]).await?; - while let Some(chunk_result) = stream.next().await { - match chunk_result { - Ok(chunk) => println!("{}", chunk), - Err(e) => { - println!("Error: {}", e); - break; - } - } - } +// while let Some(chunk_result) = stream.next().await { +// match chunk_result { +// Ok(chunk) => println!("{}", chunk), +// Err(e) => { +// println!("Error: {}", e); +// break; +// } +// } +// } - Ok(()) -} \ No newline at end of file +// Ok(()) +// } \ No newline at end of file