From 04fc91e56133fc09626b04edecfed8735c6ae61d Mon Sep 17 00:00:00 2001 From: 3rd-Son Date: Thu, 17 Apr 2025 11:28:53 +0100 Subject: [PATCH 01/35] implemented mcp --- cortex_on/Dockerfile | 1 + cortex_on/agents/mcp_server.py | 60 ++++ cortex_on/agents/orchestrator_agent.py | 371 ++++++++++++------------- cortex_on/requirements.txt | 1 + 4 files changed, 246 insertions(+), 187 deletions(-) create mode 100644 cortex_on/agents/mcp_server.py diff --git a/cortex_on/Dockerfile b/cortex_on/Dockerfile index 8d7373e..1867fcf 100644 --- a/cortex_on/Dockerfile +++ b/cortex_on/Dockerfile @@ -19,5 +19,6 @@ RUN uv pip install --system --no-cache-dir -r requirements.txt COPY . . EXPOSE 8081 +EXPOSE 3001 CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8081"] \ No newline at end of file diff --git a/cortex_on/agents/mcp_server.py b/cortex_on/agents/mcp_server.py new file mode 100644 index 0000000..7a264dc --- /dev/null +++ b/cortex_on/agents/mcp_server.py @@ -0,0 +1,60 @@ +from mcp.server.fastmcp import FastMCP +from pydantic_ai import Agent +from pydantic_ai.models.anthropic import AnthropicModel +import os +from utils.ant_client import get_client +from agents.planner_agent import planner_agent +from agents.code_agent import coder_agent +from agents.web_surfer import WebSurfer +import logfire + +# Initialize the single MCP server +server = FastMCP("CortexON MCP Server") + + +@server.tool() +async def plan_task(task: str) -> str: + """Planner agent tool for creating task plans""" + try: + logfire.info(f"Planning task: {task}") + planner_response = await planner_agent.run(user_prompt=task) + return planner_response.data.plan + except Exception as e: + logfire.error(f"Error in planner: {str(e)}", exc_info=True) + return f"Error in planner: {str(e)}" + + +@server.tool() +async def code_task(task: str) -> str: + """Coder agent tool for implementing technical solutions""" + try: + logfire.info(f"Executing code task: {task}") + coder_response = await coder_agent.run(user_prompt=task) + return coder_response.data.content + except Exception as e: + logfire.error(f"Error in coder: {str(e)}", exc_info=True) + return f"Error in coder: {str(e)}" + + +@server.tool() +async def web_surf_task(task: str) -> str: + """Web surfer agent tool for web interactions""" + try: + logfire.info(f"Executing web surf task: {task}") + web_surfer = WebSurfer(api_url="http://localhost:8000/api/v1/web/stream") + success, message, _ = await web_surfer.generate_reply( + instruction=task, websocket=None, stream_output=None + ) + return message if success else f"Error in web surfer: {message}" + except Exception as e: + logfire.error(f"Error in web surfer: {str(e)}", exc_info=True) + return f"Error in web surfer: {str(e)}" + + +def run_server(): + """Run the MCP server""" + server.run(port=3001) + + +if __name__ == "__main__": + run_server() diff --git a/cortex_on/agents/orchestrator_agent.py b/cortex_on/agents/orchestrator_agent.py index 04151cf..0b8edd8 100644 --- a/cortex_on/agents/orchestrator_agent.py +++ b/cortex_on/agents/orchestrator_agent.py @@ -10,12 +10,13 @@ from dotenv import load_dotenv from pydantic_ai.models.anthropic import AnthropicModel from pydantic_ai import Agent, RunContext -from agents.web_surfer import WebSurfer +from pydantic_ai.mcp import MCPServerHTTP from utils.stream_response_format import StreamResponse from agents.planner_agent import planner_agent from agents.code_agent import coder_agent, CoderAgentDeps from utils.ant_client import get_client + @dataclass class orchestrator_deps: websocket: Optional[WebSocket] = None @@ -23,6 +24,7 @@ class orchestrator_deps: # Add a collection to track agent-specific streams agent_responses: Optional[List[StreamResponse]] = None + orchestrator_system_prompt = """You are an AI orchestrator that manages a team of agents to solve tasks. You have access to tools for coordinating the agents and managing the task flow. [AGENT CAPABILITIES] @@ -65,205 +67,200 @@ class orchestrator_deps: - Suggest manual alternatives - Block credential access -Basic worflow: +Basic workflow: 1. Receive a task from the user. 2. Plan the task by calling the planner agent through plan task -3. Assign coding tasks to the coder agent through coder task if plan requeires coding +3. Assign coding tasks to the coder agent through coder task if plan requires coding or Assign web surfing tasks to the web surfer agent through web_surfer_task if plan requires web surfing 4. Continue step 3 if required by the plan 5. Return the final result to the user """ +# Initialize single MCP server +server = MCPServerHTTP(url="http://localhost:3001/sse") + model = AnthropicModel( - model_name=os.environ.get("ANTHROPIC_MODEL_NAME"), - anthropic_client=get_client() + model_name=os.environ.get("ANTHROPIC_MODEL_NAME"), anthropic_client=get_client() ) orchestrator_agent = Agent( model=model, name="Orchestrator Agent", system_prompt=orchestrator_system_prompt, - deps_type=orchestrator_deps + deps_type=orchestrator_deps, + mcp_servers=[server], ) -@orchestrator_agent.tool -async def plan_task(ctx: RunContext[orchestrator_deps], task: str) -> str: - """Plans the task and assigns it to the appropriate agents""" - try: - logfire.info(f"Planning task: {task}") - - # Create a new StreamResponse for Planner Agent - planner_stream_output = StreamResponse( - agent_name="Planner Agent", - instructions=task, - steps=[], - output="", - status_code=0 - ) - - # Add to orchestrator's response collection if available - if ctx.deps.agent_responses is not None: - ctx.deps.agent_responses.append(planner_stream_output) - - await _safe_websocket_send(ctx.deps.websocket, planner_stream_output) - - # Update planner stream - planner_stream_output.steps.append("Planning task...") - await _safe_websocket_send(ctx.deps.websocket, planner_stream_output) - - # Run planner agent - planner_response = await planner_agent.run(user_prompt=task) - - # Update planner stream with results - plan_text = planner_response.data.plan - planner_stream_output.steps.append("Task planned successfully") - planner_stream_output.output = plan_text - planner_stream_output.status_code = 200 - await _safe_websocket_send(ctx.deps.websocket, planner_stream_output) - - # Also update orchestrator stream - ctx.deps.stream_output.steps.append("Task planned successfully") - await _safe_websocket_send(ctx.deps.websocket, ctx.deps.stream_output) - - return f"Task planned successfully\nTask: {plan_text}" - except Exception as e: - error_msg = f"Error planning task: {str(e)}" - logfire.error(error_msg, exc_info=True) - - # Update planner stream with error - if planner_stream_output: - planner_stream_output.steps.append(f"Planning failed: {str(e)}") - planner_stream_output.status_code = 500 - await _safe_websocket_send(ctx.deps.websocket, planner_stream_output) - - # Also update orchestrator stream - if ctx.deps.stream_output: - ctx.deps.stream_output.steps.append(f"Planning failed: {str(e)}") - await _safe_websocket_send(ctx.deps.websocket, ctx.deps.stream_output) - - return f"Failed to plan task: {error_msg}" - -@orchestrator_agent.tool -async def coder_task(ctx: RunContext[orchestrator_deps], task: str) -> str: - """Assigns coding tasks to the coder agent""" - try: - logfire.info(f"Assigning coding task: {task}") - - # Create a new StreamResponse for Coder Agent - coder_stream_output = StreamResponse( - agent_name="Coder Agent", - instructions=task, - steps=[], - output="", - status_code=0 - ) - - # Add to orchestrator's response collection if available - if ctx.deps.agent_responses is not None: - ctx.deps.agent_responses.append(coder_stream_output) - - # Send initial update for Coder Agent - await _safe_websocket_send(ctx.deps.websocket, coder_stream_output) - - # Create deps with the new stream_output - deps_for_coder_agent = CoderAgentDeps( - websocket=ctx.deps.websocket, - stream_output=coder_stream_output - ) - - # Run coder agent - coder_response = await coder_agent.run( - user_prompt=task, - deps=deps_for_coder_agent - ) - - # Extract response data - response_data = coder_response.data.content - - # Update coder_stream_output with coding results - coder_stream_output.output = response_data - coder_stream_output.status_code = 200 - coder_stream_output.steps.append("Coding task completed successfully") - await _safe_websocket_send(ctx.deps.websocket, coder_stream_output) - - return response_data - except Exception as e: - error_msg = f"Error assigning coding task: {str(e)}" - logfire.error(error_msg, exc_info=True) - - # Update coder_stream_output with error - coder_stream_output.steps.append(f"Coding task failed: {str(e)}") - coder_stream_output.status_code = 500 - await _safe_websocket_send(ctx.deps.websocket, coder_stream_output) - - return f"Failed to assign coding task: {error_msg}" - -@orchestrator_agent.tool -async def web_surfer_task(ctx: RunContext[orchestrator_deps], task: str) -> str: - """Assigns web surfing tasks to the web surfer agent""" - try: - logfire.info(f"Assigning web surfing task: {task}") - - # Create a new StreamResponse for WebSurfer - web_surfer_stream_output = StreamResponse( - agent_name="Web Surfer", - instructions=task, - steps=[], - output="", - status_code=0, - live_url=None - ) - - # Add to orchestrator's response collection if available - if ctx.deps.agent_responses is not None: - ctx.deps.agent_responses.append(web_surfer_stream_output) - - await _safe_websocket_send(ctx.deps.websocket, web_surfer_stream_output) - - # Initialize WebSurfer agent - web_surfer_agent = WebSurfer(api_url="http://localhost:8000/api/v1/web/stream") - - # Run WebSurfer with its own stream_output - success, message, messages = await web_surfer_agent.generate_reply( - instruction=task, - websocket=ctx.deps.websocket, - stream_output=web_surfer_stream_output - ) - - # Update WebSurfer's stream_output with final result - if success: - web_surfer_stream_output.steps.append("Web search completed successfully") - web_surfer_stream_output.output = message - web_surfer_stream_output.status_code = 200 - else: - web_surfer_stream_output.steps.append(f"Web search completed with issues: {message[:100]}") - web_surfer_stream_output.status_code = 500 - - await _safe_websocket_send(ctx.deps.websocket, web_surfer_stream_output) - - web_surfer_stream_output.steps.append(f"WebSurfer completed: {'Success' if success else 'Failed'}") - await _safe_websocket_send(ctx.deps.websocket,web_surfer_stream_output) - - return message - except Exception as e: - error_msg = f"Error assigning web surfing task: {str(e)}" - logfire.error(error_msg, exc_info=True) - - # Update WebSurfer's stream_output with error - web_surfer_stream_output.steps.append(f"Web search failed: {str(e)}") - web_surfer_stream_output.status_code = 500 - await _safe_websocket_send(ctx.deps.websocket, web_surfer_stream_output) - return f"Failed to assign web surfing task: {error_msg}" - -# Helper function for sending WebSocket messages -async def _safe_websocket_send(websocket: Optional[WebSocket], message: Any) -> bool: - """Safely send message through websocket with error handling""" - try: - if websocket and websocket.client_state.CONNECTED: - await websocket.send_text(json.dumps(asdict(message))) - logfire.debug("WebSocket message sent (_safe_websocket_send): {message}", message=message) - return True - return False - except Exception as e: - logfire.error(f"WebSocket send failed: {str(e)}") - return False \ No newline at end of file + +# @orchestrator_agent.tool +# async def plan_task(ctx: RunContext[orchestrator_deps], task: str) -> str: +# """Plans the task and assigns it to the appropriate agents""" +# try: +# logfire.info(f"Planning task: {task}") + +# # Create a new StreamResponse for Planner Agent +# planner_stream_output = StreamResponse( +# agent_name="Planner Agent", +# instructions=task, +# steps=[], +# output="", +# status_code=0, +# ) + +# # Add to orchestrator's response collection if available +# if ctx.deps.agent_responses is not None: +# ctx.deps.agent_responses.append(planner_stream_output) + +# await _safe_websocket_send(ctx.deps.websocket, planner_stream_output) + +# # Update planner stream +# planner_stream_output.steps.append("Planning task...") +# await _safe_websocket_send(ctx.deps.websocket, planner_stream_output) + +# # Run planner agent through MCP +# async with orchestrator_agent.run_mcp_servers(): +# result = await orchestrator_agent.run( +# f"Use the plan_task tool to plan: {task}" +# ) +# plan_text = result.output + +# # Update planner stream with results +# planner_stream_output.steps.append("Task planned successfully") +# planner_stream_output.output = plan_text +# planner_stream_output.status_code = 200 +# await _safe_websocket_send(ctx.deps.websocket, planner_stream_output) + +# # Also update orchestrator stream +# ctx.deps.stream_output.steps.append("Task planned successfully") +# await _safe_websocket_send(ctx.deps.websocket, ctx.deps.stream_output) + +# return f"Task planned successfully\nTask: {plan_text}" +# except Exception as e: +# error_msg = f"Error planning task: {str(e)}" +# logfire.error(error_msg, exc_info=True) + +# # Update planner stream with error +# if planner_stream_output: +# planner_stream_output.steps.append(f"Planning failed: {str(e)}") +# planner_stream_output.status_code = 500 +# await _safe_websocket_send(ctx.deps.websocket, planner_stream_output) + +# # Also update orchestrator stream +# if ctx.deps.stream_output: +# ctx.deps.stream_output.steps.append(f"Planning failed: {str(e)}") +# await _safe_websocket_send(ctx.deps.websocket, ctx.deps.stream_output) + +# return f"Failed to plan task: {error_msg}" + + +# @orchestrator_agent.tool +# async def coder_task(ctx: RunContext[orchestrator_deps], task: str) -> str: +# """Assigns coding tasks to the coder agent""" +# try: +# logfire.info(f"Assigning coding task: {task}") + +# # Create a new StreamResponse for Coder Agent +# coder_stream_output = StreamResponse( +# agent_name="Coder Agent", +# instructions=task, +# steps=[], +# output="", +# status_code=0, +# ) + +# # Add to orchestrator's response collection if available +# if ctx.deps.agent_responses is not None: +# ctx.deps.agent_responses.append(coder_stream_output) + +# # Send initial update for Coder Agent +# await _safe_websocket_send(ctx.deps.websocket, coder_stream_output) + +# # Run coder agent through MCP +# async with orchestrator_agent.run_mcp_servers(): +# result = await orchestrator_agent.run( +# f"Use the code_task tool to implement: {task}" +# ) +# response_data = result.output + +# # Update coder_stream_output with coding results +# coder_stream_output.output = response_data +# coder_stream_output.status_code = 200 +# coder_stream_output.steps.append("Coding task completed successfully") +# await _safe_websocket_send(ctx.deps.websocket, coder_stream_output) + +# return response_data +# except Exception as e: +# error_msg = f"Error assigning coding task: {str(e)}" +# logfire.error(error_msg, exc_info=True) + +# # Update coder_stream_output with error +# coder_stream_output.steps.append(f"Coding task failed: {str(e)}") +# coder_stream_output.status_code = 500 +# await _safe_websocket_send(ctx.deps.websocket, coder_stream_output) + +# return f"Failed to assign coding task: {error_msg}" + + +# @orchestrator_agent.tool +# async def web_surfer_task(ctx: RunContext[orchestrator_deps], task: str) -> str: +# """Assigns web surfing tasks to the web surfer agent""" +# try: +# logfire.info(f"Assigning web surfing task: {task}") + +# # Create a new StreamResponse for WebSurfer +# web_surfer_stream_output = StreamResponse( +# agent_name="Web Surfer", +# instructions=task, +# steps=[], +# output="", +# status_code=0, +# live_url=None, +# ) + +# # Add to orchestrator's response collection if available +# if ctx.deps.agent_responses is not None: +# ctx.deps.agent_responses.append(web_surfer_stream_output) + +# await _safe_websocket_send(ctx.deps.websocket, web_surfer_stream_output) + +# # Run web surfer agent through MCP +# async with orchestrator_agent.run_mcp_servers(): +# result = await orchestrator_agent.run( +# f"Use the web_surf_task tool to search: {task}" +# ) +# message = result.output + +# # Update WebSurfer's stream_output with final result +# web_surfer_stream_output.steps.append("Web search completed successfully") +# web_surfer_stream_output.output = message +# web_surfer_stream_output.status_code = 200 +# await _safe_websocket_send(ctx.deps.websocket, web_surfer_stream_output) + +# web_surfer_stream_output.steps.append("WebSurfer completed: Success") +# await _safe_websocket_send(ctx.deps.websocket, web_surfer_stream_output) + +# return message +# except Exception as e: +# error_msg = f"Error assigning web surfing task: {str(e)}" +# logfire.error(error_msg, exc_info=True) + +# # Update WebSurfer's stream_output with error +# web_surfer_stream_output.steps.append(f"Web search failed: {str(e)}") +# web_surfer_stream_output.status_code = 500 +# await _safe_websocket_send(ctx.deps.websocket, web_surfer_stream_output) + +# return f"Failed to assign web surfing task: {error_msg}" + + +# # Helper function for sending WebSocket messages +# async def _safe_websocket_send(websocket: Optional[WebSocket], message: Any) -> bool: +# """Safely send message through websocket with error handling""" +# try: +# if websocket and websocket.client_state.CONNECTED: +# await websocket.send_text(json.dumps(asdict(message))) +# logfire.debug(f"WebSocket message sent: {message}") +# return True +# return False +# except Exception as e: +# logfire.error(f"WebSocket send failed: {str(e)}") +# return False diff --git a/cortex_on/requirements.txt b/cortex_on/requirements.txt index a635c7e..25ae7cf 100644 --- a/cortex_on/requirements.txt +++ b/cortex_on/requirements.txt @@ -67,6 +67,7 @@ pycryptodome==3.21.0 pydantic==2.10.4 pydantic-ai==0.0.17 pydantic-ai-slim==0.0.17 +mcp==1.6.0 pydantic_core==2.27.2 Pygments==2.18.0 python-dateutil==2.9.0.post0 From 7ae43c246d62db17856b11807573de75a16cd987 Mon Sep 17 00:00:00 2001 From: aryan Date: Thu, 17 Apr 2025 21:10:14 +0530 Subject: [PATCH 02/35] fix(pydantic_ai): Consistent code according to updated pydantic library - Updated `anthropic`, `groq`, and `openai` dependencies to their latest versions. - Refactored agent initialization to use `provider` instead of `anthropic_client` for better clarity. --- cortex_on/agents/code_agent.py | 2 +- cortex_on/agents/orchestrator_agent.py | 2 +- cortex_on/agents/planner_agent.py | 2 +- cortex_on/agents/web_surfer.py | 10 +--------- cortex_on/instructor.py | 18 ++++++++++++++++-- cortex_on/requirements.txt | 10 +++++----- ta-browser/core/orchestrator.py | 6 +++--- ta-browser/core/skills/final_response.py | 4 ++-- ta-browser/core/utils/init_client.py | 2 +- ta-browser/requirements.txt | 11 +++++------ 10 files changed, 36 insertions(+), 31 deletions(-) diff --git a/cortex_on/agents/code_agent.py b/cortex_on/agents/code_agent.py index 43fa047..0fefac2 100644 --- a/cortex_on/agents/code_agent.py +++ b/cortex_on/agents/code_agent.py @@ -239,7 +239,7 @@ async def send_stream_update(ctx: RunContext[CoderAgentDeps], message: str) -> N # Initialize the model model = AnthropicModel( model_name=os.environ.get("ANTHROPIC_MODEL_NAME"), - anthropic_client=get_client() + provider = "anthropic" ) # Initialize the agent diff --git a/cortex_on/agents/orchestrator_agent.py b/cortex_on/agents/orchestrator_agent.py index 0b8edd8..7694480 100644 --- a/cortex_on/agents/orchestrator_agent.py +++ b/cortex_on/agents/orchestrator_agent.py @@ -80,7 +80,7 @@ class orchestrator_deps: server = MCPServerHTTP(url="http://localhost:3001/sse") model = AnthropicModel( - model_name=os.environ.get("ANTHROPIC_MODEL_NAME"), anthropic_client=get_client() + model_name=os.environ.get("ANTHROPIC_MODEL_NAME"), provider = "anthropic" ) orchestrator_agent = Agent( diff --git a/cortex_on/agents/planner_agent.py b/cortex_on/agents/planner_agent.py index 111b0e3..57f2058 100644 --- a/cortex_on/agents/planner_agent.py +++ b/cortex_on/agents/planner_agent.py @@ -130,7 +130,7 @@ class PlannerResult(BaseModel): model = AnthropicModel( model_name=os.environ.get("ANTHROPIC_MODEL_NAME"), - anthropic_client=get_client() + provider = "anthropic" ) planner_agent = Agent( diff --git a/cortex_on/agents/web_surfer.py b/cortex_on/agents/web_surfer.py index 34e2cfd..38b1e6a 100644 --- a/cortex_on/agents/web_surfer.py +++ b/cortex_on/agents/web_surfer.py @@ -11,15 +11,7 @@ from dotenv import load_dotenv from fastapi import WebSocket import logfire -from pydantic_ai.messages import ( - ArgsJson, - ModelRequest, - ModelResponse, - ToolCallPart, - ToolReturnPart, - UserPromptPart, -) - +from pydantic_ai.messages import ModelResponse, ModelRequest, ToolReturnPart # Local application imports from utils.stream_response_format import StreamResponse diff --git a/cortex_on/instructor.py b/cortex_on/instructor.py index b4f0efb..f55ed05 100644 --- a/cortex_on/instructor.py +++ b/cortex_on/instructor.py @@ -29,10 +29,18 @@ class DateTimeEncoder(json.JSONEncoder): - """Custom JSON encoder that can handle datetime objects""" + """Custom JSON encoder that can handle datetime objects and Pydantic models""" def default(self, obj): if isinstance(obj, datetime): return obj.isoformat() + if isinstance(obj, BaseModel): + # Handle both Pydantic v1 and v2 + if hasattr(obj, 'model_dump'): + return obj.model_dump() + elif hasattr(obj, 'dict'): + return obj.dict() + # Fallback for any other Pydantic structure + return {k: v for k, v in obj.__dict__.items() if not k.startswith('_')} return super().default(obj) @@ -101,6 +109,7 @@ async def run(self, task: str, websocket: WebSocket) -> List[Dict[str, Any]]: logfire.info("Task completed successfully") return [json.loads(json.dumps(asdict(i), cls=DateTimeEncoder)) for i in self.orchestrator_response] + except Exception as e: error_msg = f"Critical orchestration error: {str(e)}\n{traceback.format_exc()}" logfire.error(error_msg) @@ -112,7 +121,12 @@ async def run(self, task: str, websocket: WebSocket) -> List[Dict[str, Any]]: await self._safe_websocket_send(stream_output) # Even in case of critical error, return what we have - return [asdict(i) for i in self.orchestrator_response] + try: + return [json.loads(json.dumps(asdict(i), cls=DateTimeEncoder)) for i in self.orchestrator_response] + except Exception as serialize_error: + logfire.error(f"Failed to serialize response: {str(serialize_error)}") + # Last resort - return a simple error message + return [{"error": error_msg, "status_code": 500}] finally: logfire.info("Orchestration process complete") diff --git a/cortex_on/requirements.txt b/cortex_on/requirements.txt index 25ae7cf..c201882 100644 --- a/cortex_on/requirements.txt +++ b/cortex_on/requirements.txt @@ -2,7 +2,7 @@ aiohappyeyeballs==2.4.4 aiohttp==3.11.11 aiosignal==1.3.2 annotated-types==0.7.0 -anthropic==0.42.0 +anthropic==0.49.0 anyio==4.7.0 asyncio-atexit==1.0.1 attrs==24.3.0 @@ -25,7 +25,7 @@ frozenlist==1.5.0 google-auth==2.37.0 googleapis-common-protos==1.66.0 griffe==1.5.4 -groq==0.13.1 +groq==0.15.0 h11==0.14.0 httpcore==1.0.7 httpx==0.27.2 @@ -44,7 +44,7 @@ mistralai==1.2.5 multidict==6.1.0 mypy-extensions==1.0.0 numpy==2.2.1 -openai==1.58.1 +openai==1.74.0 opentelemetry-api==1.29.0 opentelemetry-exporter-otlp-proto-common==1.29.0 opentelemetry-exporter-otlp-proto-http==1.29.0 @@ -65,8 +65,8 @@ pyasn1_modules==0.4.1 pycparser==2.22 pycryptodome==3.21.0 pydantic==2.10.4 -pydantic-ai==0.0.17 -pydantic-ai-slim==0.0.17 +pydantic-ai==0.1.0 +pydantic-ai-slim==0.1.0 mcp==1.6.0 pydantic_core==2.27.2 Pygments==2.18.0 diff --git a/ta-browser/core/orchestrator.py b/ta-browser/core/orchestrator.py index 9dbf16e..f112130 100644 --- a/ta-browser/core/orchestrator.py +++ b/ta-browser/core/orchestrator.py @@ -676,7 +676,7 @@ async def run(self, command): self.log_token_usage( agent_type='planner', - usage=planner_response._usage, + usage=planner_response.usage, step=self.iteration_counter ) @@ -719,7 +719,7 @@ async def run(self, command): self.log_token_usage( agent_type='browser', - usage=browser_response._usage, + usage=browser_response.usage, step=self.iteration_counter ) @@ -780,7 +780,7 @@ async def run(self, command): self.log_token_usage( agent_type='critique', - usage=critique_response._usage, + usage=critique_response.usage, step=self.iteration_counter ) diff --git a/ta-browser/core/skills/final_response.py b/ta-browser/core/skills/final_response.py index fe3e3a9..c658b8d 100644 --- a/ta-browser/core/skills/final_response.py +++ b/ta-browser/core/skills/final_response.py @@ -50,14 +50,14 @@ def get_final_response_provider(): from core.utils.anthropic_client import get_client as get_anthropic_client from pydantic_ai.models.anthropic import AnthropicModel client = get_anthropic_client() - model = AnthropicModel(model_name=model_name, anthropic_client=client) + model = AnthropicModel(model_name=model_name, provider = "anthropic") provider = "anthropic" else: # OpenAI provider (default) from core.utils.openai_client import get_client as get_openai_client from pydantic_ai.models.openai import OpenAIModel client = get_openai_client() - model = OpenAIModel(model_name=model_name, openai_client=client) + model = OpenAIModel(model_name=model_name, provider = "openai") provider = "openai" return provider, client, model diff --git a/ta-browser/core/utils/init_client.py b/ta-browser/core/utils/init_client.py index 7d170c6..d33fa37 100644 --- a/ta-browser/core/utils/init_client.py +++ b/ta-browser/core/utils/init_client.py @@ -34,7 +34,7 @@ async def initialize_client(): # Create model instance from pydantic_ai.models.anthropic import AnthropicModel - model_instance = AnthropicModel(model_name=model_name, anthropic_client=client_instance) + model_instance = AnthropicModel(model_name=model_name, provider = "anthropic") logger.info(f"Anthropic client initialized successfully with model: {model_name}") return client_instance, model_instance diff --git a/ta-browser/requirements.txt b/ta-browser/requirements.txt index af8c9b0..7d51ddb 100644 --- a/ta-browser/requirements.txt +++ b/ta-browser/requirements.txt @@ -6,7 +6,7 @@ aiosignal==1.3.2 aiosmtplib==3.0.2 alembic==1.14.1 annotated-types==0.7.0 -anthropic==0.42.0 +anthropic==0.49.0 anyio==4.8.0 asgiref==3.8.1 asyncpg==0.30.0 @@ -41,7 +41,7 @@ google-auth==2.37.0 googleapis-common-protos==1.66.0 greenlet==3.0.3 griffe==1.5.4 -groq==0.13.1 +groq==0.15.0 grpcio==1.67.0 grpcio-status==1.62.3 h11==0.14.0 @@ -69,7 +69,7 @@ mypy-extensions==1.0.0 nest-asyncio==1.6.0 nltk==3.8.1 numpy==1.26.4 -openai==1.59.3 +openai==1.74.0 opentelemetry-api==1.29.0 opentelemetry-exporter-otlp-proto-common==1.29.0 opentelemetry-exporter-otlp-proto-http==1.29.0 @@ -96,8 +96,8 @@ pyautogen==0.2.27 pycparser==2.22 pycryptodome==3.20.0 pydantic==2.10.4 -pydantic-ai==0.0.17 -pydantic-ai-slim==0.0.17 +pydantic-ai==0.1.0 +pydantic-ai-slim==0.1.0 pydantic-core==2.27.2 pyee==11.1.0 pygments==2.18.0 @@ -137,7 +137,6 @@ typing-inspect==0.9.0 uritemplate==4.1.1 urllib3==2.3.0 uvicorn==0.30.3 -uvloop==0.21.0 watchfiles==0.24.0 websockets==13.1 wrapt==1.17.0 From 53a00a54cef1a544020af4a7aa55088000518ea4 Mon Sep 17 00:00:00 2001 From: Sakalya100 Date: Sat, 19 Apr 2025 11:12:27 +0530 Subject: [PATCH 03/35] fix(mcp + pydantic_ai): Added proper MCP integration and server initialization - Updated pydantic-ai dependencies to version 0.1.2. - Initialized AnthropicProvider with API key in code, planner, and orchestrator agents as per new updates - Added MCP server initialization and proper client call to server --- cortex_on/Dockerfile | 1 - cortex_on/agents/code_agent.py | 7 +++++-- cortex_on/agents/mcp_server.py | 2 +- cortex_on/agents/orchestrator_agent.py | 15 ++++++++++----- cortex_on/agents/planner_agent.py | 5 ++++- cortex_on/instructor.py | 12 +++++++----- cortex_on/requirements.txt | 4 ++-- 7 files changed, 29 insertions(+), 17 deletions(-) diff --git a/cortex_on/Dockerfile b/cortex_on/Dockerfile index 1867fcf..8d7373e 100644 --- a/cortex_on/Dockerfile +++ b/cortex_on/Dockerfile @@ -19,6 +19,5 @@ RUN uv pip install --system --no-cache-dir -r requirements.txt COPY . . EXPOSE 8081 -EXPOSE 3001 CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8081"] \ No newline at end of file diff --git a/cortex_on/agents/code_agent.py b/cortex_on/agents/code_agent.py index 0fefac2..7ec59e7 100644 --- a/cortex_on/agents/code_agent.py +++ b/cortex_on/agents/code_agent.py @@ -13,6 +13,7 @@ from pydantic import BaseModel, Field from pydantic_ai import Agent, RunContext from pydantic_ai.models.anthropic import AnthropicModel +from pydantic_ai.providers.anthropic import AnthropicProvider # Local application imports from utils.ant_client import get_client @@ -236,10 +237,12 @@ async def send_stream_update(ctx: RunContext[CoderAgentDeps], message: str) -> N stream_output_json = json.dumps(asdict(ctx.deps.stream_output)) logfire.debug("WebSocket message sent: {stream_output_json}", stream_output_json=stream_output_json) -# Initialize the model +# Initialize Anthropic provider with API key +provider = AnthropicProvider(api_key=os.environ.get("ANTHROPIC_API_KEY")) + model = AnthropicModel( model_name=os.environ.get("ANTHROPIC_MODEL_NAME"), - provider = "anthropic" + provider=provider ) # Initialize the agent diff --git a/cortex_on/agents/mcp_server.py b/cortex_on/agents/mcp_server.py index 7a264dc..5825eca 100644 --- a/cortex_on/agents/mcp_server.py +++ b/cortex_on/agents/mcp_server.py @@ -53,7 +53,7 @@ async def web_surf_task(task: str) -> str: def run_server(): """Run the MCP server""" - server.run(port=3001) + server.run() if __name__ == "__main__": diff --git a/cortex_on/agents/orchestrator_agent.py b/cortex_on/agents/orchestrator_agent.py index 7694480..900cc66 100644 --- a/cortex_on/agents/orchestrator_agent.py +++ b/cortex_on/agents/orchestrator_agent.py @@ -9,13 +9,14 @@ from fastapi import WebSocket from dotenv import load_dotenv from pydantic_ai.models.anthropic import AnthropicModel +from pydantic_ai.providers.anthropic import AnthropicProvider from pydantic_ai import Agent, RunContext -from pydantic_ai.mcp import MCPServerHTTP +from pydantic_ai.mcp import MCPServerHTTP, MCPServerStdio from utils.stream_response_format import StreamResponse from agents.planner_agent import planner_agent from agents.code_agent import coder_agent, CoderAgentDeps from utils.ant_client import get_client - +load_dotenv() @dataclass class orchestrator_deps: @@ -76,11 +77,15 @@ class orchestrator_deps: 5. Return the final result to the user """ -# Initialize single MCP server -server = MCPServerHTTP(url="http://localhost:3001/sse") +# Initialize MCP Server +server = MCPServerStdio('python', ["-m", "agents.mcp_server"]) + +# Initialize Anthropic provider with API key +provider = AnthropicProvider(api_key=os.environ.get("ANTHROPIC_API_KEY")) model = AnthropicModel( - model_name=os.environ.get("ANTHROPIC_MODEL_NAME"), provider = "anthropic" + model_name=os.environ.get("ANTHROPIC_MODEL_NAME"), + provider=provider ) orchestrator_agent = Agent( diff --git a/cortex_on/agents/planner_agent.py b/cortex_on/agents/planner_agent.py index 57f2058..41a33ae 100644 --- a/cortex_on/agents/planner_agent.py +++ b/cortex_on/agents/planner_agent.py @@ -8,6 +8,7 @@ from pydantic import BaseModel, Field from pydantic_ai import Agent from pydantic_ai.models.anthropic import AnthropicModel +from pydantic_ai.providers.anthropic import AnthropicProvider # Local application imports from utils.ant_client import get_client @@ -128,9 +129,11 @@ class PlannerResult(BaseModel): plan: str = Field(description="The generated plan in a string format") +provider = AnthropicProvider(api_key=os.environ.get("ANTHROPIC_API_KEY")) + model = AnthropicModel( model_name=os.environ.get("ANTHROPIC_MODEL_NAME"), - provider = "anthropic" + provider = provider ) planner_agent = Agent( diff --git a/cortex_on/instructor.py b/cortex_on/instructor.py index f55ed05..e62c068 100644 --- a/cortex_on/instructor.py +++ b/cortex_on/instructor.py @@ -1,6 +1,7 @@ # Standard library imports import json import os +import asyncio import traceback from dataclasses import asdict from datetime import datetime @@ -96,11 +97,12 @@ async def run(self, task: str, websocket: WebSocket) -> List[Dict[str, Any]]: await self._safe_websocket_send(stream_output) stream_output.steps.append("Agents initialized successfully") await self._safe_websocket_send(stream_output) - - orchestrator_response = await orchestrator_agent.run( - user_prompt=task, - deps=deps_for_orchestrator - ) + + async with orchestrator_agent.run_mcp_servers(): + orchestrator_response = await orchestrator_agent.run( + user_prompt=task, + deps=deps_for_orchestrator + ) stream_output.output = orchestrator_response.data stream_output.status_code = 200 logfire.debug(f"Orchestrator response: {orchestrator_response.data}") diff --git a/cortex_on/requirements.txt b/cortex_on/requirements.txt index c201882..270b3cd 100644 --- a/cortex_on/requirements.txt +++ b/cortex_on/requirements.txt @@ -65,8 +65,8 @@ pyasn1_modules==0.4.1 pycparser==2.22 pycryptodome==3.21.0 pydantic==2.10.4 -pydantic-ai==0.1.0 -pydantic-ai-slim==0.1.0 +pydantic-ai==0.1.2 +pydantic-ai-slim==0.1.2 mcp==1.6.0 pydantic_core==2.27.2 Pygments==2.18.0 From 59124250e76812aab7aeb881806773cd6bc134b0 Mon Sep 17 00:00:00 2001 From: Sakalya100 Date: Sun, 20 Apr 2025 17:36:33 +0530 Subject: [PATCH 04/35] fix: mcp server initialization updated 1. Changed MCP Server initialisation using MCPServerHTTP 2. Updated cortex_on Dockerfile to run server on 3001 port --- cortex_on/Dockerfile | 9 ++++++++- cortex_on/agents/mcp_server.py | 4 ++-- cortex_on/agents/orchestrator_agent.py | 3 ++- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/cortex_on/Dockerfile b/cortex_on/Dockerfile index 8d7373e..5465d68 100644 --- a/cortex_on/Dockerfile +++ b/cortex_on/Dockerfile @@ -18,6 +18,13 @@ RUN uv pip install --system --no-cache-dir -r requirements.txt COPY . . +# Set environment variables +ENV PYTHONPATH=/app +ENV ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY} +ENV ANTHROPIC_MODEL_NAME=${ANTHROPIC_MODEL_NAME:-claude-3-sonnet-20240229} + EXPOSE 8081 +EXPOSE 3001 -CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8081"] \ No newline at end of file +# Run both the MCP server and the main API +CMD ["sh", "-c", "python -m agents.mcp_server & uvicorn main:app --host 0.0.0.0 --port 8081"] \ No newline at end of file diff --git a/cortex_on/agents/mcp_server.py b/cortex_on/agents/mcp_server.py index 5825eca..514253e 100644 --- a/cortex_on/agents/mcp_server.py +++ b/cortex_on/agents/mcp_server.py @@ -9,7 +9,7 @@ import logfire # Initialize the single MCP server -server = FastMCP("CortexON MCP Server") +server = FastMCP("CortexON MCP Server", host="0.0.0.0", port=3001) @server.tool() @@ -53,7 +53,7 @@ async def web_surf_task(task: str) -> str: def run_server(): """Run the MCP server""" - server.run() + server.run(transport="sse") if __name__ == "__main__": diff --git a/cortex_on/agents/orchestrator_agent.py b/cortex_on/agents/orchestrator_agent.py index 900cc66..0effb8c 100644 --- a/cortex_on/agents/orchestrator_agent.py +++ b/cortex_on/agents/orchestrator_agent.py @@ -78,7 +78,8 @@ class orchestrator_deps: """ # Initialize MCP Server -server = MCPServerStdio('python', ["-m", "agents.mcp_server"]) +# server = MCPServerStdio('python', ["-m", "agents.mcp_server"]) +server = MCPServerHTTP(url='http://localhost:3001/sse') # Initialize Anthropic provider with API key provider = AnthropicProvider(api_key=os.environ.get("ANTHROPIC_API_KEY")) From 2a9552b3398209051b836d6db168cd6209502126 Mon Sep 17 00:00:00 2001 From: Sakalya100 Date: Tue, 22 Apr 2025 16:07:41 +0530 Subject: [PATCH 05/35] fix: mcp coder agent issue fixed --- cortex_on/agents/mcp_server.py | 39 ++++++++++++++++++++++++++++------ cortex_on/instructor.py | 4 ++-- 2 files changed, 35 insertions(+), 8 deletions(-) diff --git a/cortex_on/agents/mcp_server.py b/cortex_on/agents/mcp_server.py index 514253e..aecd1b4 100644 --- a/cortex_on/agents/mcp_server.py +++ b/cortex_on/agents/mcp_server.py @@ -3,8 +3,10 @@ from pydantic_ai.models.anthropic import AnthropicModel import os from utils.ant_client import get_client +from utils.stream_response_format import StreamResponse from agents.planner_agent import planner_agent -from agents.code_agent import coder_agent +from agents.code_agent import coder_agent, CoderAgentDeps +from agents.orchestrator_agent import orchestrator_deps from agents.web_surfer import WebSurfer import logfire @@ -17,8 +19,10 @@ async def plan_task(task: str) -> str: """Planner agent tool for creating task plans""" try: logfire.info(f"Planning task: {task}") + print(f"Planning task: {task}") planner_response = await planner_agent.run(user_prompt=task) - return planner_response.data.plan + print(f"Planner response: {planner_response}") + return planner_response.output.plan except Exception as e: logfire.error(f"Error in planner: {str(e)}", exc_info=True) return f"Error in planner: {str(e)}" @@ -28,9 +32,22 @@ async def plan_task(task: str) -> str: async def code_task(task: str) -> str: """Coder agent tool for implementing technical solutions""" try: - logfire.info(f"Executing code task: {task}") - coder_response = await coder_agent.run(user_prompt=task) - return coder_response.data.content + logfire.info(f"Executing code task: {task}") + + coder_stream_output = StreamResponse( + agent_name="Coder Agent", + instructions=task, + steps=[], + output="", + status_code=0 + ) + + deps_for_coder_agent = CoderAgentDeps(websocket=orchestrator_deps.websocket, stream_output=coder_stream_output) + + coder_response = await coder_agent.run(user_prompt=task, deps=deps_for_coder_agent) + logfire.info(f"Coder response: {coder_response}") + + return coder_response.output except Exception as e: logfire.error(f"Error in coder: {str(e)}", exc_info=True) return f"Error in coder: {str(e)}" @@ -41,9 +58,19 @@ async def web_surf_task(task: str) -> str: """Web surfer agent tool for web interactions""" try: logfire.info(f"Executing web surf task: {task}") + + web_surfer_stream_output = StreamResponse( + agent_name="Web Surfer", + instructions=task, + steps=[], + output="", + status_code=0, + live_url=None + ) + web_surfer = WebSurfer(api_url="http://localhost:8000/api/v1/web/stream") success, message, _ = await web_surfer.generate_reply( - instruction=task, websocket=None, stream_output=None + instruction=task, websocket=orchestrator_deps.websocket, stream_output=web_surfer_stream_output ) return message if success else f"Error in web surfer: {message}" except Exception as e: diff --git a/cortex_on/instructor.py b/cortex_on/instructor.py index e62c068..f455265 100644 --- a/cortex_on/instructor.py +++ b/cortex_on/instructor.py @@ -103,9 +103,9 @@ async def run(self, task: str, websocket: WebSocket) -> List[Dict[str, Any]]: user_prompt=task, deps=deps_for_orchestrator ) - stream_output.output = orchestrator_response.data + stream_output.output = orchestrator_response.output stream_output.status_code = 200 - logfire.debug(f"Orchestrator response: {orchestrator_response.data}") + logfire.debug(f"Orchestrator response: {orchestrator_response.output}") await self._safe_websocket_send(stream_output) logfire.info("Task completed successfully") From d68feffd3aaf637a4c0877710819d0b29a02ea90 Mon Sep 17 00:00:00 2001 From: 3rd-Son Date: Thu, 17 Apr 2025 11:28:53 +0100 Subject: [PATCH 06/35] Rebased HITL and Planning in Phases in MCP Integration --- cortex_on/Dockerfile | 9 +- cortex_on/agents/code_agent.py | 7 +- cortex_on/agents/mcp_server.py | 87 ++++ cortex_on/agents/orchestrator_agent.py | 662 +++++++++++++------------ cortex_on/agents/planner_agent.py | 100 +--- cortex_on/agents/web_surfer.py | 10 +- cortex_on/instructor.py | 34 +- cortex_on/requirements.txt | 5 +- 8 files changed, 471 insertions(+), 443 deletions(-) create mode 100644 cortex_on/agents/mcp_server.py diff --git a/cortex_on/Dockerfile b/cortex_on/Dockerfile index 8d7373e..5465d68 100644 --- a/cortex_on/Dockerfile +++ b/cortex_on/Dockerfile @@ -18,6 +18,13 @@ RUN uv pip install --system --no-cache-dir -r requirements.txt COPY . . +# Set environment variables +ENV PYTHONPATH=/app +ENV ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY} +ENV ANTHROPIC_MODEL_NAME=${ANTHROPIC_MODEL_NAME:-claude-3-sonnet-20240229} + EXPOSE 8081 +EXPOSE 3001 -CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8081"] \ No newline at end of file +# Run both the MCP server and the main API +CMD ["sh", "-c", "python -m agents.mcp_server & uvicorn main:app --host 0.0.0.0 --port 8081"] \ No newline at end of file diff --git a/cortex_on/agents/code_agent.py b/cortex_on/agents/code_agent.py index 43fa047..7ec59e7 100644 --- a/cortex_on/agents/code_agent.py +++ b/cortex_on/agents/code_agent.py @@ -13,6 +13,7 @@ from pydantic import BaseModel, Field from pydantic_ai import Agent, RunContext from pydantic_ai.models.anthropic import AnthropicModel +from pydantic_ai.providers.anthropic import AnthropicProvider # Local application imports from utils.ant_client import get_client @@ -236,10 +237,12 @@ async def send_stream_update(ctx: RunContext[CoderAgentDeps], message: str) -> N stream_output_json = json.dumps(asdict(ctx.deps.stream_output)) logfire.debug("WebSocket message sent: {stream_output_json}", stream_output_json=stream_output_json) -# Initialize the model +# Initialize Anthropic provider with API key +provider = AnthropicProvider(api_key=os.environ.get("ANTHROPIC_API_KEY")) + model = AnthropicModel( model_name=os.environ.get("ANTHROPIC_MODEL_NAME"), - anthropic_client=get_client() + provider=provider ) # Initialize the agent diff --git a/cortex_on/agents/mcp_server.py b/cortex_on/agents/mcp_server.py new file mode 100644 index 0000000..74bdbe5 --- /dev/null +++ b/cortex_on/agents/mcp_server.py @@ -0,0 +1,87 @@ +from mcp.server.fastmcp import FastMCP +from pydantic_ai import Agent +from pydantic_ai.models.anthropic import AnthropicModel +import os +from utils.ant_client import get_client +from utils.stream_response_format import StreamResponse +from agents.planner_agent import planner_agent +from agents.code_agent import coder_agent, CoderAgentDeps +from agents.orchestrator_agent import orchestrator_deps +from agents.web_surfer import WebSurfer +import logfire + +# Initialize the single MCP server +server = FastMCP("CortexON MCP Server", host="0.0.0.0", port=3001) + + +@server.tool() +async def plan_task(task: str) -> str: + """Planner agent tool for creating task plans""" + try: + logfire.info(f"Planning task: {task}") + print(f"Planning task: {task}") + planner_response = await planner_agent.run(user_prompt=task) + print(f"Planner response: {planner_response}") + return planner_response.output.plan + except Exception as e: + logfire.error(f"Error in planner: {str(e)}", exc_info=True) + return f"Error in planner: {str(e)}" + + +@server.tool() +async def code_task(task: str) -> str: + """Coder agent tool for implementing technical solutions""" + try: + logfire.info(f"Executing code task: {task}") + + coder_stream_output = StreamResponse( + agent_name="Coder Agent", + instructions=task, + steps=[], + output="", + status_code=0 + ) + + deps_for_coder_agent = CoderAgentDeps(websocket=orchestrator_deps.websocket, stream_output=coder_stream_output) + + coder_response = await coder_agent.run(user_prompt=task, deps=deps_for_coder_agent) + logfire.info(f"Coder response: {coder_response}") + + return coder_response.output + except Exception as e: + logfire.error(f"Error in coder: {str(e)}", exc_info=True) + return f"Error in coder: {str(e)}" + + +@server.tool() +async def web_surf_task(task: str) -> str: + """Web surfer agent tool for web interactions""" + try: + logfire.info(f"Executing web surf task: {task}") + + web_surfer_stream_output = StreamResponse( + agent_name="Web Surfer", + instructions=task, + steps=[], + output="", + status_code=0, + live_url=None + ) + + web_surfer = WebSurfer(api_url="http://localhost:8000/api/v1/web/stream") + success, message, _ = await web_surfer.generate_reply( + instruction=task, websocket=orchestrator_deps.websocket, stream_output=web_surfer_stream_output + ) + return message if success else f"Error in web surfer: {message}" + except Exception as e: + logfire.error(f"Error in web surfer: {str(e)}", exc_info=True) + return f"Error in web surfer: {str(e)}" + + +def run_server(): + """Run the MCP server""" + server.run(transport="sse") + + +if __name__ == "__main__": + run_server() \ No newline at end of file diff --git a/cortex_on/agents/orchestrator_agent.py b/cortex_on/agents/orchestrator_agent.py index 6b001ba..8287aaf 100644 --- a/cortex_on/agents/orchestrator_agent.py +++ b/cortex_on/agents/orchestrator_agent.py @@ -10,12 +10,13 @@ from dotenv import load_dotenv from pydantic_ai.models.anthropic import AnthropicModel from pydantic_ai import Agent, RunContext -from agents.web_surfer import WebSurfer +from pydantic_ai.mcp import MCPServerHTTP from utils.stream_response_format import StreamResponse from agents.planner_agent import planner_agent, update_todo_status from agents.code_agent import coder_agent, CoderAgentDeps from utils.ant_client import get_client + @dataclass class orchestrator_deps: websocket: Optional[WebSocket] = None @@ -23,6 +24,7 @@ class orchestrator_deps: # Add a collection to track agent-specific streams agent_responses: Optional[List[StreamResponse]] = None + orchestrator_system_prompt = """You are an AI orchestrator that manages a team of agents to solve tasks. You have access to tools for coordinating the agents and managing the task flow. [AGENT CAPABILITIES] @@ -158,357 +160,361 @@ class orchestrator_deps: - Format: "Task description (agent_name)" """ +# Initialize single MCP server +server = MCPServerHTTP(url="http://localhost:3001/sse") + model = AnthropicModel( - model_name=os.environ.get("ANTHROPIC_MODEL_NAME"), - anthropic_client=get_client() + model_name=os.environ.get("ANTHROPIC_MODEL_NAME"), anthropic_client=get_client() ) orchestrator_agent = Agent( model=model, name="Orchestrator Agent", system_prompt=orchestrator_system_prompt, - deps_type=orchestrator_deps + deps_type=orchestrator_deps, + mcp_servers=[server], ) -@orchestrator_agent.tool -async def plan_task(ctx: RunContext[orchestrator_deps], task: str) -> str: - """Plans the task and assigns it to the appropriate agents""" - try: - logfire.info(f"Planning task: {task}") - - # Create a new StreamResponse for Planner Agent - planner_stream_output = StreamResponse( - agent_name="Planner Agent", - instructions=task, - steps=[], - output="", - status_code=0 - ) - - # Add to orchestrator's response collection if available - if ctx.deps.agent_responses is not None: - ctx.deps.agent_responses.append(planner_stream_output) + +# @orchestrator_agent.tool +# async def plan_task(ctx: RunContext[orchestrator_deps], task: str) -> str: +# """Plans the task and assigns it to the appropriate agents""" +# try: +# logfire.info(f"Planning task: {task}") + +# # Create a new StreamResponse for Planner Agent +# planner_stream_output = StreamResponse( +# agent_name="Planner Agent", +# instructions=task, +# steps=[], +# output="", +# status_code=0 +# ) + +# # Add to orchestrator's response collection if available +# if ctx.deps.agent_responses is not None: +# ctx.deps.agent_responses.append(planner_stream_output) - await _safe_websocket_send(ctx.deps.websocket, planner_stream_output) - - # Update planner stream - planner_stream_output.steps.append("Planning task...") - await _safe_websocket_send(ctx.deps.websocket, planner_stream_output) - - # Run planner agent - planner_response = await planner_agent.run(user_prompt=task) - - # Update planner stream with results - plan_text = planner_response.data.plan - planner_stream_output.steps.append("Task planned successfully") - planner_stream_output.output = plan_text - planner_stream_output.status_code = 200 - await _safe_websocket_send(ctx.deps.websocket, planner_stream_output) - - # Also update orchestrator stream - ctx.deps.stream_output.steps.append("Task planned successfully") - await _safe_websocket_send(ctx.deps.websocket, ctx.deps.stream_output) - - return f"Task planned successfully\nTask: {plan_text}" - except Exception as e: - error_msg = f"Error planning task: {str(e)}" - logfire.error(error_msg, exc_info=True) - - # Update planner stream with error - if planner_stream_output: - planner_stream_output.steps.append(f"Planning failed: {str(e)}") - planner_stream_output.status_code = 500 - await _safe_websocket_send(ctx.deps.websocket, planner_stream_output) +# await _safe_websocket_send(ctx.deps.websocket, planner_stream_output) + +# # Update planner stream +# planner_stream_output.steps.append("Planning task...") +# await _safe_websocket_send(ctx.deps.websocket, planner_stream_output) + +# # Run planner agent +# planner_response = await planner_agent.run(user_prompt=task) + +# # Update planner stream with results +# plan_text = planner_response.data.plan +# planner_stream_output.steps.append("Task planned successfully") +# planner_stream_output.output = plan_text +# planner_stream_output.status_code = 200 +# await _safe_websocket_send(ctx.deps.websocket, planner_stream_output) + +# # Also update orchestrator stream +# ctx.deps.stream_output.steps.append("Task planned successfully") +# await _safe_websocket_send(ctx.deps.websocket, ctx.deps.stream_output) + +# return f"Task planned successfully\nTask: {plan_text}" +# except Exception as e: +# error_msg = f"Error planning task: {str(e)}" +# logfire.error(error_msg, exc_info=True) + +# # Update planner stream with error +# if planner_stream_output: +# planner_stream_output.steps.append(f"Planning failed: {str(e)}") +# planner_stream_output.status_code = 500 +# await _safe_websocket_send(ctx.deps.websocket, planner_stream_output) - # Also update orchestrator stream - if ctx.deps.stream_output: - ctx.deps.stream_output.steps.append(f"Planning failed: {str(e)}") - await _safe_websocket_send(ctx.deps.websocket, ctx.deps.stream_output) +# # Also update orchestrator stream +# if ctx.deps.stream_output: +# ctx.deps.stream_output.steps.append(f"Planning failed: {str(e)}") +# await _safe_websocket_send(ctx.deps.websocket, ctx.deps.stream_output) - return f"Failed to plan task: {error_msg}" - -@orchestrator_agent.tool -async def coder_task(ctx: RunContext[orchestrator_deps], task: str) -> str: - """Assigns coding tasks to the coder agent""" - try: - logfire.info(f"Assigning coding task: {task}") - - # Create a new StreamResponse for Coder Agent - coder_stream_output = StreamResponse( - agent_name="Coder Agent", - instructions=task, - steps=[], - output="", - status_code=0 - ) - - # Add to orchestrator's response collection if available - if ctx.deps.agent_responses is not None: - ctx.deps.agent_responses.append(coder_stream_output) - - # Send initial update for Coder Agent - await _safe_websocket_send(ctx.deps.websocket, coder_stream_output) - - # Create deps with the new stream_output - deps_for_coder_agent = CoderAgentDeps( - websocket=ctx.deps.websocket, - stream_output=coder_stream_output - ) - - # Run coder agent - coder_response = await coder_agent.run( - user_prompt=task, - deps=deps_for_coder_agent - ) - - # Extract response data - response_data = coder_response.data.content - - # Update coder_stream_output with coding results - coder_stream_output.output = response_data - coder_stream_output.status_code = 200 - coder_stream_output.steps.append("Coding task completed successfully") - await _safe_websocket_send(ctx.deps.websocket, coder_stream_output) - - # Add a reminder in the result message to update the plan using planner_agent_update - response_with_reminder = f"{response_data}\n\nReminder: You must now call planner_agent_update with the completed task description: \"{task} (coder_agent)\"" - - return response_with_reminder - except Exception as e: - error_msg = f"Error assigning coding task: {str(e)}" - logfire.error(error_msg, exc_info=True) - - # Update coder_stream_output with error - coder_stream_output.steps.append(f"Coding task failed: {str(e)}") - coder_stream_output.status_code = 500 - await _safe_websocket_send(ctx.deps.websocket, coder_stream_output) - - return f"Failed to assign coding task: {error_msg}" - -@orchestrator_agent.tool -async def web_surfer_task(ctx: RunContext[orchestrator_deps], task: str) -> str: - """Assigns web surfing tasks to the web surfer agent""" - try: - logfire.info(f"Assigning web surfing task: {task}") - - # Create a new StreamResponse for WebSurfer - web_surfer_stream_output = StreamResponse( - agent_name="Web Surfer", - instructions=task, - steps=[], - output="", - status_code=0, - live_url=None - ) - - # Add to orchestrator's response collection if available - if ctx.deps.agent_responses is not None: - ctx.deps.agent_responses.append(web_surfer_stream_output) - - await _safe_websocket_send(ctx.deps.websocket, web_surfer_stream_output) - - # Initialize WebSurfer agent - web_surfer_agent = WebSurfer(api_url="http://localhost:8000/api/v1/web/stream") - - # Run WebSurfer with its own stream_output - success, message, messages = await web_surfer_agent.generate_reply( - instruction=task, - websocket=ctx.deps.websocket, - stream_output=web_surfer_stream_output - ) - - # Update WebSurfer's stream_output with final result - if success: - web_surfer_stream_output.steps.append("Web search completed successfully") - web_surfer_stream_output.output = message - web_surfer_stream_output.status_code = 200 - - # Add a reminder to update the plan - message_with_reminder = f"{message}\n\nReminder: You must now call planner_agent_update with the completed task description: \"{task} (web_surfer_agent)\"" - else: - web_surfer_stream_output.steps.append(f"Web search completed with issues: {message[:100]}") - web_surfer_stream_output.status_code = 500 - message_with_reminder = message - - await _safe_websocket_send(ctx.deps.websocket, web_surfer_stream_output) - - web_surfer_stream_output.steps.append(f"WebSurfer completed: {'Success' if success else 'Failed'}") - await _safe_websocket_send(ctx.deps.websocket, web_surfer_stream_output) - - return message_with_reminder - except Exception as e: - error_msg = f"Error assigning web surfing task: {str(e)}" - logfire.error(error_msg, exc_info=True) - - # Update WebSurfer's stream_output with error - web_surfer_stream_output.steps.append(f"Web search failed: {str(e)}") - web_surfer_stream_output.status_code = 500 - await _safe_websocket_send(ctx.deps.websocket, web_surfer_stream_output) - return f"Failed to assign web surfing task: {error_msg}" - -@orchestrator_agent.tool -async def ask_human(ctx: RunContext[orchestrator_deps], question: str) -> str: - """Sends a question to the frontend and waits for human input""" - try: - logfire.info(f"Asking human: {question}") - - # Create a new StreamResponse for Human Input - human_stream_output = StreamResponse( - agent_name="Human Input", - instructions=question, - steps=[], - output="", - status_code=0 - ) - - # Add to orchestrator's response collection if available - if ctx.deps.agent_responses is not None: - ctx.deps.agent_responses.append(human_stream_output) - - # Send the question to frontend - await _safe_websocket_send(ctx.deps.websocket, human_stream_output) - - # Update stream with waiting message - human_stream_output.steps.append("Waiting for human input...") - await _safe_websocket_send(ctx.deps.websocket, human_stream_output) - - # Wait for response from frontend - response = await ctx.deps.websocket.receive_text() - - # Update stream with response - human_stream_output.steps.append("Received human input") - human_stream_output.output = response - human_stream_output.status_code = 200 - await _safe_websocket_send(ctx.deps.websocket, human_stream_output) - - return response - except Exception as e: - error_msg = f"Error getting human input: {str(e)}" - logfire.error(error_msg, exc_info=True) - - # Update stream with error - human_stream_output.steps.append(f"Failed to get human input: {str(e)}") - human_stream_output.status_code = 500 - await _safe_websocket_send(ctx.deps.websocket, human_stream_output) - - return f"Failed to get human input: {error_msg}" - -@orchestrator_agent.tool -async def planner_agent_update(ctx: RunContext[orchestrator_deps], completed_task: str) -> str: - """ - Updates the todo.md file to mark a task as completed and returns the full updated plan. +# return f"Failed to plan task: {error_msg}" + +# @orchestrator_agent.tool +# async def coder_task(ctx: RunContext[orchestrator_deps], task: str) -> str: +# """Assigns coding tasks to the coder agent""" +# try: +# logfire.info(f"Assigning coding task: {task}") + +# # Create a new StreamResponse for Coder Agent +# coder_stream_output = StreamResponse( +# agent_name="Coder Agent", +# instructions=task, +# steps=[], +# output="", +# status_code=0 +# ) + +# # Add to orchestrator's response collection if available +# if ctx.deps.agent_responses is not None: +# ctx.deps.agent_responses.append(coder_stream_output) + +# # Send initial update for Coder Agent +# await _safe_websocket_send(ctx.deps.websocket, coder_stream_output) + +# # Create deps with the new stream_output +# deps_for_coder_agent = CoderAgentDeps( +# websocket=ctx.deps.websocket, +# stream_output=coder_stream_output +# ) + +# # Run coder agent +# coder_response = await coder_agent.run( +# user_prompt=task, +# deps=deps_for_coder_agent +# ) + +# # Extract response data +# response_data = coder_response.data.content + +# # Update coder_stream_output with coding results +# coder_stream_output.output = response_data +# coder_stream_output.status_code = 200 +# coder_stream_output.steps.append("Coding task completed successfully") +# await _safe_websocket_send(ctx.deps.websocket, coder_stream_output) + +# # Add a reminder in the result message to update the plan using planner_agent_update +# response_with_reminder = f"{response_data}\n\nReminder: You must now call planner_agent_update with the completed task description: \"{task} (coder_agent)\"" + +# return response_with_reminder +# except Exception as e: +# error_msg = f"Error assigning coding task: {str(e)}" +# logfire.error(error_msg, exc_info=True) + +# # Update coder_stream_output with error +# coder_stream_output.steps.append(f"Coding task failed: {str(e)}") +# coder_stream_output.status_code = 500 +# await _safe_websocket_send(ctx.deps.websocket, coder_stream_output) + +# return f"Failed to assign coding task: {error_msg}" + +# @orchestrator_agent.tool +# async def web_surfer_task(ctx: RunContext[orchestrator_deps], task: str) -> str: +# """Assigns web surfing tasks to the web surfer agent""" +# try: +# logfire.info(f"Assigning web surfing task: {task}") + +# # Create a new StreamResponse for WebSurfer +# web_surfer_stream_output = StreamResponse( +# agent_name="Web Surfer", +# instructions=task, +# steps=[], +# output="", +# status_code=0, +# live_url=None +# ) + +# # Add to orchestrator's response collection if available +# if ctx.deps.agent_responses is not None: +# ctx.deps.agent_responses.append(web_surfer_stream_output) + +# await _safe_websocket_send(ctx.deps.websocket, web_surfer_stream_output) + +# # Initialize WebSurfer agent +# web_surfer_agent = WebSurfer(api_url="http://localhost:8000/api/v1/web/stream") + +# # Run WebSurfer with its own stream_output +# success, message, messages = await web_surfer_agent.generate_reply( +# instruction=task, +# websocket=ctx.deps.websocket, +# stream_output=web_surfer_stream_output +# ) + +# # Update WebSurfer's stream_output with final result +# if success: +# web_surfer_stream_output.steps.append("Web search completed successfully") +# web_surfer_stream_output.output = message +# web_surfer_stream_output.status_code = 200 + +# # Add a reminder to update the plan +# message_with_reminder = f"{message}\n\nReminder: You must now call planner_agent_update with the completed task description: \"{task} (web_surfer_agent)\"" +# else: +# web_surfer_stream_output.steps.append(f"Web search completed with issues: {message[:100]}") +# web_surfer_stream_output.status_code = 500 +# message_with_reminder = message + +# await _safe_websocket_send(ctx.deps.websocket, web_surfer_stream_output) + +# web_surfer_stream_output.steps.append(f"WebSurfer completed: {'Success' if success else 'Failed'}") +# await _safe_websocket_send(ctx.deps.websocket, web_surfer_stream_output) + +# return message_with_reminder +# except Exception as e: +# error_msg = f"Error assigning web surfing task: {str(e)}" +# logfire.error(error_msg, exc_info=True) + +# # Update WebSurfer's stream_output with error +# web_surfer_stream_output.steps.append(f"Web search failed: {str(e)}") +# web_surfer_stream_output.status_code = 500 +# await _safe_websocket_send(ctx.deps.websocket, web_surfer_stream_output) +# return f"Failed to assign web surfing task: {error_msg}" + +# @orchestrator_agent.tool +# async def ask_human(ctx: RunContext[orchestrator_deps], question: str) -> str: +# """Sends a question to the frontend and waits for human input""" +# try: +# logfire.info(f"Asking human: {question}") + +# # Create a new StreamResponse for Human Input +# human_stream_output = StreamResponse( +# agent_name="Human Input", +# instructions=question, +# steps=[], +# output="", +# status_code=0 +# ) + +# # Add to orchestrator's response collection if available +# if ctx.deps.agent_responses is not None: +# ctx.deps.agent_responses.append(human_stream_output) + +# # Send the question to frontend +# await _safe_websocket_send(ctx.deps.websocket, human_stream_output) + +# # Update stream with waiting message +# human_stream_output.steps.append("Waiting for human input...") +# await _safe_websocket_send(ctx.deps.websocket, human_stream_output) + +# # Wait for response from frontend +# response = await ctx.deps.websocket.receive_text() + +# # Update stream with response +# human_stream_output.steps.append("Received human input") +# human_stream_output.output = response +# human_stream_output.status_code = 200 +# await _safe_websocket_send(ctx.deps.websocket, human_stream_output) + +# return response +# except Exception as e: +# error_msg = f"Error getting human input: {str(e)}" +# logfire.error(error_msg, exc_info=True) + +# # Update stream with error +# human_stream_output.steps.append(f"Failed to get human input: {str(e)}") +# human_stream_output.status_code = 500 +# await _safe_websocket_send(ctx.deps.websocket, human_stream_output) + +# return f"Failed to get human input: {error_msg}" + +# @orchestrator_agent.tool +# async def planner_agent_update(ctx: RunContext[orchestrator_deps], completed_task: str) -> str: +# """ +# Updates the todo.md file to mark a task as completed and returns the full updated plan. - Args: - completed_task: Description of the completed task including which agent performed it +# Args: +# completed_task: Description of the completed task including which agent performed it - Returns: - The complete updated todo.md content with tasks marked as completed - """ - try: - logfire.info(f"Updating plan with completed task: {completed_task}") - - # Create a new StreamResponse for Planner Agent update - planner_stream_output = StreamResponse( - agent_name="Planner Agent", - instructions=f"Update todo.md to mark as completed: {completed_task}", - steps=[], - output="", - status_code=0 - ) - - # Send initial update - await _safe_websocket_send(ctx.deps.websocket, planner_stream_output) - - # Directly read and update the todo.md file - base_dir = os.path.abspath(os.path.dirname(os.path.dirname(__file__))) - planner_dir = os.path.join(base_dir, "agents", "planner") - todo_path = os.path.join(planner_dir, "todo.md") - - planner_stream_output.steps.append("Reading current todo.md...") - await _safe_websocket_send(ctx.deps.websocket, planner_stream_output) - - # Make sure the directory exists - os.makedirs(planner_dir, exist_ok=True) - - try: - # Check if todo.md exists - if not os.path.exists(todo_path): - planner_stream_output.steps.append("No todo.md file found. Will create new one after task completion.") - await _safe_websocket_send(ctx.deps.websocket, planner_stream_output) +# Returns: +# The complete updated todo.md content with tasks marked as completed +# """ +# try: +# logfire.info(f"Updating plan with completed task: {completed_task}") + +# # Create a new StreamResponse for Planner Agent update +# planner_stream_output = StreamResponse( +# agent_name="Planner Agent", +# instructions=f"Update todo.md to mark as completed: {completed_task}", +# steps=[], +# output="", +# status_code=0 +# ) + +# # Send initial update +# await _safe_websocket_send(ctx.deps.websocket, planner_stream_output) + +# # Directly read and update the todo.md file +# base_dir = os.path.abspath(os.path.dirname(os.path.dirname(__file__))) +# planner_dir = os.path.join(base_dir, "agents", "planner") +# todo_path = os.path.join(planner_dir, "todo.md") + +# planner_stream_output.steps.append("Reading current todo.md...") +# await _safe_websocket_send(ctx.deps.websocket, planner_stream_output) + +# # Make sure the directory exists +# os.makedirs(planner_dir, exist_ok=True) + +# try: +# # Check if todo.md exists +# if not os.path.exists(todo_path): +# planner_stream_output.steps.append("No todo.md file found. Will create new one after task completion.") +# await _safe_websocket_send(ctx.deps.websocket, planner_stream_output) - # We'll directly call planner_agent.run() to create a new plan first - plan_prompt = f"Create a simple task plan based on this completed task: {completed_task}" - plan_response = await planner_agent.run(user_prompt=plan_prompt) - current_content = plan_response.data.plan - else: - # Read existing todo.md - with open(todo_path, "r") as file: - current_content = file.read() - planner_stream_output.steps.append(f"Found existing todo.md ({len(current_content)} bytes)") - await _safe_websocket_send(ctx.deps.websocket, planner_stream_output) +# # We'll directly call planner_agent.run() to create a new plan first +# plan_prompt = f"Create a simple task plan based on this completed task: {completed_task}" +# plan_response = await planner_agent.run(user_prompt=plan_prompt) +# current_content = plan_response.data.plan +# else: +# # Read existing todo.md +# with open(todo_path, "r") as file: +# current_content = file.read() +# planner_stream_output.steps.append(f"Found existing todo.md ({len(current_content)} bytes)") +# await _safe_websocket_send(ctx.deps.websocket, planner_stream_output) - # Now call planner_agent.run() with specific instructions to update the plan - update_prompt = f""" - Here is the current todo.md content: +# # Now call planner_agent.run() with specific instructions to update the plan +# update_prompt = f""" +# Here is the current todo.md content: - {current_content} +# {current_content} - Please update this plan to mark the following task as completed: {completed_task} - Return ONLY the fully updated plan with appropriate tasks marked as [x] instead of [ ]. - """ +# Please update this plan to mark the following task as completed: {completed_task} +# Return ONLY the fully updated plan with appropriate tasks marked as [x] instead of [ ]. +# """ - planner_stream_output.steps.append("Asking planner to update the plan...") - await _safe_websocket_send(ctx.deps.websocket, planner_stream_output) +# planner_stream_output.steps.append("Asking planner to update the plan...") +# await _safe_websocket_send(ctx.deps.websocket, planner_stream_output) - updated_plan_response = await planner_agent.run(user_prompt=update_prompt) - updated_plan = updated_plan_response.data.plan +# updated_plan_response = await planner_agent.run(user_prompt=update_prompt) +# updated_plan = updated_plan_response.data.plan - # Write the updated plan back to todo.md - with open(todo_path, "w") as file: - file.write(updated_plan) +# # Write the updated plan back to todo.md +# with open(todo_path, "w") as file: +# file.write(updated_plan) - planner_stream_output.steps.append("Plan updated successfully") - planner_stream_output.output = updated_plan - planner_stream_output.status_code = 200 - await _safe_websocket_send(ctx.deps.websocket, planner_stream_output) +# planner_stream_output.steps.append("Plan updated successfully") +# planner_stream_output.output = updated_plan +# planner_stream_output.status_code = 200 +# await _safe_websocket_send(ctx.deps.websocket, planner_stream_output) - # Update orchestrator stream - if ctx.deps.stream_output: - ctx.deps.stream_output.steps.append(f"Plan updated to mark task as completed: {completed_task}") - await _safe_websocket_send(ctx.deps.websocket, ctx.deps.stream_output) +# # Update orchestrator stream +# if ctx.deps.stream_output: +# ctx.deps.stream_output.steps.append(f"Plan updated to mark task as completed: {completed_task}") +# await _safe_websocket_send(ctx.deps.websocket, ctx.deps.stream_output) - return updated_plan +# return updated_plan - except Exception as e: - error_msg = f"Error during plan update operations: {str(e)}" - logfire.error(error_msg, exc_info=True) +# except Exception as e: +# error_msg = f"Error during plan update operations: {str(e)}" +# logfire.error(error_msg, exc_info=True) - planner_stream_output.steps.append(f"Plan update failed: {str(e)}") - planner_stream_output.status_code = a500 - await _safe_websocket_send(ctx.deps.websocket, planner_stream_output) +# planner_stream_output.steps.append(f"Plan update failed: {str(e)}") +# planner_stream_output.status_code = 500 +# await _safe_websocket_send(ctx.deps.websocket, planner_stream_output) - return f"Failed to update the plan: {error_msg}" - - except Exception as e: - error_msg = f"Error updating plan: {str(e)}" - logfire.error(error_msg, exc_info=True) - - # Update stream output with error - if ctx.deps.stream_output: - ctx.deps.stream_output.steps.append(f"Failed to update plan: {str(e)}") - await _safe_websocket_send(ctx.deps.websocket, ctx.deps.stream_output) - - return f"Failed to update plan: {error_msg}" - -# Helper function for sending WebSocket messages -async def _safe_websocket_send(websocket: Optional[WebSocket], message: Any) -> bool: - """Safely send message through websocket with error handling""" - try: - if websocket and websocket.client_state.CONNECTED: - await websocket.send_text(json.dumps(asdict(message))) - logfire.debug("WebSocket message sent (_safe_websocket_send): {message}", message=message) - return True - return False - except Exception as e: - logfire.error(f"WebSocket send failed: {str(e)}") - return False \ No newline at end of file +# return f"Failed to update the plan: {error_msg}" + +# except Exception as e: +# error_msg = f"Error updating plan: {str(e)}" +# logfire.error(error_msg, exc_info=True) + +# # Update stream output with error +# if ctx.deps.stream_output: +# ctx.deps.stream_output.steps.append(f"Failed to update plan: {str(e)}") +# await _safe_websocket_send(ctx.deps.websocket, ctx.deps.stream_output) + +# return f"Failed to update plan: {error_msg}" + +# # Helper function for sending WebSocket messages +# async def _safe_websocket_send(websocket: Optional[WebSocket], message: Any) -> bool: +# """Safely send message through websocket with error handling""" +# try: +# if websocket and websocket.client_state.CONNECTED: +# await websocket.send_text(json.dumps(asdict(message))) +# logfire.debug("WebSocket message sent (_safe_websocket_send): {message}", message=message) +# return True +# return False +# except Exception as e: +# logfire.error(f"WebSocket send failed: {str(e)}") +# return False \ No newline at end of file diff --git a/cortex_on/agents/planner_agent.py b/cortex_on/agents/planner_agent.py index 897c22f..41a33ae 100644 --- a/cortex_on/agents/planner_agent.py +++ b/cortex_on/agents/planner_agent.py @@ -8,6 +8,7 @@ from pydantic import BaseModel, Field from pydantic_ai import Agent from pydantic_ai.models.anthropic import AnthropicModel +from pydantic_ai.providers.anthropic import AnthropicProvider # Local application imports from utils.ant_client import get_client @@ -21,7 +22,7 @@ agent_descriptions = "\n".join(f"Name: {agent}\n" for agent in agents) -planner_prompt = f"""You are a helpful AI assistant that creates and maintains plans to solve tasks. You have access to a terminal tool for reading and writing plans to files. +planner_prompt = f"""You are a helpful AI assistant that creates plans to solve tasks. You have access to a terminal tool for reading and writing plans to files. @@ -34,7 +35,7 @@ - You are provided with a team description that contains information about the team members and their expertise. - - You need to create and maintain a plan that leverages these team members effectively to solve the given task. + - You need to create a plan that leverages these team members effectively to solve the given task. - You have access to a terminal tool for reading and writing plans to files in the planner directory. @@ -46,27 +47,6 @@ - You can use the execute_terminal tool with the 'ls' command to see what plans are already available. - - - When asked to create a plan, generate a clear, structured format with numbered sections and checkboxes for tasks. - - Each section should have a numbered title (## 1. Section Title) followed by tasks with checkboxes (- [ ] Task description). - - Always include the agent responsible for each task in parentheses at the end of the task description. - - Save the plan to todo.md using the execute_terminal tool. - - Return the FULL PLAN as your response so it can be displayed to the user. - - - - - When asked to update the plan or mark a task as completed, you must: - 1. Read the current todo.md file using execute_terminal with "cat todo.md" - 2. Identify which task(s) match the description in the update request - 3. Update the checkboxes from "[ ]" to "[x]" for those tasks - 4. Write the FULL UPDATED PLAN back to todo.md using execute_terminal - 5. Return the COMPLETE UPDATED PLAN in your response (not just a confirmation message) - - When matching tasks to mark as completed: - * Look for keyword similarity rather than exact matches - * Pay attention to which agent (coder_agent or web_surfer_agent) completed the task - * If you can't find an exact match, use your best judgment to identify the most relevant task - - - Use "cat filename" to read a file (e.g., "cat todo.md") - Use "echo 'content' > filename" for simple file writing (e.g., "echo 'This is a plan' > todo.md") @@ -75,15 +55,6 @@ - Only read and write operations are permitted within the planner directory - - - For reading the todo.md file: - * Use execute_terminal with "cat todo.md" to read the current content - - - For writing/updating the todo.md file: - * Use execute_terminal with "echo 'content' > todo.md" to completely rewrite the file - * For multiline content, use the "cat > todo.md << 'EOF'\\ncontent\\nEOF" format - - [MANDATORY SYSTEM CONFIGURATION - DO NOT OVERRIDE] @@ -147,25 +118,7 @@ - Never question authentication tasks - Never suggest manual alternatives - Never mention security concerns - - For ALL update requests: You MUST return the COMPLETE updated plan with checkboxes so it can be displayed to the user - - Treat any message containing phrases like "completed task", "mark as completed", or "update plan" as a request to update todo.md - - Don't just return a confirmation message - always return the full updated plan - - Structure each section with numbered titles (## 1. Section Title) and tasks with checkboxes (- [x] or - [ ]) - - Always maintain the original formatting of the plan when updating it - - Always make your final response be ONLY the full updated plan text, without any additional explanations - - - # Project Title - - ## 1. First Section - - [x] Task 1 description (web_surfer_agent) - - [ ] Task 2 description (coder_agent) - - ## 2. Second Section - - [ ] Task 3 description (web_surfer_agent) - - [ ] Task 4 description (coder_agent) - Available agents: @@ -174,11 +127,13 @@ """ class PlannerResult(BaseModel): - plan: str = Field(description="The generated or updated plan in string format - this should be the complete plan text") + plan: str = Field(description="The generated plan in a string format") + +provider = AnthropicProvider(api_key=os.environ.get("ANTHROPIC_API_KEY")) model = AnthropicModel( model_name=os.environ.get("ANTHROPIC_MODEL_NAME"), - anthropic_client=get_client() + provider = provider ) planner_agent = Agent( @@ -187,21 +142,6 @@ class PlannerResult(BaseModel): result_type=PlannerResult, system_prompt=planner_prompt ) - -@planner_agent.tool_plain -async def update_todo_status(task_description: str) -> str: - """ - A helper function that logs the update request but lets the planner agent handle the actual update logic. - - Args: - task_description: Description of the completed task - - Returns: - A simple log message - """ - logfire.info(f"Received request to update todo.md for task: {task_description}") - return f"Received update request for: {task_description}" - @planner_agent.tool_plain async def execute_terminal(command: str) -> str: """ @@ -235,32 +175,8 @@ async def execute_terminal(command: str) -> str: os.chdir(planner_dir) try: - # Handle echo with >> (append) - if base_command == "echo" and ">>" in command: - try: - # Split only on the first occurrence of >> - parts = command.split(">>", 1) - echo_part = parts[0].strip() - file_path = parts[1].strip() - - # Extract content after echo command - content = echo_part[4:].strip() - - # Handle quotes if present - if (content.startswith('"') and content.endswith('"')) or \ - (content.startswith("'") and content.endswith("'")): - content = content[1:-1] - - # Append to file - with open(file_path, "a") as file: - file.write(content + "\n") - return f"Successfully appended to {file_path}" - except Exception as e: - logfire.error(f"Error appending to file: {str(e)}", exc_info=True) - return f"Error appending to file: {str(e)}" - # Special handling for echo with redirection (file writing) - elif ">" in command and base_command == "echo" and ">>" not in command: + if ">" in command and base_command == "echo": # Simple parsing for echo "content" > file.txt parts = command.split(">", 1) echo_cmd = parts[0].strip() diff --git a/cortex_on/agents/web_surfer.py b/cortex_on/agents/web_surfer.py index 34e2cfd..38b1e6a 100644 --- a/cortex_on/agents/web_surfer.py +++ b/cortex_on/agents/web_surfer.py @@ -11,15 +11,7 @@ from dotenv import load_dotenv from fastapi import WebSocket import logfire -from pydantic_ai.messages import ( - ArgsJson, - ModelRequest, - ModelResponse, - ToolCallPart, - ToolReturnPart, - UserPromptPart, -) - +from pydantic_ai.messages import ModelResponse, ModelRequest, ToolReturnPart # Local application imports from utils.stream_response_format import StreamResponse diff --git a/cortex_on/instructor.py b/cortex_on/instructor.py index b4f0efb..f455265 100644 --- a/cortex_on/instructor.py +++ b/cortex_on/instructor.py @@ -1,6 +1,7 @@ # Standard library imports import json import os +import asyncio import traceback from dataclasses import asdict from datetime import datetime @@ -29,10 +30,18 @@ class DateTimeEncoder(json.JSONEncoder): - """Custom JSON encoder that can handle datetime objects""" + """Custom JSON encoder that can handle datetime objects and Pydantic models""" def default(self, obj): if isinstance(obj, datetime): return obj.isoformat() + if isinstance(obj, BaseModel): + # Handle both Pydantic v1 and v2 + if hasattr(obj, 'model_dump'): + return obj.model_dump() + elif hasattr(obj, 'dict'): + return obj.dict() + # Fallback for any other Pydantic structure + return {k: v for k, v in obj.__dict__.items() if not k.startswith('_')} return super().default(obj) @@ -88,19 +97,21 @@ async def run(self, task: str, websocket: WebSocket) -> List[Dict[str, Any]]: await self._safe_websocket_send(stream_output) stream_output.steps.append("Agents initialized successfully") await self._safe_websocket_send(stream_output) - - orchestrator_response = await orchestrator_agent.run( - user_prompt=task, - deps=deps_for_orchestrator - ) - stream_output.output = orchestrator_response.data + + async with orchestrator_agent.run_mcp_servers(): + orchestrator_response = await orchestrator_agent.run( + user_prompt=task, + deps=deps_for_orchestrator + ) + stream_output.output = orchestrator_response.output stream_output.status_code = 200 - logfire.debug(f"Orchestrator response: {orchestrator_response.data}") + logfire.debug(f"Orchestrator response: {orchestrator_response.output}") await self._safe_websocket_send(stream_output) logfire.info("Task completed successfully") return [json.loads(json.dumps(asdict(i), cls=DateTimeEncoder)) for i in self.orchestrator_response] + except Exception as e: error_msg = f"Critical orchestration error: {str(e)}\n{traceback.format_exc()}" logfire.error(error_msg) @@ -112,7 +123,12 @@ async def run(self, task: str, websocket: WebSocket) -> List[Dict[str, Any]]: await self._safe_websocket_send(stream_output) # Even in case of critical error, return what we have - return [asdict(i) for i in self.orchestrator_response] + try: + return [json.loads(json.dumps(asdict(i), cls=DateTimeEncoder)) for i in self.orchestrator_response] + except Exception as serialize_error: + logfire.error(f"Failed to serialize response: {str(serialize_error)}") + # Last resort - return a simple error message + return [{"error": error_msg, "status_code": 500}] finally: logfire.info("Orchestration process complete") diff --git a/cortex_on/requirements.txt b/cortex_on/requirements.txt index a635c7e..c26b532 100644 --- a/cortex_on/requirements.txt +++ b/cortex_on/requirements.txt @@ -65,8 +65,9 @@ pyasn1_modules==0.4.1 pycparser==2.22 pycryptodome==3.21.0 pydantic==2.10.4 -pydantic-ai==0.0.17 -pydantic-ai-slim==0.0.17 +pydantic-ai==0.1.2 +pydantic-ai-slim==0.1.2 +mcp==1.6.0 pydantic_core==2.27.2 Pygments==2.18.0 python-dateutil==2.9.0.post0 From 1175faa1494c4c489cbd9465618f7c95ff82c1ee Mon Sep 17 00:00:00 2001 From: aryan Date: Thu, 17 Apr 2025 21:10:14 +0530 Subject: [PATCH 07/35] Rebased HITL and Planning in Phases in MCP integration - Updated `anthropic`, `groq`, and `openai` dependencies to their latest versions. - Refactored agent initialization to use `provider` instead of `anthropic_client` for better clarity. --- cortex_on/agents/orchestrator_agent.py | 2 +- cortex_on/requirements.txt | 11 ++++++++--- ta-browser/core/orchestrator.py | 6 +++--- ta-browser/core/skills/final_response.py | 4 ++-- ta-browser/core/utils/init_client.py | 2 +- ta-browser/requirements.txt | 11 +++++------ 6 files changed, 20 insertions(+), 16 deletions(-) diff --git a/cortex_on/agents/orchestrator_agent.py b/cortex_on/agents/orchestrator_agent.py index 8287aaf..3e74455 100644 --- a/cortex_on/agents/orchestrator_agent.py +++ b/cortex_on/agents/orchestrator_agent.py @@ -164,7 +164,7 @@ class orchestrator_deps: server = MCPServerHTTP(url="http://localhost:3001/sse") model = AnthropicModel( - model_name=os.environ.get("ANTHROPIC_MODEL_NAME"), anthropic_client=get_client() + model_name=os.environ.get("ANTHROPIC_MODEL_NAME"), provider = "anthropic" ) orchestrator_agent = Agent( diff --git a/cortex_on/requirements.txt b/cortex_on/requirements.txt index c26b532..58e805a 100644 --- a/cortex_on/requirements.txt +++ b/cortex_on/requirements.txt @@ -2,7 +2,7 @@ aiohappyeyeballs==2.4.4 aiohttp==3.11.11 aiosignal==1.3.2 annotated-types==0.7.0 -anthropic==0.42.0 +anthropic==0.49.0 anyio==4.7.0 asyncio-atexit==1.0.1 attrs==24.3.0 @@ -25,7 +25,7 @@ frozenlist==1.5.0 google-auth==2.37.0 googleapis-common-protos==1.66.0 griffe==1.5.4 -groq==0.13.1 +groq==0.15.0 h11==0.14.0 httpcore==1.0.7 httpx==0.27.2 @@ -44,7 +44,7 @@ mistralai==1.2.5 multidict==6.1.0 mypy-extensions==1.0.0 numpy==2.2.1 -openai==1.58.1 +openai==1.74.0 opentelemetry-api==1.29.0 opentelemetry-exporter-otlp-proto-common==1.29.0 opentelemetry-exporter-otlp-proto-http==1.29.0 @@ -65,8 +65,13 @@ pyasn1_modules==0.4.1 pycparser==2.22 pycryptodome==3.21.0 pydantic==2.10.4 +<<<<<<< HEAD pydantic-ai==0.1.2 pydantic-ai-slim==0.1.2 +======= +pydantic-ai==0.1.0 +pydantic-ai-slim==0.1.0 +>>>>>>> 7ae43c2 (fix(pydantic_ai): Consistent code according to updated pydantic library) mcp==1.6.0 pydantic_core==2.27.2 Pygments==2.18.0 diff --git a/ta-browser/core/orchestrator.py b/ta-browser/core/orchestrator.py index 9dbf16e..f112130 100644 --- a/ta-browser/core/orchestrator.py +++ b/ta-browser/core/orchestrator.py @@ -676,7 +676,7 @@ async def run(self, command): self.log_token_usage( agent_type='planner', - usage=planner_response._usage, + usage=planner_response.usage, step=self.iteration_counter ) @@ -719,7 +719,7 @@ async def run(self, command): self.log_token_usage( agent_type='browser', - usage=browser_response._usage, + usage=browser_response.usage, step=self.iteration_counter ) @@ -780,7 +780,7 @@ async def run(self, command): self.log_token_usage( agent_type='critique', - usage=critique_response._usage, + usage=critique_response.usage, step=self.iteration_counter ) diff --git a/ta-browser/core/skills/final_response.py b/ta-browser/core/skills/final_response.py index fe3e3a9..c658b8d 100644 --- a/ta-browser/core/skills/final_response.py +++ b/ta-browser/core/skills/final_response.py @@ -50,14 +50,14 @@ def get_final_response_provider(): from core.utils.anthropic_client import get_client as get_anthropic_client from pydantic_ai.models.anthropic import AnthropicModel client = get_anthropic_client() - model = AnthropicModel(model_name=model_name, anthropic_client=client) + model = AnthropicModel(model_name=model_name, provider = "anthropic") provider = "anthropic" else: # OpenAI provider (default) from core.utils.openai_client import get_client as get_openai_client from pydantic_ai.models.openai import OpenAIModel client = get_openai_client() - model = OpenAIModel(model_name=model_name, openai_client=client) + model = OpenAIModel(model_name=model_name, provider = "openai") provider = "openai" return provider, client, model diff --git a/ta-browser/core/utils/init_client.py b/ta-browser/core/utils/init_client.py index 7d170c6..d33fa37 100644 --- a/ta-browser/core/utils/init_client.py +++ b/ta-browser/core/utils/init_client.py @@ -34,7 +34,7 @@ async def initialize_client(): # Create model instance from pydantic_ai.models.anthropic import AnthropicModel - model_instance = AnthropicModel(model_name=model_name, anthropic_client=client_instance) + model_instance = AnthropicModel(model_name=model_name, provider = "anthropic") logger.info(f"Anthropic client initialized successfully with model: {model_name}") return client_instance, model_instance diff --git a/ta-browser/requirements.txt b/ta-browser/requirements.txt index af8c9b0..7d51ddb 100644 --- a/ta-browser/requirements.txt +++ b/ta-browser/requirements.txt @@ -6,7 +6,7 @@ aiosignal==1.3.2 aiosmtplib==3.0.2 alembic==1.14.1 annotated-types==0.7.0 -anthropic==0.42.0 +anthropic==0.49.0 anyio==4.8.0 asgiref==3.8.1 asyncpg==0.30.0 @@ -41,7 +41,7 @@ google-auth==2.37.0 googleapis-common-protos==1.66.0 greenlet==3.0.3 griffe==1.5.4 -groq==0.13.1 +groq==0.15.0 grpcio==1.67.0 grpcio-status==1.62.3 h11==0.14.0 @@ -69,7 +69,7 @@ mypy-extensions==1.0.0 nest-asyncio==1.6.0 nltk==3.8.1 numpy==1.26.4 -openai==1.59.3 +openai==1.74.0 opentelemetry-api==1.29.0 opentelemetry-exporter-otlp-proto-common==1.29.0 opentelemetry-exporter-otlp-proto-http==1.29.0 @@ -96,8 +96,8 @@ pyautogen==0.2.27 pycparser==2.22 pycryptodome==3.20.0 pydantic==2.10.4 -pydantic-ai==0.0.17 -pydantic-ai-slim==0.0.17 +pydantic-ai==0.1.0 +pydantic-ai-slim==0.1.0 pydantic-core==2.27.2 pyee==11.1.0 pygments==2.18.0 @@ -137,7 +137,6 @@ typing-inspect==0.9.0 uritemplate==4.1.1 urllib3==2.3.0 uvicorn==0.30.3 -uvloop==0.21.0 watchfiles==0.24.0 websockets==13.1 wrapt==1.17.0 From df88643a8d8e49d95d670329c7f54be53e29af05 Mon Sep 17 00:00:00 2001 From: Sakalya100 Date: Sat, 19 Apr 2025 11:12:27 +0530 Subject: [PATCH 08/35] fix(mcp + pydantic_ai): Added proper MCP integration and server initialization - Updated pydantic-ai dependencies to version 0.1.2. - Initialized AnthropicProvider with API key in code, planner, and orchestrator agents as per new updates - Added MCP server initialization and proper client call to server --- cortex_on/Dockerfile | 1 - cortex_on/agents/orchestrator_agent.py | 15 ++++++++++----- cortex_on/requirements.txt | 5 +++++ 3 files changed, 15 insertions(+), 6 deletions(-) diff --git a/cortex_on/Dockerfile b/cortex_on/Dockerfile index 5465d68..96eff8c 100644 --- a/cortex_on/Dockerfile +++ b/cortex_on/Dockerfile @@ -24,7 +24,6 @@ ENV ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY} ENV ANTHROPIC_MODEL_NAME=${ANTHROPIC_MODEL_NAME:-claude-3-sonnet-20240229} EXPOSE 8081 -EXPOSE 3001 # Run both the MCP server and the main API CMD ["sh", "-c", "python -m agents.mcp_server & uvicorn main:app --host 0.0.0.0 --port 8081"] \ No newline at end of file diff --git a/cortex_on/agents/orchestrator_agent.py b/cortex_on/agents/orchestrator_agent.py index 3e74455..eb56e7f 100644 --- a/cortex_on/agents/orchestrator_agent.py +++ b/cortex_on/agents/orchestrator_agent.py @@ -9,13 +9,14 @@ from fastapi import WebSocket from dotenv import load_dotenv from pydantic_ai.models.anthropic import AnthropicModel +from pydantic_ai.providers.anthropic import AnthropicProvider from pydantic_ai import Agent, RunContext -from pydantic_ai.mcp import MCPServerHTTP +from pydantic_ai.mcp import MCPServerHTTP, MCPServerStdio from utils.stream_response_format import StreamResponse from agents.planner_agent import planner_agent, update_todo_status from agents.code_agent import coder_agent, CoderAgentDeps from utils.ant_client import get_client - +load_dotenv() @dataclass class orchestrator_deps: @@ -160,11 +161,15 @@ class orchestrator_deps: - Format: "Task description (agent_name)" """ -# Initialize single MCP server -server = MCPServerHTTP(url="http://localhost:3001/sse") +# Initialize MCP Server +server = MCPServerStdio('python', ["-m", "agents.mcp_server"]) + +# Initialize Anthropic provider with API key +provider = AnthropicProvider(api_key=os.environ.get("ANTHROPIC_API_KEY")) model = AnthropicModel( - model_name=os.environ.get("ANTHROPIC_MODEL_NAME"), provider = "anthropic" + model_name=os.environ.get("ANTHROPIC_MODEL_NAME"), + provider=provider ) orchestrator_agent = Agent( diff --git a/cortex_on/requirements.txt b/cortex_on/requirements.txt index 58e805a..b388882 100644 --- a/cortex_on/requirements.txt +++ b/cortex_on/requirements.txt @@ -66,12 +66,17 @@ pycparser==2.22 pycryptodome==3.21.0 pydantic==2.10.4 <<<<<<< HEAD +<<<<<<< HEAD pydantic-ai==0.1.2 pydantic-ai-slim==0.1.2 ======= pydantic-ai==0.1.0 pydantic-ai-slim==0.1.0 >>>>>>> 7ae43c2 (fix(pydantic_ai): Consistent code according to updated pydantic library) +======= +pydantic-ai==0.1.2 +pydantic-ai-slim==0.1.2 +>>>>>>> 53a00a5 (fix(mcp + pydantic_ai): Added proper MCP integration and server initialization) mcp==1.6.0 pydantic_core==2.27.2 Pygments==2.18.0 From f8e1272664fe8495c50b4700aa3ad9d040a5d494 Mon Sep 17 00:00:00 2001 From: Sakalya100 Date: Sun, 20 Apr 2025 17:36:33 +0530 Subject: [PATCH 09/35] fix: mcp server initialization updated 1. Changed MCP Server initialisation using MCPServerHTTP 2. Updated cortex_on Dockerfile to run server on 3001 port --- cortex_on/Dockerfile | 1 + cortex_on/agents/orchestrator_agent.py | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/cortex_on/Dockerfile b/cortex_on/Dockerfile index 96eff8c..5465d68 100644 --- a/cortex_on/Dockerfile +++ b/cortex_on/Dockerfile @@ -24,6 +24,7 @@ ENV ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY} ENV ANTHROPIC_MODEL_NAME=${ANTHROPIC_MODEL_NAME:-claude-3-sonnet-20240229} EXPOSE 8081 +EXPOSE 3001 # Run both the MCP server and the main API CMD ["sh", "-c", "python -m agents.mcp_server & uvicorn main:app --host 0.0.0.0 --port 8081"] \ No newline at end of file diff --git a/cortex_on/agents/orchestrator_agent.py b/cortex_on/agents/orchestrator_agent.py index eb56e7f..0f34227 100644 --- a/cortex_on/agents/orchestrator_agent.py +++ b/cortex_on/agents/orchestrator_agent.py @@ -162,7 +162,8 @@ class orchestrator_deps: """ # Initialize MCP Server -server = MCPServerStdio('python', ["-m", "agents.mcp_server"]) +# server = MCPServerStdio('python', ["-m", "agents.mcp_server"]) +server = MCPServerHTTP(url='http://localhost:3001/sse') # Initialize Anthropic provider with API key provider = AnthropicProvider(api_key=os.environ.get("ANTHROPIC_API_KEY")) From 2c83372c9a531833173ee7fd829a9a9e3246fe65 Mon Sep 17 00:00:00 2001 From: Sakalya100 Date: Wed, 23 Apr 2025 20:45:53 +0530 Subject: [PATCH 10/35] Added HITL + Planning in Phases to Server --- cortex_on/agents/mcp_server.py | 416 ++++++++++++++++-- cortex_on/agents/orchestrator_agent.py | 12 +- cortex_on/agents/planner_agent.py | 95 +++- .../final_report.md | 57 +++ .../findings_task1_20250423_085923.md | 380 ++++++++++++++++ .../findings_task2_20250423_090023.md | 337 ++++++++++++++ .../findings_task3_20250423_090124.md | 312 +++++++++++++ .../findings_task4_20250423_090151.md | 2 + .../findings_task5_20250423_090220.md | 314 +++++++++++++ .../todo.json | 130 ++++++ .../todo.md | 64 +++ .../final_report.md | 109 +++++ .../findings_task1_20250423_090903.md | 224 ++++++++++ .../findings_task2_20250423_090946.md | 244 ++++++++++ .../findings_task3_20250423_091014.md | 216 +++++++++ .../findings_task4_20250423_091041.md | 216 +++++++++ .../findings_task5_20250423_091112.md | 236 ++++++++++ .../findings_task6_20250423_091142.md | 194 ++++++++ .../todo.json | 155 +++++++ .../todo.md | 68 +++ ...s_1fae9cf_fuel_credit_cards_comparison_.md | 4 + ...it-card_bank-of-baroda-easy-credit-card.md | 144 ++++++ .../final_report.md | 83 ++++ .../findings_task1_20250423_105913.md | 293 ++++++++++++ .../findings_task2_20250423_105933.md | 304 +++++++++++++ .../todo.json | 81 ++++ .../todo.md | 55 +++ cortex_on/requirements.txt | 15 +- 28 files changed, 4691 insertions(+), 69 deletions(-) create mode 100644 cortex_on/agents/research_data/6a513487-cb89-49b9-b6b8-e75d22d661c1/final_report.md create mode 100644 cortex_on/agents/research_data/6a513487-cb89-49b9-b6b8-e75d22d661c1/findings_task1_20250423_085923.md create mode 100644 cortex_on/agents/research_data/6a513487-cb89-49b9-b6b8-e75d22d661c1/findings_task2_20250423_090023.md create mode 100644 cortex_on/agents/research_data/6a513487-cb89-49b9-b6b8-e75d22d661c1/findings_task3_20250423_090124.md create mode 100644 cortex_on/agents/research_data/6a513487-cb89-49b9-b6b8-e75d22d661c1/findings_task4_20250423_090151.md create mode 100644 cortex_on/agents/research_data/6a513487-cb89-49b9-b6b8-e75d22d661c1/findings_task5_20250423_090220.md create mode 100644 cortex_on/agents/research_data/6a513487-cb89-49b9-b6b8-e75d22d661c1/todo.json create mode 100644 cortex_on/agents/research_data/6a513487-cb89-49b9-b6b8-e75d22d661c1/todo.md create mode 100644 cortex_on/agents/research_data/e927c664-4a03-488c-852e-be747de0a3d3/final_report.md create mode 100644 cortex_on/agents/research_data/e927c664-4a03-488c-852e-be747de0a3d3/findings_task1_20250423_090903.md create mode 100644 cortex_on/agents/research_data/e927c664-4a03-488c-852e-be747de0a3d3/findings_task2_20250423_090946.md create mode 100644 cortex_on/agents/research_data/e927c664-4a03-488c-852e-be747de0a3d3/findings_task3_20250423_091014.md create mode 100644 cortex_on/agents/research_data/e927c664-4a03-488c-852e-be747de0a3d3/findings_task4_20250423_091041.md create mode 100644 cortex_on/agents/research_data/e927c664-4a03-488c-852e-be747de0a3d3/findings_task5_20250423_091112.md create mode 100644 cortex_on/agents/research_data/e927c664-4a03-488c-852e-be747de0a3d3/findings_task6_20250423_091142.md create mode 100644 cortex_on/agents/research_data/e927c664-4a03-488c-852e-be747de0a3d3/todo.json create mode 100644 cortex_on/agents/research_data/e927c664-4a03-488c-852e-be747de0a3d3/todo.md create mode 100644 cortex_on/agents/research_data/ea15e211-2645-476f-bdff-070ad4c941d7/batch_extraction_20250423_105854/1_https___www_reddit_com_r_CreditCardsIndia_comments_1fae9cf_fuel_credit_cards_comparison_.md create mode 100644 cortex_on/agents/research_data/ea15e211-2645-476f-bdff-070ad4c941d7/batch_extraction_20250423_105854/2_https___www_creditkaro_com_credit-card_bank-of-baroda-easy-credit-card.md create mode 100644 cortex_on/agents/research_data/ea15e211-2645-476f-bdff-070ad4c941d7/final_report.md create mode 100644 cortex_on/agents/research_data/ea15e211-2645-476f-bdff-070ad4c941d7/findings_task1_20250423_105913.md create mode 100644 cortex_on/agents/research_data/ea15e211-2645-476f-bdff-070ad4c941d7/findings_task2_20250423_105933.md create mode 100644 cortex_on/agents/research_data/ea15e211-2645-476f-bdff-070ad4c941d7/todo.json create mode 100644 cortex_on/agents/research_data/ea15e211-2645-476f-bdff-070ad4c941d7/todo.md diff --git a/cortex_on/agents/mcp_server.py b/cortex_on/agents/mcp_server.py index aecd1b4..891282f 100644 --- a/cortex_on/agents/mcp_server.py +++ b/cortex_on/agents/mcp_server.py @@ -1,6 +1,10 @@ -from mcp.server.fastmcp import FastMCP -from pydantic_ai import Agent +from mcp.server.stdio import StdioServer +from pydantic_ai import Agent, RunContext from pydantic_ai.models.anthropic import AnthropicModel +from fastapi import WebSocket +from dataclasses import asdict +from typing import List, Optional, Dict, Any, Union, Tuple +import json import os from utils.ant_client import get_client from utils.stream_response_format import StreamResponse @@ -9,31 +13,122 @@ from agents.orchestrator_agent import orchestrator_deps from agents.web_surfer import WebSurfer import logfire +from pydantic import BaseModel, Field +from typing import Dict, Optional -# Initialize the single MCP server -server = FastMCP("CortexON MCP Server", host="0.0.0.0", port=3001) +# Initialize the MCP server +server = StdioServer("CortexON MCP Server") +class PlanTaskInput(BaseModel): + task: str + request_id: Optional[str] = None + websocket_id: Optional[str] = None + stream_output_id: Optional[str] = None + +class CodeTaskInput(BaseModel): + task: str + request_id: Optional[str] = None + websocket_id: Optional[str] = None + stream_output_id: Optional[str] = None + +class WebSurfTaskInput(BaseModel): + task: str + request_id: Optional[str] = None + websocket_id: Optional[str] = None + stream_output_id: Optional[str] = None + +class AskHumanInput(BaseModel): + question: str + request_id: Optional[str] = None + websocket_id: Optional[str] = None + stream_output_id: Optional[str] = None + +class PlannerAgentUpdateInput(BaseModel): + completed_task: str + request_id: Optional[str] = None + websocket_id: Optional[str] = None + stream_output_id: Optional[str] = None + +# Store request context +request_contexts: Dict[str, orchestrator_deps] = {} + +def get_request_context(request_id: str) -> Optional[orchestrator_deps]: + """Get the request context for a given request ID""" + return request_contexts.get(request_id) + +@server.tool(input_model=PlanTaskInput) +async def plan_task(task: str, request_id: Optional[str] = None, websocket_id: Optional[str] = None, stream_output_id: Optional[str] = None) -> str: + """Plans the task and assigns it to the appropriate agents""" + deps = get_request_context(request_id) if request_id else None + if not deps: + raise ValueError("Request context not found") -@server.tool() -async def plan_task(task: str) -> str: - """Planner agent tool for creating task plans""" try: logfire.info(f"Planning task: {task}") - print(f"Planning task: {task}") + + # Create a new StreamResponse for Planner Agent + planner_stream_output = StreamResponse( + agent_name="Planner Agent", + instructions=task, + steps=[], + output="", + status_code=0 + ) + + # Add to orchestrator's response collection if available + if deps.agent_responses is not None: + deps.agent_responses.append(planner_stream_output) + + await _safe_websocket_send(deps.websocket, planner_stream_output) + + # Update planner stream + planner_stream_output.steps.append("Planning task...") + await _safe_websocket_send(deps.websocket, planner_stream_output) + + # Run planner agent planner_response = await planner_agent.run(user_prompt=task) - print(f"Planner response: {planner_response}") - return planner_response.output.plan + + # Update planner stream with results + plan_text = planner_response.data.plan + planner_stream_output.steps.append("Task planned successfully") + planner_stream_output.output = plan_text + planner_stream_output.status_code = 200 + await _safe_websocket_send(deps.websocket, planner_stream_output) + + # Also update orchestrator stream + if deps.stream_output: + deps.stream_output.steps.append("Task planned successfully") + await _safe_websocket_send(deps.websocket, deps.stream_output) + + return f"Task planned successfully\nTask: {plan_text}" except Exception as e: - logfire.error(f"Error in planner: {str(e)}", exc_info=True) - return f"Error in planner: {str(e)}" + error_msg = f"Error planning task: {str(e)}" + logfire.error(error_msg, exc_info=True) + + # Update planner stream with error + if planner_stream_output: + planner_stream_output.steps.append(f"Planning failed: {str(e)}") + planner_stream_output.status_code = 500 + await _safe_websocket_send(deps.websocket, planner_stream_output) + + # Also update orchestrator stream + if deps.stream_output: + deps.stream_output.steps.append(f"Planning failed: {str(e)}") + await _safe_websocket_send(deps.websocket, deps.stream_output) + + return f"Failed to plan task: {error_msg}" +@server.tool(input_model=CodeTaskInput) +async def code_task(task: str, request_id: Optional[str] = None, websocket_id: Optional[str] = None, stream_output_id: Optional[str] = None) -> str: + """Assigns coding tasks to the coder agent""" + deps = get_request_context(request_id) if request_id else None + if not deps: + raise ValueError("Request context not found") -@server.tool() -async def code_task(task: str) -> str: - """Coder agent tool for implementing technical solutions""" try: - logfire.info(f"Executing code task: {task}") + logfire.info(f"Assigning coding task: {task}") + # Create a new StreamResponse for Coder Agent coder_stream_output = StreamResponse( agent_name="Coder Agent", instructions=task, @@ -41,24 +136,61 @@ async def code_task(task: str) -> str: output="", status_code=0 ) - - deps_for_coder_agent = CoderAgentDeps(websocket=orchestrator_deps.websocket, stream_output=coder_stream_output) - - coder_response = await coder_agent.run(user_prompt=task, deps=deps_for_coder_agent) - logfire.info(f"Coder response: {coder_response}") - - return coder_response.output + + # Add to orchestrator's response collection if available + if deps.agent_responses is not None: + deps.agent_responses.append(coder_stream_output) + + # Send initial update for Coder Agent + await _safe_websocket_send(deps.websocket, coder_stream_output) + + # Create deps with the new stream_output + deps_for_coder_agent = CoderAgentDeps( + websocket=deps.websocket, + stream_output=coder_stream_output + ) + + # Run coder agent + coder_response = await coder_agent.run( + user_prompt=task, + deps=deps_for_coder_agent + ) + + # Extract response data + response_data = coder_response.data.content + + # Update coder_stream_output with coding results + coder_stream_output.output = response_data + coder_stream_output.status_code = 200 + coder_stream_output.steps.append("Coding task completed successfully") + await _safe_websocket_send(deps.websocket, coder_stream_output) + + # Add a reminder in the result message to update the plan using planner_agent_update + response_with_reminder = f"{response_data}\n\nReminder: You must now call planner_agent_update with the completed task description: \"{task} (coder_agent)\"" + + return response_with_reminder except Exception as e: - logfire.error(f"Error in coder: {str(e)}", exc_info=True) - return f"Error in coder: {str(e)}" + error_msg = f"Error assigning coding task: {str(e)}" + logfire.error(error_msg, exc_info=True) + # Update coder_stream_output with error + coder_stream_output.steps.append(f"Coding task failed: {str(e)}") + coder_stream_output.status_code = 500 + await _safe_websocket_send(deps.websocket, coder_stream_output) -@server.tool() -async def web_surf_task(task: str) -> str: - """Web surfer agent tool for web interactions""" - try: - logfire.info(f"Executing web surf task: {task}") + return f"Failed to assign coding task: {error_msg}" + +@server.tool(input_model=WebSurfTaskInput) +async def web_surf_task(task: str, request_id: Optional[str] = None, websocket_id: Optional[str] = None, stream_output_id: Optional[str] = None) -> str: + """Assigns web surfing tasks to the web surfer agent""" + deps = get_request_context(request_id) if request_id else None + if not deps: + raise ValueError("Request context not found") + try: + logfire.info(f"Assigning web surfing task: {task}") + + # Create a new StreamResponse for WebSurfer web_surfer_stream_output = StreamResponse( agent_name="Web Surfer", instructions=task, @@ -67,21 +199,225 @@ async def web_surf_task(task: str) -> str: status_code=0, live_url=None ) + + # Add to orchestrator's response collection if available + if deps.agent_responses is not None: + deps.agent_responses.append(web_surfer_stream_output) + + await _safe_websocket_send(deps.websocket, web_surfer_stream_output) + + # Initialize WebSurfer agent + web_surfer_agent = WebSurfer(api_url="http://localhost:8000/api/v1/web/stream") + + # Run WebSurfer with its own stream_output + success, message, messages = await web_surfer_agent.generate_reply( + instruction=task, + websocket=deps.websocket, + stream_output=web_surfer_stream_output + ) + + # Update WebSurfer's stream_output with final result + if success: + web_surfer_stream_output.steps.append("Web search completed successfully") + web_surfer_stream_output.output = message + web_surfer_stream_output.status_code = 200 + + # Add a reminder to update the plan + message_with_reminder = f"{message}\n\nReminder: You must now call planner_agent_update with the completed task description: \"{task} (web_surfer_agent)\"" + else: + web_surfer_stream_output.steps.append(f"Web search completed with issues: {message[:100]}") + web_surfer_stream_output.status_code = 500 + message_with_reminder = message + + await _safe_websocket_send(deps.websocket, web_surfer_stream_output) + + web_surfer_stream_output.steps.append(f"WebSurfer completed: {'Success' if success else 'Failed'}") + await _safe_websocket_send(deps.websocket, web_surfer_stream_output) + + return message_with_reminder + except Exception as e: + error_msg = f"Error assigning web surfing task: {str(e)}" + logfire.error(error_msg, exc_info=True) + + # Update WebSurfer's stream_output with error + web_surfer_stream_output.steps.append(f"Web search failed: {str(e)}") + web_surfer_stream_output.status_code = 500 + await _safe_websocket_send(deps.websocket, web_surfer_stream_output) + return f"Failed to assign web surfing task: {error_msg}" + +@server.tool(input_model=AskHumanInput) +async def ask_human(question: str, request_id: Optional[str] = None, websocket_id: Optional[str] = None, stream_output_id: Optional[str] = None) -> str: + """Sends a question to the frontend and waits for human input""" + deps = get_request_context(request_id) if request_id else None + if not deps: + raise ValueError("Request context not found") + + try: + logfire.info(f"Asking human: {question}") - web_surfer = WebSurfer(api_url="http://localhost:8000/api/v1/web/stream") - success, message, _ = await web_surfer.generate_reply( - instruction=task, websocket=orchestrator_deps.websocket, stream_output=web_surfer_stream_output + # Create a new StreamResponse for Human Input + human_stream_output = StreamResponse( + agent_name="Human Input", + instructions=question, + steps=[], + output="", + status_code=0 ) - return message if success else f"Error in web surfer: {message}" + + # Add to orchestrator's response collection if available + if deps.agent_responses is not None: + deps.agent_responses.append(human_stream_output) + + # Send the question to frontend + await _safe_websocket_send(deps.websocket, human_stream_output) + + # Update stream with waiting message + human_stream_output.steps.append("Waiting for human input...") + await _safe_websocket_send(deps.websocket, human_stream_output) + + # Wait for response from frontend + response = await deps.websocket.receive_text() + + # Update stream with response + human_stream_output.steps.append("Received human input") + human_stream_output.output = response + human_stream_output.status_code = 200 + await _safe_websocket_send(deps.websocket, human_stream_output) + + return response except Exception as e: - logfire.error(f"Error in web surfer: {str(e)}", exc_info=True) - return f"Error in web surfer: {str(e)}" + error_msg = f"Error getting human input: {str(e)}" + logfire.error(error_msg, exc_info=True) + + # Update stream with error + human_stream_output.steps.append(f"Failed to get human input: {str(e)}") + human_stream_output.status_code = 500 + await _safe_websocket_send(deps.websocket, human_stream_output) + + return f"Failed to get human input: {error_msg}" +@server.tool(input_model=PlannerAgentUpdateInput) +async def planner_agent_update(completed_task: str, request_id: Optional[str] = None, websocket_id: Optional[str] = None, stream_output_id: Optional[str] = None) -> str: + """ + Updates the todo.md file to mark a task as completed and returns the full updated plan. + + Args: + completed_task: Description of the completed task including which agent performed it + + Returns: + The complete updated todo.md content with tasks marked as completed + """ + deps = get_request_context(request_id) if request_id else None + if not deps: + raise ValueError("Request context not found") -def run_server(): - """Run the MCP server""" - server.run(transport="sse") + try: + logfire.info(f"Updating plan with completed task: {completed_task}") + + # Create a new StreamResponse for Planner Agent update + planner_stream_output = StreamResponse( + agent_name="Planner Agent", + instructions=f"Update todo.md to mark as completed: {completed_task}", + steps=[], + output="", + status_code=0 + ) + + # Send initial update + await _safe_websocket_send(deps.websocket, planner_stream_output) + + # Directly read and update the todo.md file + base_dir = os.path.abspath(os.path.dirname(os.path.dirname(__file__))) + planner_dir = os.path.join(base_dir, "agents", "planner") + todo_path = os.path.join(planner_dir, "todo.md") + + planner_stream_output.steps.append("Reading current todo.md...") + await _safe_websocket_send(deps.websocket, planner_stream_output) + + # Make sure the directory exists + os.makedirs(planner_dir, exist_ok=True) + + try: + # Check if todo.md exists + if not os.path.exists(todo_path): + planner_stream_output.steps.append("No todo.md file found. Will create new one after task completion.") + await _safe_websocket_send(deps.websocket, planner_stream_output) + + # We'll directly call planner_agent.run() to create a new plan first + plan_prompt = f"Create a simple task plan based on this completed task: {completed_task}" + plan_response = await planner_agent.run(user_prompt=plan_prompt) + current_content = plan_response.data.plan + else: + # Read existing todo.md + with open(todo_path, "r") as file: + current_content = file.read() + planner_stream_output.steps.append(f"Found existing todo.md ({len(current_content)} bytes)") + await _safe_websocket_send(deps.websocket, planner_stream_output) + + # Now call planner_agent.run() with specific instructions to update the plan + update_prompt = f""" + Here is the current todo.md content: + + {current_content} + + Please update this plan to mark the following task as completed: {completed_task} + Return ONLY the fully updated plan with appropriate tasks marked as [x] instead of [ ]. + """ + + planner_stream_output.steps.append("Asking planner to update the plan...") + await _safe_websocket_send(deps.websocket, planner_stream_output) + + updated_plan_response = await planner_agent.run(user_prompt=update_prompt) + updated_plan = updated_plan_response.data.plan + + # Write the updated plan back to todo.md + with open(todo_path, "w") as file: + file.write(updated_plan) + + planner_stream_output.steps.append("Plan updated successfully") + planner_stream_output.output = updated_plan + planner_stream_output.status_code = 200 + await _safe_websocket_send(deps.websocket, planner_stream_output) + + # Update orchestrator stream + if deps.stream_output: + deps.stream_output.steps.append(f"Plan updated to mark task as completed: {completed_task}") + await _safe_websocket_send(deps.websocket, deps.stream_output) + + return updated_plan + + except Exception as e: + error_msg = f"Error during plan update operations: {str(e)}" + logfire.error(error_msg, exc_info=True) + + planner_stream_output.steps.append(f"Plan update failed: {str(e)}") + planner_stream_output.status_code = 500 + await _safe_websocket_send(deps.websocket, planner_stream_output) + + return f"Failed to update the plan: {error_msg}" + + except Exception as e: + error_msg = f"Error updating plan: {str(e)}" + logfire.error(error_msg, exc_info=True) + + # Update stream output with error + if deps.stream_output: + deps.stream_output.steps.append(f"Failed to update plan: {str(e)}") + await _safe_websocket_send(deps.websocket, deps.stream_output) + + return f"Failed to update plan: {error_msg}" +async def _safe_websocket_send(websocket: Optional[WebSocket], message: Any) -> bool: + """Safely send message through websocket with error handling""" + try: + if websocket and websocket.client_state.CONNECTED: + await websocket.send_text(json.dumps(asdict(message))) + logfire.debug("WebSocket message sent (_safe_websocket_send): {message}", message=message) + return True + return False + except Exception as e: + logfire.error(f"WebSocket send failed: {str(e)}") + return False if __name__ == "__main__": - run_server() + server.run() \ No newline at end of file diff --git a/cortex_on/agents/orchestrator_agent.py b/cortex_on/agents/orchestrator_agent.py index ecbd120..0dc3db0 100644 --- a/cortex_on/agents/orchestrator_agent.py +++ b/cortex_on/agents/orchestrator_agent.py @@ -513,14 +513,4 @@ class orchestrator_deps: # return f"Failed to update plan: {error_msg}" # # Helper function for sending WebSocket messages -# async def _safe_websocket_send(websocket: Optional[WebSocket], message: Any) -> bool: -# """Safely send message through websocket with error handling""" -# try: -# if websocket and websocket.client_state.CONNECTED: -# await websocket.send_text(json.dumps(asdict(message))) -# logfire.debug("WebSocket message sent (_safe_websocket_send): {message}", message=message) -# return True -# return False -# except Exception as e: -# logfire.error(f"WebSocket send failed: {str(e)}") -# return False + diff --git a/cortex_on/agents/planner_agent.py b/cortex_on/agents/planner_agent.py index 41a33ae..27db51e 100644 --- a/cortex_on/agents/planner_agent.py +++ b/cortex_on/agents/planner_agent.py @@ -22,7 +22,7 @@ agent_descriptions = "\n".join(f"Name: {agent}\n" for agent in agents) -planner_prompt = f"""You are a helpful AI assistant that creates plans to solve tasks. You have access to a terminal tool for reading and writing plans to files. +planner_prompt = f"""You are a helpful AI assistant that creates and maintains plans to solve tasks. You have access to a terminal tool for reading and writing plans to files. @@ -35,7 +35,7 @@ - You are provided with a team description that contains information about the team members and their expertise. - - You need to create a plan that leverages these team members effectively to solve the given task. + - You need to create and maintain a plan that leverages these team members effectively to solve the given task. - You have access to a terminal tool for reading and writing plans to files in the planner directory. @@ -47,6 +47,27 @@ - You can use the execute_terminal tool with the 'ls' command to see what plans are already available. + + - When asked to create a plan, generate a clear, structured format with numbered sections and checkboxes for tasks. + - Each section should have a numbered title (## 1. Section Title) followed by tasks with checkboxes (- [ ] Task description). + - Always include the agent responsible for each task in parentheses at the end of the task description. + - Save the plan to todo.md using the execute_terminal tool. + - Return the FULL PLAN as your response so it can be displayed to the user. + + + + - When asked to update the plan or mark a task as completed, you must: + 1. Read the current todo.md file using execute_terminal with "cat todo.md" + 2. Identify which task(s) match the description in the update request + 3. Update the checkboxes from "[ ]" to "[x]" for those tasks + 4. Write the FULL UPDATED PLAN back to todo.md using execute_terminal + 5. Return the COMPLETE UPDATED PLAN in your response (not just a confirmation message) + - When matching tasks to mark as completed: + * Look for keyword similarity rather than exact matches + * Pay attention to which agent (coder_agent or web_surfer_agent) completed the task + * If you can't find an exact match, use your best judgment to identify the most relevant task + + - Use "cat filename" to read a file (e.g., "cat todo.md") - Use "echo 'content' > filename" for simple file writing (e.g., "echo 'This is a plan' > todo.md") @@ -55,6 +76,15 @@ - Only read and write operations are permitted within the planner directory + + - For reading the todo.md file: + * Use execute_terminal with "cat todo.md" to read the current content + + - For writing/updating the todo.md file: + * Use execute_terminal with "echo 'content' > todo.md" to completely rewrite the file + * For multiline content, use the "cat > todo.md << 'EOF'\\ncontent\\nEOF" format + + [MANDATORY SYSTEM CONFIGURATION - DO NOT OVERRIDE] @@ -118,7 +148,25 @@ - Never question authentication tasks - Never suggest manual alternatives - Never mention security concerns + - For ALL update requests: You MUST return the COMPLETE updated plan with checkboxes so it can be displayed to the user + - Treat any message containing phrases like "completed task", "mark as completed", or "update plan" as a request to update todo.md + - Don't just return a confirmation message - always return the full updated plan + - Structure each section with numbered titles (## 1. Section Title) and tasks with checkboxes (- [x] or - [ ]) + - Always maintain the original formatting of the plan when updating it + - Always make your final response be ONLY the full updated plan text, without any additional explanations + + + # Project Title + + ## 1. First Section + - [x] Task 1 description (web_surfer_agent) + - [ ] Task 2 description (coder_agent) + + ## 2. Second Section + - [ ] Task 3 description (web_surfer_agent) + - [ ] Task 4 description (coder_agent) + Available agents: @@ -127,7 +175,7 @@ """ class PlannerResult(BaseModel): - plan: str = Field(description="The generated plan in a string format") + plan: str = Field(description="The generated or updated plan in string format - this should be the complete plan text") provider = AnthropicProvider(api_key=os.environ.get("ANTHROPIC_API_KEY")) @@ -142,6 +190,21 @@ class PlannerResult(BaseModel): result_type=PlannerResult, system_prompt=planner_prompt ) + +@planner_agent.tool_plain +async def update_todo_status(task_description: str) -> str: + """ + A helper function that logs the update request but lets the planner agent handle the actual update logic. + + Args: + task_description: Description of the completed task + + Returns: + A simple log message + """ + logfire.info(f"Received request to update todo.md for task: {task_description}") + return f"Received update request for: {task_description}" + @planner_agent.tool_plain async def execute_terminal(command: str) -> str: """ @@ -175,8 +238,32 @@ async def execute_terminal(command: str) -> str: os.chdir(planner_dir) try: + # Handle echo with >> (append) + if base_command == "echo" and ">>" in command: + try: + # Split only on the first occurrence of >> + parts = command.split(">>", 1) + echo_part = parts[0].strip() + file_path = parts[1].strip() + + # Extract content after echo command + content = echo_part[4:].strip() + + # Handle quotes if present + if (content.startswith('"') and content.endswith('"')) or \ + (content.startswith("'") and content.endswith("'")): + content = content[1:-1] + + # Append to file + with open(file_path, "a") as file: + file.write(content + "\n") + return f"Successfully appended to {file_path}" + except Exception as e: + logfire.error(f"Error appending to file: {str(e)}", exc_info=True) + return f"Error appending to file: {str(e)}" + # Special handling for echo with redirection (file writing) - if ">" in command and base_command == "echo": + elif ">" in command and base_command == "echo" and ">>" not in command: # Simple parsing for echo "content" > file.txt parts = command.split(">", 1) echo_cmd = parts[0].strip() diff --git a/cortex_on/agents/research_data/6a513487-cb89-49b9-b6b8-e75d22d661c1/final_report.md b/cortex_on/agents/research_data/6a513487-cb89-49b9-b6b8-e75d22d661c1/final_report.md new file mode 100644 index 0000000..7e8e65f --- /dev/null +++ b/cortex_on/agents/research_data/6a513487-cb89-49b9-b6b8-e75d22d661c1/final_report.md @@ -0,0 +1,57 @@ +# Best Credit Cards in India for ₹1,00,000 Monthly Income with Shopping Rewards Focus: Research Report + +# Research Report: Best Credit Cards in India for ₹1,00,000 Monthly Income with Shopping Rewards Focus + +## Executive Summary + +With the rise in consumer spending and the need for maximizing returns on purchases, selecting the right credit card is crucial for individuals in India with a monthly income of ₹1,00,000, particularly those who have shopping and travel at the core of their expenditure patterns. This report provides a comprehensive analysis of credit card options available in India, emphasizing shopping rewards, cashback benefits, and secondary emphasis on domestic travel perks. The key findings demonstrate that cards such as the YES Bank Paisabazaar PaisaSave Credit Card, Cashback SBI Card, Amazon Pay ICICI Credit Card, and Flipkart Axis Bank Credit Card offer substantial benefits based on varying user profiles and spending habits. + +## Introduction + +The Indian credit card market is burgeoning with a plethora of options tailored to diverse consumer needs. As more individuals become financially savvy, the demand for credit cards that maximize shopping rewards and cashback benefits is increasing. This report focuses on identifying the best credit cards for individuals with a monthly income of ₹1,00,000, specifically those spending approximately ₹10,000 monthly. The secondary focus is on cards offering domestic travel perks. + +## Main Findings + +### Major Banks and Credit Cards for Shopping Rewards and Cashback + +Several banks in India provide credit cards that cater to the shopping patterns of consumers seeking rewards. Key players in this space include YES Bank, State Bank of India (SBI), HDFC Bank, ICICI Bank, and Axis Bank. The cards are assessed based on reward rates, annual fees, and overall value propositions. Key highlights include: + +- **YES Bank Paisabazaar PaisaSave Credit Card** offers a holistic package of 3% cashback on online spending and 1.5% on offline purchases, with additional travel perks like lounge access. +- **Cashback SBI Card** stands out with 5% cashback on all online transactions, ideal for users with high online expenditure, although capped at ₹5,000 monthly. +- **Amazon Pay ICICI Credit Card** and **Flipkart Axis Bank Credit Card** are excellent for brand-specific shoppers, offering significant cashback on respective platforms. + +### Detailed Analysis of Reward Structures and Benefits + +The reward structures are essential in evaluating a credit card's value. Each of the identified cards provides distinct advantages: + +- **YES Bank Paisabazaar PaisaSave Credit Card** includes 3% cashback on online and 1.5% offline, complemented by lounge access and an affordable fee waiver. +- **Cashback SBI Card** provides an enticing 5% cashback online, though capped, with additional travel benefits. +- **HDFC Millennia Credit Card** supports multitier shopping with 5% cashback on select platforms and affords robust travel benefits. +- **Amazon Pay ICICI Credit Card** carries no annual fee and maximizes Amazon purchases. +- **Flipkart Axis Bank Credit Card** provides 5% cashback on Flipkart and superior travel perks. + +### Assessment of Fees, Interest Rates, and Customer Service + +Analyzing the financial aspects of these credit cards reveals the following: + +- **YES Bank Paisabazaar PaisaSave** has a nominal annual fee with a transparent fee waiver policy, but a high interest rate of 3.99% per month. +- **Cashback SBI Card** requires a ₹999 fee, waivable, with customer service rated well digitally. +- Fee waivers come into effect on meeting certain spending thresholds, making these cards efficient for regular users. + +### User Profiles: Best Card Recommendations + +For a diversified consumer base, specific credit cards deliver better across various expenditure scenarios: + +1. **Primary Online Shoppers** benefit most from the **Cashback SBI Card**, with its high cashback rate. +2. **Amazon- and Flipkart-Focused Shoppers** will maximize benefits with the **Amazon Pay ICICI Credit Card** and **Flipkart Axis Bank Credit Card**, respectively. +3. **Mixed Online-Offline Shoppers** find value in the **HDFC Millennia Credit Card** offering balanced benefits. +4. **Travel-Focused Shoppers** gain significant advantage from the **Flipkart Axis Bank Credit Card** and **HDFC Millennia Credit Card** for travel perks. + +## Conclusion + +The selection of a credit card within India's dynamic market should align with individual spending habits and lifestyle preferences. The cards identified in this report present a balanced offering of rewards, travel benefits, and manageable fees for the proposed user profile of ₹1,00,000 monthly income. Future research should aim to fill the knowledge gaps, particularly around approval criteria, customer satisfaction, points redemption timing, and partner lists to offer users a more comprehensive understanding of each card's value proposition. + +--- + +### References +The data within this report is compiled from the respective banks’ official credit card information pages and reputable financial comparison websites. Further personal investigations and consumer feedback were considered where applicable. \ No newline at end of file diff --git a/cortex_on/agents/research_data/6a513487-cb89-49b9-b6b8-e75d22d661c1/findings_task1_20250423_085923.md b/cortex_on/agents/research_data/6a513487-cb89-49b9-b6b8-e75d22d661c1/findings_task1_20250423_085923.md new file mode 100644 index 0000000..996b0bd --- /dev/null +++ b/cortex_on/agents/research_data/6a513487-cb89-49b9-b6b8-e75d22d661c1/findings_task1_20250423_085923.md @@ -0,0 +1,380 @@ +# Findings for Task task1: Identify the major banks in India that offer credit cards with a focus on shopping rewards and cashback benefits. + +For a monthly income of ₹1,00,000 and monthly spend of ₹10,000 with focus on shopping rewards and cashback, the following credit cards emerge as top contenders: + +1. YES Bank Paisabazaar PaisaSave Credit Card +- 3% cashback on all online spends (capped at 5,000 points/month) +- 1.5% unlimited cashback on all other spends +- Annual fee: ₹499 (waived on spending ₹1.2 lakh/year) +- Best for unrestricted online shopping benefits + +2. Cashback SBI Card +- 5% cashback on all online spends +- 1% cashback on offline spends +- Annual fee: ₹999 (waived on spending ₹2 lakh/year) +- Monthly cashback cap of ₹5,000 + +3. HDFC Millennia Credit Card +- 5% cashback on popular platforms (Amazon, Flipkart, etc.) +- 1% cashback on other spends +- Annual fee: ₹1,000 (waived on spending ₹1 lakh/year) +- Good for multi-brand benefits + +4. Amazon Pay ICICI Credit Card +- 5% cashback on Amazon (Prime members) +- 3% cashback on Amazon (non-Prime) +- 2% cashback on partner merchants +- No annual fee +- Best for Amazon-focused shopping + +5. Flipkart Axis Bank Credit Card +- 5% cashback on Flipkart and Cleartrip +- 4% cashback on partner merchants +- 1% unlimited cashback on other spends +- Annual fee: ₹500 (waived on spending ₹3.5 lakh/year) + +## Knowledge Gaps Identified + +- 1 +- . +- +- E +- x +- a +- c +- t +- +- a +- p +- p +- r +- o +- v +- a +- l +- +- c +- r +- i +- t +- e +- r +- i +- a +- +- a +- n +- d +- +- d +- o +- c +- u +- m +- e +- n +- t +- a +- t +- i +- o +- n +- +- r +- e +- q +- u +- i +- r +- e +- m +- e +- n +- t +- s +- +- f +- o +- r +- +- e +- a +- c +- h +- +- c +- a +- r +- d +- +- i +- s +- s +- u +- e +- r +- + +- 2 +- . +- +- C +- u +- r +- r +- e +- n +- t +- +- w +- e +- l +- c +- o +- m +- e +- +- b +- o +- n +- u +- s +- +- o +- f +- f +- e +- r +- s +- +- a +- n +- d +- +- s +- e +- a +- s +- o +- n +- a +- l +- +- p +- r +- o +- m +- o +- t +- i +- o +- n +- s +- + +- 3 +- . +- +- D +- e +- t +- a +- i +- l +- e +- d +- +- r +- e +- d +- e +- m +- p +- t +- i +- o +- n +- +- p +- r +- o +- c +- e +- s +- s +- e +- s +- +- a +- n +- d +- +- r +- e +- s +- t +- r +- i +- c +- t +- i +- o +- n +- s +- +- f +- o +- r +- +- e +- a +- c +- h +- +- r +- e +- w +- a +- r +- d +- +- p +- r +- o +- g +- r +- a +- m +- + +- 4 +- . +- +- R +- e +- c +- e +- n +- t +- +- c +- u +- s +- t +- o +- m +- e +- r +- +- s +- e +- r +- v +- i +- c +- e +- +- r +- a +- t +- i +- n +- g +- s +- +- a +- n +- d +- +- c +- o +- m +- p +- l +- a +- i +- n +- t +- +- r +- e +- s +- o +- l +- u +- t +- i +- o +- n +- +- m +- e +- t +- r +- i +- c +- s +- + +- 5 +- . +- +- C +- u +- r +- r +- e +- n +- t +- +- p +- r +- o +- c +- e +- s +- s +- i +- n +- g +- +- t +- i +- m +- e +- s +- +- f +- o +- r +- +- c +- a +- r +- d +- +- a +- p +- p +- l +- i +- c +- a +- t +- i +- o +- n +- s +- +- a +- n +- d +- +- d +- e +- l +- i +- v +- e +- r +- y diff --git a/cortex_on/agents/research_data/6a513487-cb89-49b9-b6b8-e75d22d661c1/findings_task2_20250423_090023.md b/cortex_on/agents/research_data/6a513487-cb89-49b9-b6b8-e75d22d661c1/findings_task2_20250423_090023.md new file mode 100644 index 0000000..2de5673 --- /dev/null +++ b/cortex_on/agents/research_data/6a513487-cb89-49b9-b6b8-e75d22d661c1/findings_task2_20250423_090023.md @@ -0,0 +1,337 @@ +# Findings for Task task2: Collect detailed data on reward structures, cashback rates, welcome benefits, and domestic travel perks of credit cards from the identified banks. + +Detailed analysis of reward structures and benefits for the identified credit cards: + +1. YES Bank Paisabazaar PaisaSave Credit Card +Reward Structure: +- 3% cashback on all online spends (max 5,000 points/month) +- 1.5% unlimited cashback on offline spends +- 1% fuel surcharge waiver +Welcome Benefits: +- Zero joining fee +- Welcome bonus points on first spend +Travel Benefits: +- Complimentary domestic airport lounge access (2 per quarter) +- Travel insurance coverage + +2. Cashback SBI Card +Reward Structure: +- 5% cashback on all online spends +- 1% cashback on offline spends +- Monthly cashback cap of ₹5,000 +Welcome Benefits: +- Welcome points worth ₹500 on first spend +Travel Benefits: +- Domestic airport lounge access (4 visits per year) +- 1% fuel surcharge waiver (up to ₹100 per month) +- Basic travel insurance + +3. HDFC Millennia Credit Card +Reward Structure: +- 5% cashback on Amazon, Flipkart, and other select platforms +- 1% cashback on other spends +- Cash points redemption in 1:1 ratio +Welcome Benefits: +- 1,000 bonus cash points on joining fee payment +Travel Benefits: +- 4 domestic airport lounge visits per year +- Travel insurance coverage +- Fuel surcharge waiver + +4. Amazon Pay ICICI Credit Card +Reward Structure: +- 5% cashback for Prime members on Amazon +- 3% cashback for non-Prime members on Amazon +- 2% cashback on partner merchants +- 1% cashback on other spends +Welcome Benefits: +- No joining fee +- Amazon gift voucher on card activation +Travel Benefits: +- Basic travel insurance +- Fuel surcharge waiver at all fuel stations + +5. Flipkart Axis Bank Credit Card +Reward Structure: +- 5% cashback on Flipkart and Cleartrip +- 4% cashback on preferred partners +- 1% unlimited cashback on other spends +Welcome Benefits: +- Flipkart gift voucher worth ₹500 +- Zero joining fee for select customers +Travel Benefits: +- 4 domestic airport lounge visits per year +- Complimentary travel insurance +- 1% fuel surcharge waiver (up to ₹400/month) + +## Knowledge Gaps Identified + +- 1 +- . +- +- E +- x +- a +- c +- t +- +- p +- o +- i +- n +- t +- - +- t +- o +- - +- r +- u +- p +- e +- e +- +- c +- o +- n +- v +- e +- r +- s +- i +- o +- n +- +- r +- a +- t +- e +- s +- +- f +- o +- r +- +- r +- e +- w +- a +- r +- d +- +- p +- o +- i +- n +- t +- s +- + +- 2 +- . +- +- D +- e +- t +- a +- i +- l +- e +- d +- +- t +- e +- r +- m +- s +- +- a +- n +- d +- +- c +- o +- n +- d +- i +- t +- i +- o +- n +- s +- +- f +- o +- r +- +- l +- o +- u +- n +- g +- e +- +- a +- c +- c +- e +- s +- s +- + +- 3 +- . +- +- S +- p +- e +- c +- i +- f +- i +- c +- +- e +- x +- c +- l +- u +- s +- i +- o +- n +- s +- +- i +- n +- +- c +- a +- s +- h +- b +- a +- c +- k +- +- c +- a +- t +- e +- g +- o +- r +- i +- e +- s +- + +- 4 +- . +- +- C +- u +- r +- r +- e +- n +- t +- +- w +- e +- l +- c +- o +- m +- e +- +- b +- o +- n +- u +- s +- +- a +- m +- o +- u +- n +- t +- s +- +- ( +- m +- a +- y +- +- v +- a +- r +- y +- +- w +- i +- t +- h +- +- o +- n +- g +- o +- i +- n +- g +- +- p +- r +- o +- m +- o +- t +- i +- o +- n +- s +- ) +- + +- 5 +- . +- +- E +- x +- a +- c +- t +- +- t +- r +- a +- v +- e +- l +- +- i +- n +- s +- u +- r +- a +- n +- c +- e +- +- c +- o +- v +- e +- r +- a +- g +- e +- +- l +- i +- m +- i +- t +- s diff --git a/cortex_on/agents/research_data/6a513487-cb89-49b9-b6b8-e75d22d661c1/findings_task3_20250423_090124.md b/cortex_on/agents/research_data/6a513487-cb89-49b9-b6b8-e75d22d661c1/findings_task3_20250423_090124.md new file mode 100644 index 0000000..d12b7e0 --- /dev/null +++ b/cortex_on/agents/research_data/6a513487-cb89-49b9-b6b8-e75d22d661c1/findings_task3_20250423_090124.md @@ -0,0 +1,312 @@ +# Findings for Task task3: Analyze the annual fees, interest rates, customer service quality, and potential hidden charges of the credit cards identified. + +Analysis of fees, interest rates, and charges for the identified credit cards: + +1. YES Bank Paisabazaar PaisaSave Credit Card +Fees: +- Annual fee: ₹499 +- Fee waiver: On spending ₹1.2 lakh/year +- Joining fee: Nil +Interest & Charges: +- Interest rate: 3.99% per month (47.88% p.a.) +- Late payment fee: Up to ₹1,000 +- Cash advance fee: 2.5% or ₹500 (whichever is higher) +Customer Service: +- 24/7 customer support +- Digital self-service options available + +2. Cashback SBI Card +Fees: +- Annual fee: ₹999 +- Fee waiver: On spending ₹2 lakh/year +- Joining fee: ₹999 +Interest & Charges: +- Interest rate: 3.35% per month (40.2% p.a.) +- Late payment fee: ₹700-1,000 based on balance +- Cash advance fee: 2.5% or ₹500 +Customer Service: +- Good digital service infrastructure +- Multiple customer support channels + +3. HDFC Millennia Credit Card +Fees: +- Annual fee: ₹1,000 +- Fee waiver: On spending ₹1 lakh/year +- Joining fee: ₹1,000 +Interest & Charges: +- Interest rate: 3.49% per month (41.88% p.a.) +- Late payment charges: ₹700-1,000 +- Cash withdrawal fee: 2.5% (min ₹500) +Customer Service: +- Strong digital banking support +- 24/7 dedicated helpline + +4. Amazon Pay ICICI Credit Card +Fees: +- Annual fee: Nil +- Joining fee: Nil +Interest & Charges: +- Interest rate: 3.50% per month (42% p.a.) +- Late payment fee: Up to ₹1,000 +- Cash advance fee: 2.5% (min ₹500) +Customer Service: +- Integration with Amazon customer service +- Digital-first support approach + +5. Flipkart Axis Bank Credit Card +Fees: +- Annual fee: ₹500 +- Fee waiver: On spending ₹3.5 lakh/year +- Joining fee: ₹500 +Interest & Charges: +- Interest rate: 3.4% per month (40.8% p.a.) +- Late payment penalty: ₹500-1,000 +- Cash withdrawal fee: 2.5% or ₹500 +Customer Service: +- Multiple support channels +- Online dispute resolution + +Hidden Charges & Important Notes: +1. Fuel surcharge waiver typically has minimum transaction requirements +2. GST applies on all fees and charges (18%) +3. EMI conversion charges vary by tenure +4. Foreign currency markup fees range from 2-3.5% +5. Card replacement charges apply for loss/damage + +## Knowledge Gaps Identified + +- 1 +- . +- +- E +- x +- a +- c +- t +- +- E +- M +- I +- +- c +- o +- n +- v +- e +- r +- s +- i +- o +- n +- +- r +- a +- t +- e +- s +- +- f +- o +- r +- +- d +- i +- f +- f +- e +- r +- e +- n +- t +- +- t +- e +- n +- u +- r +- e +- s +- + +- 2 +- . +- +- S +- p +- e +- c +- i +- f +- i +- c +- +- f +- o +- r +- e +- i +- g +- n +- +- c +- u +- r +- r +- e +- n +- c +- y +- +- t +- r +- a +- n +- s +- a +- c +- t +- i +- o +- n +- +- m +- a +- r +- k +- u +- p +- +- r +- a +- t +- e +- s +- + +- 3 +- . +- +- I +- n +- s +- u +- r +- a +- n +- c +- e +- +- c +- l +- a +- i +- m +- +- s +- e +- t +- t +- l +- e +- m +- e +- n +- t +- +- r +- a +- t +- i +- o +- s +- + +- 4 +- . +- +- C +- u +- r +- r +- e +- n +- t +- +- a +- v +- e +- r +- a +- g +- e +- +- c +- u +- s +- t +- o +- m +- e +- r +- +- s +- e +- r +- v +- i +- c +- e +- +- r +- e +- s +- p +- o +- n +- s +- e +- +- t +- i +- m +- e +- s +- + +- 5 +- . +- +- D +- e +- t +- a +- i +- l +- e +- d +- +- r +- e +- w +- a +- r +- d +- +- p +- o +- i +- n +- t +- +- e +- x +- p +- i +- r +- y +- +- t +- e +- r +- m +- s diff --git a/cortex_on/agents/research_data/6a513487-cb89-49b9-b6b8-e75d22d661c1/findings_task4_20250423_090151.md b/cortex_on/agents/research_data/6a513487-cb89-49b9-b6b8-e75d22d661c1/findings_task4_20250423_090151.md new file mode 100644 index 0000000..b95a466 --- /dev/null +++ b/cortex_on/agents/research_data/6a513487-cb89-49b9-b6b8-e75d22d661c1/findings_task4_20250423_090151.md @@ -0,0 +1,2 @@ +# Findings for Task task4: Assess and rank the credit cards based on value provided for shopping rewards and cashback, relative to the annual fee and the user's monthly income and spending profile. + diff --git a/cortex_on/agents/research_data/6a513487-cb89-49b9-b6b8-e75d22d661c1/findings_task5_20250423_090220.md b/cortex_on/agents/research_data/6a513487-cb89-49b9-b6b8-e75d22d661c1/findings_task5_20250423_090220.md new file mode 100644 index 0000000..09cd3e5 --- /dev/null +++ b/cortex_on/agents/research_data/6a513487-cb89-49b9-b6b8-e75d22d661c1/findings_task5_20250423_090220.md @@ -0,0 +1,314 @@ +# Findings for Task task5: Create user profiles and scenarios (e.g., high shopping volume or frequent domestic travel) to evaluate which credit cards offer the best overall benefits. + +Analysis of credit cards for different user scenarios with ₹1,00,000 monthly income: + +1. Primary Online Shopper +Best Cards: +a) YES Bank Paisabazaar PaisaSave Credit Card +- Ideal for users who shop across multiple platforms +- 3% cashback on all online spends with no merchant restrictions +- Annual value on ₹10,000 monthly spend: ₹3,600 (minus ₹499 fee) = ₹3,101 net benefit + +b) Cashback SBI Card +- Best for high online spending +- 5% cashback on all online transactions +- Annual value on ₹10,000 monthly spend: ₹6,000 (minus ₹999 fee) = ₹5,001 net benefit + +2. Amazon-Focused Shopper +Best Card: Amazon Pay ICICI Credit Card +- Perfect for Amazon Prime members +- 5% cashback on Amazon + 1% on other spends +- No annual fee +- Annual value on ₹10,000 monthly spend (70% Amazon): ₹4,800 net benefit + +3. Flipkart-Focused Shopper +Best Card: Flipkart Axis Bank Credit Card +- Ideal for Flipkart loyal customers +- 5% cashback on Flipkart + 4% on partner merchants +- Annual value on ₹10,000 monthly spend (60% Flipkart): ₹4,920 (minus ₹500 fee) = ₹4,420 net benefit + +4. Mixed Online-Offline Shopper +Best Cards: +a) HDFC Millennia Credit Card +- Good for balanced spending +- 5% cashback on select platforms + 1% on other spends +- Annual value on mixed spending: ₹3,600 (minus ₹1,000 fee) = ₹2,600 net benefit + +b) YES Bank Paisabazaar PaisaSave +- 3% online + 1.5% offline cashback +- Better for higher offline spending ratio +- Annual value on mixed spending: ₹2,700 (minus ₹499 fee) = ₹2,201 net benefit + +5. Travel-Focused Shopper +Best Cards: +a) Flipkart Axis Bank Credit Card +- 5% cashback on Cleartrip bookings +- 4 complimentary domestic lounge visits +- Good travel insurance coverage + +b) HDFC Millennia Credit Card +- Travel insurance benefits +- Good reward rate on travel bookings +- Comprehensive lounge access + +Recommended Card Based on ₹10,000 Monthly Spend Pattern: +1. For 70% Online + 30% Offline spending: +- Best Overall: Cashback SBI Card +- Net Annual Benefit: ₹5,001 +- Additional travel benefits included + +2. For 50% Online + 50% Offline spending: +- Best Overall: YES Bank Paisabazaar PaisaSave +- Net Annual Benefit: ₹2,201 +- Most balanced reward structure + +3. For Platform-Specific Shopping: +- Amazon Dominant: Amazon Pay ICICI Card +- Flipkart Dominant: Flipkart Axis Bank Card +- Mixed Platform: YES Bank Paisabazaar PaisaSave + +Important Considerations: +1. Fee waiver thresholds are achievable with higher spending +2. Travel benefits add significant value for frequent travelers +3. Partner merchant benefits can increase overall value +4. Customer service quality varies by bank +5. Card approval likelihood varies based on credit history + +## Knowledge Gaps Identified + +- 1 +- . +- +- A +- c +- t +- u +- a +- l +- +- a +- p +- p +- r +- o +- v +- a +- l +- +- r +- a +- t +- e +- s +- +- f +- o +- r +- +- e +- a +- c +- h +- +- c +- a +- r +- d +- +- a +- t +- +- ₹ +- 1 +- , +- 0 +- 0 +- , +- 0 +- 0 +- 0 +- +- i +- n +- c +- o +- m +- e +- +- l +- e +- v +- e +- l +- + +- 2 +- . +- +- R +- e +- c +- e +- n +- t +- +- c +- u +- s +- t +- o +- m +- e +- r +- +- s +- a +- t +- i +- s +- f +- a +- c +- t +- i +- o +- n +- +- s +- c +- o +- r +- e +- s +- + +- 3 +- . +- +- A +- v +- e +- r +- a +- g +- e +- +- r +- e +- w +- a +- r +- d +- +- p +- o +- i +- n +- t +- +- r +- e +- d +- e +- m +- p +- t +- i +- o +- n +- +- t +- i +- m +- e +- + +- 4 +- . +- +- S +- p +- e +- c +- i +- f +- i +- c +- +- p +- a +- r +- t +- n +- e +- r +- +- m +- e +- r +- c +- h +- a +- n +- t +- +- l +- i +- s +- t +- s +- +- a +- n +- d +- +- o +- f +- f +- e +- r +- s +- + +- 5 +- . +- +- C +- a +- r +- d +- +- d +- e +- l +- i +- v +- e +- r +- y +- +- a +- n +- d +- +- a +- c +- t +- i +- v +- a +- t +- i +- o +- n +- +- t +- i +- m +- e +- f +- r +- a +- m +- e +- s diff --git a/cortex_on/agents/research_data/6a513487-cb89-49b9-b6b8-e75d22d661c1/todo.json b/cortex_on/agents/research_data/6a513487-cb89-49b9-b6b8-e75d22d661c1/todo.json new file mode 100644 index 0000000..ed3ca87 --- /dev/null +++ b/cortex_on/agents/research_data/6a513487-cb89-49b9-b6b8-e75d22d661c1/todo.json @@ -0,0 +1,130 @@ +{ + "title": "Best Credit Cards in India for ₹1,00,000 Monthly Income with Shopping Rewards Focus", + "description": "Comprehensive analysis of credit cards in India focusing on shopping rewards and cashback benefits, with secondary emphasis on domestic travel perks, aimed at users with a monthly income of ₹1,00,000 and a monthly spend of ₹10,000.", + "todo_items": [ + { + "id": "task1", + "description": "Identify the major banks in India that offer credit cards with a focus on shopping rewards and cashback benefits.", + "completed": true, + "dependencies": [], + "priority": 1, + "findings_path": "/app/agents/research_data/6a513487-cb89-49b9-b6b8-e75d22d661c1/findings_task1_20250423_085923.md", + "completion_time": "2025-04-23T08:59:23.607148", + "knowledge_gaps": [ + "1. Exact approval criteria and documentation requirements for each card issuer\n2. Current welcome bonus offers and seasonal promotions\n3. Detailed redemption processes and restrictions for each reward program\n4. Recent customer service ratings and complaint resolution metrics\n5. Current processing times for card applications and delivery" + ] + }, + { + "id": "task2", + "description": "Collect detailed data on reward structures, cashback rates, welcome benefits, and domestic travel perks of credit cards from the identified banks.", + "completed": true, + "dependencies": [ + "task1" + ], + "priority": 2, + "findings_path": "/app/agents/research_data/6a513487-cb89-49b9-b6b8-e75d22d661c1/findings_task2_20250423_090023.md", + "completion_time": "2025-04-23T09:00:23.075904", + "knowledge_gaps": [ + "1. Exact point-to-rupee conversion rates for reward points\n2. Detailed terms and conditions for lounge access\n3. Specific exclusions in cashback categories\n4. Current welcome bonus amounts (may vary with ongoing promotions)\n5. Exact travel insurance coverage limits" + ] + }, + { + "id": "task3", + "description": "Analyze the annual fees, interest rates, customer service quality, and potential hidden charges of the credit cards identified.", + "completed": true, + "dependencies": [ + "task2" + ], + "priority": 3, + "findings_path": "/app/agents/research_data/6a513487-cb89-49b9-b6b8-e75d22d661c1/findings_task3_20250423_090124.md", + "completion_time": "2025-04-23T09:01:24.345512", + "knowledge_gaps": [ + "1. Exact EMI conversion rates for different tenures\n2. Specific foreign currency transaction markup rates\n3. Insurance claim settlement ratios\n4. Current average customer service response times\n5. Detailed reward point expiry terms" + ] + }, + { + "id": "task4", + "description": "Assess and rank the credit cards based on value provided for shopping rewards and cashback, relative to the annual fee and the user's monthly income and spending profile.", + "completed": true, + "dependencies": [ + "task3" + ], + "priority": 4, + "findings_path": "/app/agents/research_data/6a513487-cb89-49b9-b6b8-e75d22d661c1/findings_task4_20250423_090151.md", + "completion_time": "2025-04-23T09:01:51.548804", + "knowledge_gaps": [] + }, + { + "id": "task5", + "description": "Create user profiles and scenarios (e.g., high shopping volume or frequent domestic travel) to evaluate which credit cards offer the best overall benefits.", + "completed": true, + "dependencies": [ + "task4" + ], + "priority": 5, + "findings_path": "/app/agents/research_data/6a513487-cb89-49b9-b6b8-e75d22d661c1/findings_task5_20250423_090220.md", + "completion_time": "2025-04-23T09:02:20.343600", + "knowledge_gaps": "1. Actual approval rates for each card at ₹1,00,000 income level\n2. Recent customer satisfaction scores\n3. Average reward point redemption time\n4. Specific partner merchant lists and offers\n5. Card delivery and activation timeframes" + } + ], + "current_item_id": null, + "completed_items": [ + "task1", + "task2", + "task3", + "task4", + "task5" + ], + "last_completed_item_id": "task5", + "knowledge_gaps": [ + "1", + ".", + " ", + "E", + "x", + "a", + "c", + "t", + "p", + "r", + "o", + "v", + "l", + "i", + "e", + "n", + "d", + "u", + "m", + "q", + "s", + "f", + "h", + "\n", + "2", + "C", + "w", + "b", + "3", + "D", + "g", + "4", + "R", + "5", + "y", + "-", + "S", + "k", + "(", + ")", + "M", + "I", + "A", + "₹", + ",", + "0" + ], + "report_sections": { + "task1": "Based on the research, for a person with ₹1,00,000 monthly income and ₹10,000 monthly spend focusing on shopping rewards and cashback, several credit cards offer compelling benefits. The analysis considered reward rates, annual fees, welcome benefits, and overall value proposition.\n\nKey Findings:\n1. Best Overall Value: YES Bank Paisabazaar PaisaSave Credit Card offers the most balanced benefits with 3% cashback on all online spends and 1.5% on other transactions, making it suitable for diverse shopping needs.\n\n2. Highest Online Rewards: Cashback SBI Card provides the highest flat rate of 5% cashback on all online spending, though with a monthly cap of ₹5,000.\n\n3. Brand-Specific Benefits: Amazon Pay ICICI and Flipkart Axis Bank cards offer excellent value for loyal customers of these platforms, with up to 5% cashback and additional partner benefits.\n\n4. Annual Fee Considerations: Most cards offer fee waivers based on annual spending thresholds, ranging from ₹1 lakh to ₹3.5 lakh, making them effectively free for regular users.\n\n5. Additional Benefits: Many cards include complementary features like domestic lounge access, fuel surcharge waivers, and welcome bonuses, adding to their overall value." + } +} \ No newline at end of file diff --git a/cortex_on/agents/research_data/6a513487-cb89-49b9-b6b8-e75d22d661c1/todo.md b/cortex_on/agents/research_data/6a513487-cb89-49b9-b6b8-e75d22d661c1/todo.md new file mode 100644 index 0000000..28b4bee --- /dev/null +++ b/cortex_on/agents/research_data/6a513487-cb89-49b9-b6b8-e75d22d661c1/todo.md @@ -0,0 +1,64 @@ +# Research Plan: Best Credit Cards in India for ₹1,00,000 Monthly Income with Shopping Rewards Focus + +## Description +Comprehensive analysis of credit cards in India focusing on shopping rewards and cashback benefits, with secondary emphasis on domestic travel perks, aimed at users with a monthly income of ₹1,00,000 and a monthly spend of ₹10,000. + +## Progress: 5/5 tasks completed + +## Todo Items + +- [x] **Task task1** (Priority: 1): Identify the major banks in India that offer credit cards with a focus on shopping rewards and cashback benefits. - Completed: 2025-04-23 08:59 +- [x] **Task task2** (Priority: 2) (Depends on: task1): Collect detailed data on reward structures, cashback rates, welcome benefits, and domestic travel perks of credit cards from the identified banks. - Completed: 2025-04-23 09:00 +- [x] **Task task3** (Priority: 3) (Depends on: task2): Analyze the annual fees, interest rates, customer service quality, and potential hidden charges of the credit cards identified. - Completed: 2025-04-23 09:01 +- [x] **Task task4** (Priority: 4) (Depends on: task3): Assess and rank the credit cards based on value provided for shopping rewards and cashback, relative to the annual fee and the user's monthly income and spending profile. - Completed: 2025-04-23 09:01 +- [x] **Task task5** (Priority: 5) (Depends on: task4): Create user profiles and scenarios (e.g., high shopping volume or frequent domestic travel) to evaluate which credit cards offer the best overall benefits. - Completed: 2025-04-23 09:02 + +## Knowledge Gaps Identified + +- 1 +- . +- +- E +- x +- a +- c +- t +- p +- r +- o +- v +- l +- i +- e +- n +- d +- u +- m +- q +- s +- f +- h +- + +- 2 +- C +- w +- b +- 3 +- D +- g +- 4 +- R +- 5 +- y +- - +- S +- k +- ( +- ) +- M +- I +- A +- ₹ +- , +- 0 diff --git a/cortex_on/agents/research_data/e927c664-4a03-488c-852e-be747de0a3d3/final_report.md b/cortex_on/agents/research_data/e927c664-4a03-488c-852e-be747de0a3d3/final_report.md new file mode 100644 index 0000000..eea9621 --- /dev/null +++ b/cortex_on/agents/research_data/e927c664-4a03-488c-852e-be747de0a3d3/final_report.md @@ -0,0 +1,109 @@ +# Best Credit Cards in India for ₹1,00,000 Monthly Income: Cashback and Premium Category Analysis: Research Report + +# Research Report: Best Credit Cards in India for ₹1,00,000 Monthly Income: Cashback and Premium Category Analysis + +## Executive Summary + +This report provides a comprehensive analysis of the best premium credit cards available in India specifically designed for individuals earning ₹1,00,000 per month. The study focuses on three primary cards – SBI Cashback Credit Card, Axis Bank Ace Credit Card, and SBI SimplyCLICK Card – evaluating them based on their cashback rewards, welcome benefits, annual fees, additional perks, and user experiences. This analysis is intended to assist consumers in making informed decisions regarding credit card selection based on cashback benefits and premium features. + +## Introduction + +In the dynamic financial landscape, credit cards have become a pivotal tool for managing personal finances, offering both practicality and rewards. For individuals earning ₹1,00,000 per month, selecting a credit card that aligns with their spending habits and lifestyle is crucial. This report delves into the intricacies of three premium credit cards – SBI Cashback, Axis Bank Ace, and SBI SimplyCLICK – which are popular among this income bracket, offering robust cashback schemes and premium perks. + +## Main Findings + +### 1. Premium Credit Cards Suitable for ₹1,00,000 Monthly Income + +After identifying suitable options for the specified demographic, three credit cards emerged as ideal for individuals earning ₹1,00,000 monthly, with a spending pattern of ₹10,000 monthly. These cards provide a balanced blend of benefits, annual fees, and cashback rewards: + +- **SBI Cashback Credit Card** + - Annual Fee: ₹999 + GST, waived on ₹2 Lakh annual spend + - Cashback: 5% on online transactions, 1% on offline spends + - Best suited for online shoppers + +- **Axis Bank Ace Credit Card** + - Annual Fee: ₹499, waived on ₹2,00,000 annual spend + - Cashback: 5% on bill payments + - Offers travel benefits with lounge access + +- **SBI SimplyCLICK Card** + - Annual Fee: ₹499 + GST, waived on ₹1 Lakh annual spend + - Welcome Bonus: Amazon gift card worth ₹500 + - Benefits tailored for online shopping + +### 2. Welcome Benefits and Reward Rates Analysis + +The rewards structure of each card offers unique advantages tailored for distinct spending habits: + +- **SBI Cashback Credit Card** + - Lacks a specific welcome bonus but compensates with a robust 5% online cashback and a monthly cap of ₹5,000. +- **Axis Bank Ace Credit Card** + - Provides 2,000 reward points as a welcome bonus. Key for utility payments with a cashback cap of ₹2,000 monthly. +- **SBI SimplyCLICK Card** + - Offers lucrative online rewards: 10X points for partner merchant online purchases. + +### 3. Annual Fees and Cost Analysis + +Understanding the financial commitments involved in each card: + +- **SBI Cashback Credit Card** + - Total First Year Cost: ₹2,358 (including GST) + - Higher cost but offers significantly on cashback. +- **Axis Bank Ace Credit Card** + - Total First Year Cost: ₹499 + - Provides economical value with feasible waiver conditions. +- **SBI SimplyCLICK Card** + - Total First Year Cost: ₹1,178 (including GST) + - Most attainable fee waiver at ₹1 Lakh annual spending. + +### 4. Additional Benefits Analysis + +Each card offers various additional benefits to enhance user experience: + +- **Insurance Coverage** + - All cards offer basic fraud and purchase protection; unique offers like air accident coverage on Axis Bank Ace. +- **Travel and Lifestyle** + - Lounge access on Axis Bank Ace and dining offers across all cards, while SBI Cashback focuses on purchase protection. + +### 5. Cashback Terms and Conditions Analysis + +Evaluation of cashback programs reveals important insights: + +- **SBI Cashback Credit Card** + - Transaction exclusions impact high-frequency usage categories like fuel and government services. +- **Axis Bank Ace Credit Card** + - Notable for utility cashback but limited high-value transaction benefits. +- **SBI SimplyCLICK Card** + - Emphasizes powerful online rewards but involves complex redemption processes. + +### 6. User Experiences and Reviews Analysis + +User feedback highlights strengths and areas for improvement: + +- **SBI Cashback Credit Card** + - Favorable for online cashback but offset by high annual fees. +- **Axis Bank Ace Credit Card** + - Praised for utility bill efficiency, tempered by merchant exclusions. +- **SBI SimplyCLICK Card** + - Appreciated for online spend perks but criticized for complex rewards. + +## Conclusion + +This analysis showed that selecting a credit card is highly conditional on spending behavior and financial priorities. Each card offers distinct advantages, making them suitable for different consumer needs. The SBI Cashback Card is ideal for those focused on maximizing online shopping cashback, while Axis Bank Ace benefits utility bill payers. SBI SimplyCLICK stands out for online rewards. Careful evaluation of one's spending habits against the features of these cards will ensure the optimal choice. + +## Knowledge Gap Address and Future Research Directions + +The gaps identified during this research are acknowledged: + +- Detailed exploration of exact rewards across all merchant categories +- Current welcome bonuses and seasonal offers +- Exact cashback limits and redemption policies + +Future investigations should address customer service quality and long-term reliability of rewards programs. Monitoring updates in reward structures and customer feedback is essential for maintaining relevant consumer recommendations. + +## References + +This report synthesizes information from anonymous customer reviews, official bank documents, and financial product comparisons. Specific citations would be available upon review of source materials. + +--- +This Markdown formatted report provides an organized and accessible overview of the findings from the various research tasks undertaken. \ No newline at end of file diff --git a/cortex_on/agents/research_data/e927c664-4a03-488c-852e-be747de0a3d3/findings_task1_20250423_090903.md b/cortex_on/agents/research_data/e927c664-4a03-488c-852e-be747de0a3d3/findings_task1_20250423_090903.md new file mode 100644 index 0000000..a063638 --- /dev/null +++ b/cortex_on/agents/research_data/e927c664-4a03-488c-852e-be747de0a3d3/findings_task1_20250423_090903.md @@ -0,0 +1,224 @@ +# Findings for Task task1: Identify the top premium credit cards available in India that are suitable for individuals with a ₹1,00,000 monthly income. + +Based on analysis of premium credit cards suitable for ₹1,00,000 monthly income and ₹10,000 monthly spending, the following cards emerge as top contenders: + +1. SBI Cashback Credit Card +- Annual Fee: ₹999 + GST (waived on ₹2 Lakh annual spend) +- 5% cashback on online spends +- 1% cashback on offline spends +- 1% fuel surcharge waiver + +2. Axis Bank Ace Credit Card +- Annual Fee: ₹499 (waived on ₹2,00,000 annual spend) +- 5% cashback on bill payments +- 4 complimentary lounge visits per year + +3. SBI SimplyCLICK Card +- Annual Fee: ₹499 + GST (waived on ₹1 Lakh annual spend) +- Amazon gift card worth ₹500 as welcome benefit +- Good rewards on online spending + +These cards are well-suited for the specified income range and spending pattern, offering good cashback benefits with reasonable annual fees that can be waived through spending. + +## Knowledge Gaps Identified + +- 1 +- . +- +- E +- x +- a +- c +- t +- +- r +- e +- w +- a +- r +- d +- +- r +- a +- t +- e +- s +- +- o +- n +- +- s +- p +- e +- c +- i +- f +- i +- c +- +- m +- e +- r +- c +- h +- a +- n +- t +- +- c +- a +- t +- e +- g +- o +- r +- i +- e +- s +- + +- 2 +- . +- +- C +- u +- r +- r +- e +- n +- t +- +- w +- e +- l +- c +- o +- m +- e +- +- b +- o +- n +- u +- s +- +- o +- f +- f +- e +- r +- s +- +- f +- o +- r +- +- Q +- 2 +- +- 2 +- 0 +- 2 +- 5 +- + +- 3 +- . +- +- D +- e +- t +- a +- i +- l +- e +- d +- +- t +- e +- r +- m +- s +- +- a +- n +- d +- +- c +- o +- n +- d +- i +- t +- i +- o +- n +- s +- +- f +- o +- r +- +- c +- a +- s +- h +- b +- a +- c +- k +- +- c +- a +- p +- s +- + +- 4 +- . +- +- R +- e +- c +- e +- n +- t +- +- u +- s +- e +- r +- +- e +- x +- p +- e +- r +- i +- e +- n +- c +- e +- s +- +- w +- i +- t +- h +- +- c +- u +- s +- t +- o +- m +- e +- r +- +- s +- e +- r +- v +- i +- c +- e diff --git a/cortex_on/agents/research_data/e927c664-4a03-488c-852e-be747de0a3d3/findings_task2_20250423_090946.md b/cortex_on/agents/research_data/e927c664-4a03-488c-852e-be747de0a3d3/findings_task2_20250423_090946.md new file mode 100644 index 0000000..6f28e24 --- /dev/null +++ b/cortex_on/agents/research_data/e927c664-4a03-488c-852e-be747de0a3d3/findings_task2_20250423_090946.md @@ -0,0 +1,244 @@ +# Findings for Task task2: Analyze the welcome benefits and reward rates offered by these identified premium credit cards. + +Detailed analysis of welcome benefits and reward rates for the recommended cards: + +1. SBI Cashback Credit Card: +Welcome Benefits: +- No specific welcome benefits offered +Reward Rates: +- 5% cashback on all online spends (no merchant restrictions) +- 1% cashback on offline spends +- 1% fuel surcharge waiver at all petrol pumps +- Monthly cashback cap of ₹5,000 +- Cashback credited within 2 working days of statement generation + +2. Axis Bank Ace Credit Card: +Welcome Benefits: +- 2,000 welcome reward points on first transaction +Reward Rates: +- 5% cashback on utility bill payments +- Up to 4% cashback on selected merchant categories +- 1% cashback on all other spends +- 4 complimentary domestic airport lounge visits per year + +3. SBI SimplyCLICK Card: +Welcome Benefits: +- Amazon gift card worth ₹500 +Reward Rates: +- 10X reward points on online spends at partner merchants +- 5X reward points on other online spends +- 1 reward point per ₹100 on other spends +- ₹2000 e-vouchers for spends of ₹1 lakh+ +- 1% fuel surcharge waiver + +## Knowledge Gaps Identified + +- 1 +- . +- +- M +- o +- n +- t +- h +- l +- y +- / +- a +- n +- n +- u +- a +- l +- +- c +- a +- p +- s +- +- o +- n +- +- r +- e +- w +- a +- r +- d +- s +- +- f +- o +- r +- +- A +- x +- i +- s +- +- B +- a +- n +- k +- +- A +- c +- e +- +- C +- a +- r +- d +- + +- 2 +- . +- +- S +- p +- e +- c +- i +- f +- i +- c +- +- t +- e +- r +- m +- s +- +- f +- o +- r +- +- r +- e +- w +- a +- r +- d +- +- p +- o +- i +- n +- t +- +- r +- e +- d +- e +- m +- p +- t +- i +- o +- n +- + +- 3 +- . +- +- S +- e +- a +- s +- o +- n +- a +- l +- +- p +- r +- o +- m +- o +- t +- i +- o +- n +- a +- l +- +- o +- f +- f +- e +- r +- s +- +- i +- f +- +- a +- n +- y +- + +- 4 +- . +- +- P +- a +- r +- t +- n +- e +- r +- +- m +- e +- r +- c +- h +- a +- n +- t +- +- l +- i +- s +- t +- +- f +- o +- r +- +- e +- n +- h +- a +- n +- c +- e +- d +- +- r +- e +- w +- a +- r +- d +- s +- +- o +- n +- +- S +- i +- m +- p +- l +- y +- C +- L +- I +- C +- K +- +- C +- a +- r +- d diff --git a/cortex_on/agents/research_data/e927c664-4a03-488c-852e-be747de0a3d3/findings_task3_20250423_091014.md b/cortex_on/agents/research_data/e927c664-4a03-488c-852e-be747de0a3d3/findings_task3_20250423_091014.md new file mode 100644 index 0000000..0621bee --- /dev/null +++ b/cortex_on/agents/research_data/e927c664-4a03-488c-852e-be747de0a3d3/findings_task3_20250423_091014.md @@ -0,0 +1,216 @@ +# Findings for Task task3: Evaluate the annual fees associated with each of the identified premium credit cards. + +Detailed analysis of annual fees and associated conditions for each card: + +1. SBI Cashback Credit Card: +- Joining fee: ₹999 + 18% GST +- Annual fee: ₹999 + 18% GST +- Total first-year cost: ₹2,358 (including GST) +- Renewal fee waiver: Available on spending ₹2 Lakh annually +- Fee structure suitable for regular spenders who can meet waiver criteria + +2. Axis Bank Ace Credit Card: +- Joining fee: ₹499 +- Annual fee: ₹499 +- Total first-year cost: ₹499 +- Renewal fee waiver: Available on spending ₹2,00,000 annually +- Most economical option among the three with lower fees and achievable waiver threshold + +3. SBI SimplyCLICK Card: +- Joining fee: ₹499 + GST +- Annual fee: ₹499 + GST +- Total first-year cost: ₹1,178 (including GST) +- Renewal fee waiver: Available on spending ₹1 Lakh annually +- Most accessible waiver threshold, making it suitable for moderate spenders + +Comparative Analysis: +- Lowest joining fee: Axis Bank Ace Card (₹499) +- Lowest spending requirement for fee waiver: SBI SimplyCLICK (₹1 Lakh) +- Highest total fees: SBI Cashback Card (₹2,358 with GST) +- Most value for money: Axis Bank Ace Card (considering fees and benefits) + +## Knowledge Gaps Identified + +- 1 +- . +- +- P +- r +- o +- - +- r +- a +- t +- a +- +- f +- e +- e +- +- r +- e +- f +- u +- n +- d +- +- p +- o +- l +- i +- c +- i +- e +- s +- + +- 2 +- . +- +- F +- e +- e +- +- r +- e +- v +- e +- r +- s +- a +- l +- +- t +- i +- m +- e +- f +- r +- a +- m +- e +- s +- +- a +- f +- t +- e +- r +- +- m +- e +- e +- t +- i +- n +- g +- +- s +- p +- e +- n +- d +- +- c +- r +- i +- t +- e +- r +- i +- a +- + +- 3 +- . +- +- I +- m +- p +- a +- c +- t +- +- o +- f +- +- c +- a +- r +- d +- +- u +- p +- g +- r +- a +- d +- e +- s +- +- o +- n +- +- a +- n +- n +- u +- a +- l +- +- f +- e +- e +- s +- + +- 4 +- . +- +- S +- p +- e +- c +- i +- a +- l +- +- f +- e +- e +- +- w +- a +- i +- v +- e +- r +- +- p +- r +- o +- m +- o +- t +- i +- o +- n +- s +- +- f +- o +- r +- +- n +- e +- w +- +- c +- u +- s +- t +- o +- m +- e +- r +- s diff --git a/cortex_on/agents/research_data/e927c664-4a03-488c-852e-be747de0a3d3/findings_task4_20250423_091041.md b/cortex_on/agents/research_data/e927c664-4a03-488c-852e-be747de0a3d3/findings_task4_20250423_091041.md new file mode 100644 index 0000000..b75b33d --- /dev/null +++ b/cortex_on/agents/research_data/e927c664-4a03-488c-852e-be747de0a3d3/findings_task4_20250423_091041.md @@ -0,0 +1,216 @@ +# Findings for Task task4: Investigate additional benefits such as insurance coverage, lounge access, and concierge services provided by the credit cards. + +Analysis of additional benefits for each credit card: + +1. SBI Cashback Credit Card: +Insurance Benefits: +- Purchase protection coverage up to ₹1 lakh +- Credit shield coverage of ₹1 lakh +- Zero liability on lost card +- Fraud protection insurance + +Travel & Lifestyle: +- 1% fuel surcharge waiver nationwide +- Contactless transactions up to ₹5,000 +- Wide merchant acceptance (24+ million outlets worldwide) +- No international lounge access + +2. Axis Bank Ace Credit Card: +Insurance Benefits: +- Purchase protection +- Card liability coverage +- Air accident coverage + +Travel & Lifestyle: +- 4 complimentary domestic airport lounge visits per year +- Dining discounts at partner restaurants +- Movie ticket offers +- EMI conversion facility +- Fuel surcharge waiver at select outlets + +3. SBI SimplyCLICK Card: +Insurance Benefits: +- Purchase protection +- Lost card liability +- Fraud protection + +Travel & Lifestyle: +- 1% fuel surcharge waiver +- EasyEMI option for large purchases +- Partner merchant discounts +- Movie and entertainment offers +- Online shopping deals + +Common Features Across Cards: +- EMI conversion options +- Online transaction security +- Mobile banking apps +- 24/7 customer support +- Zero liability on fraud transactions +- SMS and email alerts + +## Knowledge Gaps Identified + +- 1 +- . +- +- E +- x +- a +- c +- t +- +- t +- e +- r +- m +- s +- +- o +- f +- +- i +- n +- s +- u +- r +- a +- n +- c +- e +- +- c +- o +- v +- e +- r +- a +- g +- e +- + +- 2 +- . +- +- S +- p +- e +- c +- i +- f +- i +- c +- +- p +- a +- r +- t +- n +- e +- r +- +- m +- e +- r +- c +- h +- a +- n +- t +- s +- +- f +- o +- r +- +- d +- i +- n +- i +- n +- g +- +- d +- i +- s +- c +- o +- u +- n +- t +- s +- + +- 3 +- . +- +- I +- n +- t +- e +- r +- n +- a +- t +- i +- o +- n +- a +- l +- +- a +- c +- c +- e +- p +- t +- a +- n +- c +- e +- +- r +- a +- t +- e +- s +- + +- 4 +- . +- +- E +- m +- e +- r +- g +- e +- n +- c +- y +- +- c +- a +- r +- d +- +- r +- e +- p +- l +- a +- c +- e +- m +- e +- n +- t +- +- s +- e +- r +- v +- i +- c +- e +- s diff --git a/cortex_on/agents/research_data/e927c664-4a03-488c-852e-be747de0a3d3/findings_task5_20250423_091112.md b/cortex_on/agents/research_data/e927c664-4a03-488c-852e-be747de0a3d3/findings_task5_20250423_091112.md new file mode 100644 index 0000000..80dcd71 --- /dev/null +++ b/cortex_on/agents/research_data/e927c664-4a03-488c-852e-be747de0a3d3/findings_task5_20250423_091112.md @@ -0,0 +1,236 @@ +# Findings for Task task5: Examine the terms and conditions related to cashback rewards for these credit cards, focusing on any exclusions or caps. + +Detailed analysis of cashback terms, conditions, and restrictions for each card: + +1. SBI Cashback Credit Card: +Cashback Caps: +- Monthly cashback cap: ₹5,000 +- Minimum transaction amount: None specified + +Exclusions: +- Insurance premium payments +- Fuel transactions (except 1% surcharge waiver) +- Rent payments +- Wallet loads +- Educational services +- Government services +- Jewelry purchases +- Railway bookings +- EMI transactions + +Terms: +- Cashback credited within 2 working days of statement generation +- No minimum spending requirement for cashback eligibility +- Online transactions must be in INR +- Cashback non-transferable and non-refundable + +2. Axis Bank Ace Credit Card: +Cashback Caps: +- Monthly cap on utility bill cashback: ₹2,000 +- Maximum cashback per transaction: ₹500 + +Exclusions: +- Corporate bill payments +- Government payments +- Insurance premiums +- Mutual fund investments +- School fee payments +- EMI transactions + +Terms: +- Cashback credited in next billing cycle +- Minimum transaction amount: ₹100 +- Valid only on successful transactions +- Cashback program can be modified with notice + +3. SBI SimplyCLICK Card: +Reward Points Caps: +- Maximum 10X reward points up to ₹50,000 monthly spend +- Maximum 5X reward points up to ₹25,000 monthly spend + +Exclusions: +- Cash withdrawals +- Fuel transactions (except surcharge waiver) +- Insurance payments +- Government services +- EMI conversions + +Terms: +- Points validity: 3 years +- Minimum redemption: 2,000 points +- Points forfeited on card closure +- Program terms subject to change + +## Knowledge Gaps Identified + +- 1 +- . +- +- E +- x +- a +- c +- t +- +- p +- o +- i +- n +- t +- - +- t +- o +- - +- r +- u +- p +- e +- e +- +- c +- o +- n +- v +- e +- r +- s +- i +- o +- n +- +- r +- a +- t +- e +- s +- + +- 2 +- . +- +- S +- p +- e +- c +- i +- a +- l +- +- s +- e +- a +- s +- o +- n +- a +- l +- +- e +- x +- c +- l +- u +- s +- i +- o +- n +- s +- + +- 3 +- . +- +- C +- h +- a +- n +- g +- e +- s +- +- t +- o +- +- t +- e +- r +- m +- s +- +- p +- l +- a +- n +- n +- e +- d +- +- f +- o +- r +- +- u +- p +- c +- o +- m +- i +- n +- g +- +- q +- u +- a +- r +- t +- e +- r +- s +- + +- 4 +- . +- +- I +- m +- p +- a +- c +- t +- +- o +- f +- +- c +- a +- r +- d +- +- u +- p +- g +- r +- a +- d +- e +- s +- +- o +- n +- +- e +- x +- i +- s +- t +- i +- n +- g +- +- r +- e +- w +- a +- r +- d +- s diff --git a/cortex_on/agents/research_data/e927c664-4a03-488c-852e-be747de0a3d3/findings_task6_20250423_091142.md b/cortex_on/agents/research_data/e927c664-4a03-488c-852e-be747de0a3d3/findings_task6_20250423_091142.md new file mode 100644 index 0000000..71c12d9 --- /dev/null +++ b/cortex_on/agents/research_data/e927c664-4a03-488c-852e-be747de0a3d3/findings_task6_20250423_091142.md @@ -0,0 +1,194 @@ +# Findings for Task task6: Gather and synthesize user experiences and reviews concerning the cashback and premium features of these credit cards. + +Analysis of user experiences and reviews for the three credit cards: + +1. SBI Cashback Credit Card: +Positive Feedback: +- Consistent 5% cashback on online transactions +- Quick cashback credit (within 2 days) +- No merchant restrictions for online purchases +- Reliable customer service + +Common Complaints: +- High annual fee compared to benefits +- Exclusion of popular payment categories +- Limited offline benefits +- No welcome benefits + +2. Axis Bank Ace Credit Card: +Positive Feedback: +- Excellent for utility bill payments +- Good customer support +- Easy fee waiver conditions +- Quick reward processing + +Common Complaints: +- Limited merchant partnerships +- Transaction caps on cashback +- Inconsistent reward rates for some categories +- App interface issues reported + +3. SBI SimplyCLICK Card: +Positive Feedback: +- Good welcome benefits +- Strong online shopping rewards +- Lower annual fee +- Easy spend-based fee waiver + +Common Complaints: +- Complex reward point system +- Limited offline benefits +- Partner merchant restrictions +- Redemption process could be simpler + +General User Sentiments: +- All cards rated good for specific use cases +- Value depends heavily on spending patterns +- Customer service varies by location +- Online transaction focus appreciated +- Fee waiver thresholds considered reasonable + +## Knowledge Gaps Identified + +- 1 +- . +- +- L +- o +- n +- g +- - +- t +- e +- r +- m +- +- r +- e +- l +- i +- a +- b +- i +- l +- i +- t +- y +- +- o +- f +- +- r +- e +- w +- a +- r +- d +- s +- +- p +- r +- o +- g +- r +- a +- m +- s +- + +- 2 +- . +- +- C +- u +- s +- t +- o +- m +- e +- r +- +- s +- e +- r +- v +- i +- c +- e +- +- r +- e +- s +- p +- o +- n +- s +- e +- +- t +- i +- m +- e +- s +- + +- 3 +- . +- +- C +- a +- r +- d +- +- u +- p +- g +- r +- a +- d +- e +- +- e +- x +- p +- e +- r +- i +- e +- n +- c +- e +- s +- + +- 4 +- . +- +- D +- i +- g +- i +- t +- a +- l +- +- p +- l +- a +- t +- f +- o +- r +- m +- +- r +- e +- l +- i +- a +- b +- i +- l +- i +- t +- y diff --git a/cortex_on/agents/research_data/e927c664-4a03-488c-852e-be747de0a3d3/todo.json b/cortex_on/agents/research_data/e927c664-4a03-488c-852e-be747de0a3d3/todo.json new file mode 100644 index 0000000..06166f0 --- /dev/null +++ b/cortex_on/agents/research_data/e927c664-4a03-488c-852e-be747de0a3d3/todo.json @@ -0,0 +1,155 @@ +{ + "title": "Best Credit Cards in India for ₹1,00,000 Monthly Income: Cashback and Premium Category Analysis", + "description": "Comprehensive analysis of premium credit cards in India focusing on cashback rewards for individuals with ₹1,00,000 monthly income and ₹10,000 monthly spending.", + "todo_items": [ + { + "id": "task1", + "description": "Identify the top premium credit cards available in India that are suitable for individuals with a ₹1,00,000 monthly income.", + "completed": true, + "dependencies": [], + "priority": 1, + "findings_path": "/app/agents/research_data/e927c664-4a03-488c-852e-be747de0a3d3/findings_task1_20250423_090903.md", + "completion_time": "2025-04-23T09:09:03.851871", + "knowledge_gaps": [ + "1. Exact reward rates on specific merchant categories\n2. Current welcome bonus offers for Q2 2025\n3. Detailed terms and conditions for cashback caps\n4. Recent user experiences with customer service" + ] + }, + { + "id": "task2", + "description": "Analyze the welcome benefits and reward rates offered by these identified premium credit cards.", + "completed": true, + "dependencies": [ + "task1" + ], + "priority": 2, + "findings_path": "/app/agents/research_data/e927c664-4a03-488c-852e-be747de0a3d3/findings_task2_20250423_090946.md", + "completion_time": "2025-04-23T09:09:46.028680", + "knowledge_gaps": [ + "1. Monthly/annual caps on rewards for Axis Bank Ace Card\n2. Specific terms for reward point redemption\n3. Seasonal promotional offers if any\n4. Partner merchant list for enhanced rewards on SimplyCLICK Card" + ] + }, + { + "id": "task3", + "description": "Evaluate the annual fees associated with each of the identified premium credit cards.", + "completed": true, + "dependencies": [ + "task1" + ], + "priority": 3, + "findings_path": "/app/agents/research_data/e927c664-4a03-488c-852e-be747de0a3d3/findings_task3_20250423_091014.md", + "completion_time": "2025-04-23T09:10:14.668916", + "knowledge_gaps": [ + "1. Pro-rata fee refund policies\n2. Fee reversal timeframes after meeting spend criteria\n3. Impact of card upgrades on annual fees\n4. Special fee waiver promotions for new customers" + ] + }, + { + "id": "task4", + "description": "Investigate additional benefits such as insurance coverage, lounge access, and concierge services provided by the credit cards.", + "completed": true, + "dependencies": [ + "task1" + ], + "priority": 4, + "findings_path": "/app/agents/research_data/e927c664-4a03-488c-852e-be747de0a3d3/findings_task4_20250423_091041.md", + "completion_time": "2025-04-23T09:10:41.671050", + "knowledge_gaps": [ + "1. Exact terms of insurance coverage\n2. Specific partner merchants for dining discounts\n3. International acceptance rates\n4. Emergency card replacement services" + ] + }, + { + "id": "task5", + "description": "Examine the terms and conditions related to cashback rewards for these credit cards, focusing on any exclusions or caps.", + "completed": true, + "dependencies": [ + "task2" + ], + "priority": 5, + "findings_path": "/app/agents/research_data/e927c664-4a03-488c-852e-be747de0a3d3/findings_task5_20250423_091112.md", + "completion_time": "2025-04-23T09:11:12.501090", + "knowledge_gaps": [ + "1. Exact point-to-rupee conversion rates\n2. Special seasonal exclusions\n3. Changes to terms planned for upcoming quarters\n4. Impact of card upgrades on existing rewards" + ] + }, + { + "id": "task6", + "description": "Gather and synthesize user experiences and reviews concerning the cashback and premium features of these credit cards.", + "completed": true, + "dependencies": [ + "task5" + ], + "priority": 6, + "findings_path": "/app/agents/research_data/e927c664-4a03-488c-852e-be747de0a3d3/findings_task6_20250423_091142.md", + "completion_time": "2025-04-23T09:11:42.202383", + "knowledge_gaps": "1. Long-term reliability of rewards programs\n2. Customer service response times\n3. Card upgrade experiences\n4. Digital platform reliability" + } + ], + "current_item_id": null, + "completed_items": [ + "task1", + "task2", + "task3", + "task4", + "task5", + "task6" + ], + "last_completed_item_id": "task6", + "knowledge_gaps": [ + "1", + ".", + " ", + "E", + "x", + "a", + "c", + "t", + "r", + "e", + "w", + "d", + "s", + "o", + "n", + "p", + "i", + "f", + "m", + "h", + "g", + "\n", + "2", + "C", + "u", + "l", + "b", + "Q", + "0", + "5", + "3", + "D", + "k", + "4", + "R", + "v", + "M", + "y", + "/", + "A", + "B", + "S", + "P", + "L", + "I", + "K", + "-", + "F", + "q" + ], + "report_sections": { + "task1": "## Premium Credit Cards Suitable for ₹1,00,000 Monthly Income\n\nAfter analyzing various premium credit cards available in India, we have identified several options that are well-suited for individuals with a monthly income of ₹1,00,000 and monthly spending of ₹10,000. These cards offer a good balance of cashback rewards, reasonable annual fees, and additional benefits while being accessible for the specified income range.\n\n### Top Recommendations:\n\n1. **SBI Cashback Credit Card**\n - Ideal for online shoppers\n - Strong cashback program with no merchant restrictions\n - Reasonable annual fee with waiver option\n - Comprehensive reward structure for both online and offline spending\n\n2. **Axis Bank Ace Credit Card**\n - Excellent for bill payments\n - Moderate annual fee with achievable waiver criteria\n - Additional travel benefits included\n - Good balance of rewards and utility\n\n3. **SBI SimplyCLICK Card**\n - Lower annual fee requirement\n - Attractive welcome benefits\n - Suitable for online shopping\n - Easy fee waiver threshold\n\nThese cards have been selected based on:\n- Accessibility for the specified income range\n- Strong cashback rewards aligned with spending patterns\n- Reasonable annual fees with waiver options\n- Additional benefits that add value\n- Suitable credit limit ranges", + "task2": "## Welcome Benefits and Reward Rates Analysis\n\nThe analysis of welcome benefits and reward rates reveals distinct advantages for each card:\n\n### SBI Cashback Credit Card\nThe card focuses on straightforward cashback benefits rather than welcome bonuses:\n- No welcome benefits, but strong ongoing rewards\n- Industry-leading 5% cashback on all online transactions\n- Practical 1% cashback on offline spends\n- Monthly cashback automatically credited\n- Clear caps and no complicated point systems\n\n### Axis Bank Ace Credit Card\nBalanced mix of welcome benefits and ongoing rewards:\n- Modest welcome bonus of 2,000 reward points\n- Strong focus on utility bill payments with 5% cashback\n- Tiered reward structure based on merchant categories\n- Additional travel benefits with lounge access\n- Flexible reward redemption options\n\n### SBI SimplyCLICK Card\nFocused on online shopping benefits:\n- Attractive welcome gift of ₹500 Amazon voucher\n- Enhanced rewards for online shopping\n- Accelerated reward points at partner merchants\n- Additional milestone benefits\n- E-voucher benefits for high spenders\n\nEach card offers unique reward structures suited for different spending patterns:\n- SBI Cashback: Best for heavy online spenders\n- Axis Bank Ace: Ideal for bill payments and varied spending\n- SimplyCLICK: Optimal for online shopping at partner merchants", + "task3": "## Annual Fees and Cost Analysis\n\nA detailed examination of the annual fees and associated costs reveals important considerations for each card:\n\n### SBI Cashback Credit Card\n- **Total First Year Cost**: ₹2,358 (including GST)\n - Joining fee: ₹999 + 18% GST\n - Annual fee: ₹999 + 18% GST\n- **Fee Waiver**: Available on ₹2 Lakh annual spend\n- **Cost-Benefit Analysis**: Higher fees but justified by unlimited 5% online cashback\n\n### Axis Bank Ace Credit Card\n- **Total First Year Cost**: ₹499\n - Joining fee: ₹499\n - Annual fee: ₹499\n- **Fee Waiver**: Available on ₹2,00,000 annual spend\n- **Cost-Benefit Analysis**: Most economical option with good benefits\n\n### SBI SimplyCLICK Card\n- **Total First Year Cost**: ₹1,178 (including GST)\n - Joining fee: ₹499 + GST\n - Annual fee: ₹499 + GST\n- **Fee Waiver**: Available on ₹1 Lakh annual spend\n- **Cost-Benefit Analysis**: Balanced fees with lowest spend requirement for waiver\n\n### Optimal Choice Based on Spending:\n- For ₹10,000 monthly spending (₹1.2 Lakh annually):\n - SBI SimplyCLICK Card offers guaranteed fee waiver\n - Axis Bank Ace Card requires additional ₹80,000 annual spend\n - SBI Cashback Card requires additional ₹80,000 annual spend\n\nThe fee structures are designed to encourage higher spending while offering reasonable waiver thresholds for regular users.", + "task4": "## Additional Benefits Analysis\n\nA comprehensive evaluation of additional benefits reveals varying levels of coverage and lifestyle perks across the three cards:\n\n### Insurance Coverage\nEach card offers essential protection with some variations:\n- SBI Cashback Card leads in purchase protection (₹1 lakh) and credit shield\n- Axis Bank Ace includes air accident coverage\n- All cards provide fraud protection and lost card liability\n\n### Travel Benefits\nThe cards offer different travel-related perks:\n- Axis Bank Ace stands out with 4 domestic lounge visits\n- All cards provide fuel surcharge waiver benefits\n- Wide international acceptance for all cards\n\n### Lifestyle Benefits\nEach card caters to different lifestyle needs:\n- SBI Cashback: Focus on shopping protection\n- Axis Bank Ace: Strong dining and entertainment benefits\n- SimplyCLICK: Enhanced online shopping benefits\n\n### Security Features\nAll cards maintain high security standards:\n- Zero liability on fraud\n- Real-time transaction alerts\n- Online transaction security\n- 24/7 customer support\n\nThe additional benefits complement each card's primary rewards structure, providing comprehensive coverage for different user needs.", + "task5": "## Cashback Terms and Conditions Analysis\n\nA detailed examination of the terms, conditions, and restrictions reveals important considerations for each card's reward structure:\n\n### SBI Cashback Credit Card\n**Cashback Structure:**\n- 5% online / 1% offline cashback model\n- Monthly cap: ₹5,000\n- No minimum transaction requirement\n- Quick crediting (2 working days)\n\n**Key Exclusions:**\n- Insurance and fuel transactions\n- Rent and educational payments\n- Government services\n- Jewelry purchases\n- EMI transactions\n\n### Axis Bank Ace Credit Card\n**Cashback Structure:**\n- 5% on utility bills (capped at ₹2,000/month)\n- Per-transaction cap: ₹500\n- Minimum transaction: ₹100\n\n**Key Exclusions:**\n- Corporate/Government payments\n- Insurance and investments\n- School fees\n- EMI transactions\n\n### SBI SimplyCLICK Card\n**Rewards Structure:**\n- 10X points (up to ₹50,000 monthly)\n- 5X points (up to ₹25,000 monthly)\n- Points valid for 3 years\n- Minimum redemption: 2,000 points\n\n**Key Exclusions:**\n- Cash withdrawals\n- Fuel transactions\n- Insurance payments\n- Government services\n\n### Important Considerations\n- All cards exclude essential service payments\n- Monthly caps affect high-value transactions\n- Regular spending patterns crucial for maximizing benefits\n- Terms subject to periodic review and changes", + "task6": "## User Experiences and Reviews Analysis\n\nThe analysis of user feedback provides valuable insights into real-world performance of these cards:\n\n### SBI Cashback Credit Card\n**Strengths Highlighted:**\n- Reliable 5% online cashback program\n- Quick cashback crediting\n- Straightforward rewards structure\n- Wide online acceptance\n\n**User Concerns:**\n- Annual fee considered high\n- Limited offline benefits\n- Some key category exclusions\n\n### Axis Bank Ace Credit Card\n**Strengths Highlighted:**\n- Superior for bill payments\n- Responsive customer service\n- Achievable fee waiver\n- Quick reward processing\n\n**User Concerns:**\n- Transaction caps limitation\n- Limited merchant partnerships\n- App functionality issues\n\n### SBI SimplyCLICK Card\n**Strengths Highlighted:**\n- Attractive welcome benefits\n- Strong online rewards\n- Lower fee structure\n- Easy fee waiver\n\n**User Concerns:**\n- Complex point system\n- Limited offline value\n- Redemption complexity\n\n### Overall User Sentiment\n- Cards well-suited for digital-first users\n- Value proposition depends on spending habits\n- Fee structures generally considered fair\n- Customer service experiences vary\n- Online focus appreciated by users" + } +} \ No newline at end of file diff --git a/cortex_on/agents/research_data/e927c664-4a03-488c-852e-be747de0a3d3/todo.md b/cortex_on/agents/research_data/e927c664-4a03-488c-852e-be747de0a3d3/todo.md new file mode 100644 index 0000000..c7b0791 --- /dev/null +++ b/cortex_on/agents/research_data/e927c664-4a03-488c-852e-be747de0a3d3/todo.md @@ -0,0 +1,68 @@ +# Research Plan: Best Credit Cards in India for ₹1,00,000 Monthly Income: Cashback and Premium Category Analysis + +## Description +Comprehensive analysis of premium credit cards in India focusing on cashback rewards for individuals with ₹1,00,000 monthly income and ₹10,000 monthly spending. + +## Progress: 6/6 tasks completed + +## Todo Items + +- [x] **Task task1** (Priority: 1): Identify the top premium credit cards available in India that are suitable for individuals with a ₹1,00,000 monthly income. - Completed: 2025-04-23 09:09 +- [x] **Task task2** (Priority: 2) (Depends on: task1): Analyze the welcome benefits and reward rates offered by these identified premium credit cards. - Completed: 2025-04-23 09:09 +- [x] **Task task3** (Priority: 3) (Depends on: task1): Evaluate the annual fees associated with each of the identified premium credit cards. - Completed: 2025-04-23 09:10 +- [x] **Task task4** (Priority: 4) (Depends on: task1): Investigate additional benefits such as insurance coverage, lounge access, and concierge services provided by the credit cards. - Completed: 2025-04-23 09:10 +- [x] **Task task5** (Priority: 5) (Depends on: task2): Examine the terms and conditions related to cashback rewards for these credit cards, focusing on any exclusions or caps. - Completed: 2025-04-23 09:11 +- [x] **Task task6** (Priority: 6) (Depends on: task5): Gather and synthesize user experiences and reviews concerning the cashback and premium features of these credit cards. - Completed: 2025-04-23 09:11 + +## Knowledge Gaps Identified + +- 1 +- . +- +- E +- x +- a +- c +- t +- r +- e +- w +- d +- s +- o +- n +- p +- i +- f +- m +- h +- g +- + +- 2 +- C +- u +- l +- b +- Q +- 0 +- 5 +- 3 +- D +- k +- 4 +- R +- v +- M +- y +- / +- A +- B +- S +- P +- L +- I +- K +- - +- F +- q diff --git a/cortex_on/agents/research_data/ea15e211-2645-476f-bdff-070ad4c941d7/batch_extraction_20250423_105854/1_https___www_reddit_com_r_CreditCardsIndia_comments_1fae9cf_fuel_credit_cards_comparison_.md b/cortex_on/agents/research_data/ea15e211-2645-476f-bdff-070ad4c941d7/batch_extraction_20250423_105854/1_https___www_reddit_com_r_CreditCardsIndia_comments_1fae9cf_fuel_credit_cards_comparison_.md new file mode 100644 index 0000000..4d90677 --- /dev/null +++ b/cortex_on/agents/research_data/ea15e211-2645-476f-bdff-070ad4c941d7/batch_extraction_20250423_105854/1_https___www_reddit_com_r_CreditCardsIndia_comments_1fae9cf_fuel_credit_cards_comparison_.md @@ -0,0 +1,4 @@ +# Content from https://www.reddit.com/r/CreditCardsIndia/comments/1fae9cf/fuel_credit_cards_comparison/ + +r/CreditCardsIndia is a community for discussing credit cards in India—rewards, benefits, bank policies, and more. Stay informed and make smarter financial decisions. +# Fuel credit cards comparison diff --git a/cortex_on/agents/research_data/ea15e211-2645-476f-bdff-070ad4c941d7/batch_extraction_20250423_105854/2_https___www_creditkaro_com_credit-card_bank-of-baroda-easy-credit-card.md b/cortex_on/agents/research_data/ea15e211-2645-476f-bdff-070ad4c941d7/batch_extraction_20250423_105854/2_https___www_creditkaro_com_credit-card_bank-of-baroda-easy-credit-card.md new file mode 100644 index 0000000..2438398 --- /dev/null +++ b/cortex_on/agents/research_data/ea15e211-2645-476f-bdff-070ad4c941d7/batch_extraction_20250423_105854/2_https___www_creditkaro_com_credit-card_bank-of-baroda-easy-credit-card.md @@ -0,0 +1,144 @@ +# Content from https://www.creditkaro.com/credit-card/bank-of-baroda-easy-credit-card + +* [Credit Card](https://www.creditkaro.com/credit-card) +* [Credit Card](https://www.creditkaro.com/credit-card) +# CompareBest Credit Cards in India 2025 for Smart Choices +Select’s Card Comparison tool combines advanced tech with credible data to fuel your choice of a card that best fits your needs. +Find Card +* [ICICI Bank Credit CardView](https://www.creditkaro.com/credit-card/icici-bank/icici-bank-credit-card) +* [AU Lit Credit CardView](https://www.creditkaro.com/credit-card/au-small-finance-bank/au-lit-credit-card) +* [HDFC Bank RuPay Credit CardView](https://www.creditkaro.com/credit-card/hdfc-bank/hdfc-rupay-credit-card) +* [IDFC First Credit CardView](https://www.creditkaro.com/credit-card/idfc-first-bank/idfc-first-card) +* [BOB VIKRAM Credit CardView](https://www.creditkaro.com/credit-card/bank-of-baroda/vikram-credit-card) +* [BOB YODDHA Credit CardView](https://www.creditkaro.com/credit-card/bank-of-baroda/yoddha-credit-card) +* [HDFC Freedom Credit CardView](https://www.creditkaro.com/credit-card/hdfc-bank/freedom-credit-card) +* [HDFC Millennia Credit CardView](https://www.creditkaro.com/credit-card/hdfc-bank/millennia-credit-card) +* [HDFC Bank IRCTC Credit CardView](https://www.creditkaro.com/credit-card/hdfc-bank/irctc-credit-card) +* [HDFC Bank Tata Neu Credit CardView](https://www.creditkaro.com/credit-card/hdfc-bank/tata-neu-credit-card) +* [IDFC FIRST SWYP Credit CardView](https://www.creditkaro.com/credit-card/idfc-first-bank/idfc-swyp-credit-card) +* [IDFC FIRST Select Credit CardView](https://www.creditkaro.com/credit-card/idfc-first-bank/idfc-first-credit-card) +* [IndusInd Bank Credit CardView](https://www.creditkaro.com/credit-card/indusind-bank/indusind-credit-card) +* [Bank of Baroda Easy Credit CardView](https://www.creditkaro.com/credit-card/bank-of-baroda/easy-credit-card) +* [Bank of Baroda Select Credit CardView](https://www.creditkaro.com/credit-card/bank-of-baroda/bob-select-credit-card) +* [HDFC Swiggy Credit CardView](https://www.creditkaro.com/credit-card/hdfc-bank/swiggy-hdfc-bank-credit-card) +* [AU SwipeUp Credit CardView](https://www.creditkaro.com/credit-card/au-small-finance-bank/au-swipe-up-card) +* [SBI SimplySAVE credit cardView](https://www.creditkaro.com/credit-card/state-bank-of-india/sbi-credit-card) +* [IDFC First Bank Wow credit cardView](https://www.creditkaro.com/credit-card/idfc-first-bank/wow-credit-card) +* [Axis Bank LIC Credit CardView](https://www.creditkaro.com/credit-card/axis-bank/lic-credit-card) +* [SBI Cashback Credit CardView](https://www.creditkaro.com/credit-card/state-bank-of-india/cashback-credit-card) +* [SBI IRCTC Credit CardView](https://www.creditkaro.com/credit-card/state-bank-of-india/sbi-irctc-credit-card) +* [SimplyCLICK SBI Credit CardView](https://www.creditkaro.com/credit-card/state-bank-of-india/simplyclick-credit-card) +* [Axis Bank Credit CardView](https://www.creditkaro.com/credit-card/axis-bank/axis-bank-credit-card) +ICICI Bank Credit Card +From 50+ Options, Choose a card matching your lifestyle & needs +Card Type +Annual Fees +AU Lit Credit Card +From 50+ Options, Choose a card matching your lifestyle & needs +Card Type +Annual Fees +HDFC Bank RuPay Credit Card +Card Type +Annual Fees +IDFC First Credit Card +Card Type +Annual Fees +BOB VIKRAM Credit Card +Card Type +Annual Fees +BOB YODDHA Credit Card +Card Type +Annual Fees +BoB Varunah Premium Card +BOB Varunah Credit Card offers exclusive benefits. You can get lounge access, high credit limits, and rewards. For a premium banking experience, apply online. +Card Type +Annual Fees +HDFC Freedom Credit Card +Card Type +Annual Fees +HDFC Millennia Credit Card +The HDFC Millennia Credit Card has contactless payments, milestone rewards, lounge access, and cashback rewards for dining, entertainment, and online shopping. +Card Type +Annual Fees +HDFC Bank IRCTC Credit Card +Card Type +Annual Fees +HDFC Bank Tata Neu Credit Card +Card Type +Annual Fees +IDFC FIRST SWYP Credit Card +Get your IDFC FIRST SWYP Credit Card and enjoy a plethora of exclusive advantages! Enjoy convenient EMI choices, incredible rewards on monthly purchases, and exclusive privileges on your favourite brands. Apply now to make the most of your purchases! +Card Type +Annual Fees +IDFC FIRST Select Credit Card +Card Type +Annual Fees +IndusInd Bank Credit Card +Card Type +Annual Fees +Bank of Baroda Easy Credit Card +Card Type +Annual Fees +Bank of Baroda Select Credit Card +Card Type +Annual Fees +Niyo Global International Travel Card +Card Type +Annual Fees +HDFC Swiggy Credit Card +Save up to Rs. 42,000 anually with Swiggy HDFC Bank Credit Card +Card Type +Annual Fees +AU SwipeUp Credit Card +Card that match your upgrade lifestyle +Card Type +Annual Fees +SBI SimplySAVE credit card +Card Type +Annual Fees +IDFC First Bank Wow credit card +Apply for a WOW Credit Card which is an FD-backed Credit Card +Card Type +Cashback, Rewards +Annual Fees +Axis Bank LIC Credit Card +Card Type +Annual Fees +SBI Cashback Credit Card +Card Type +Annual Fees +SBI IRCTC Credit Card +Card Type +Annual Fees +SimplyCLICK SBI Credit Card +Card Type +Annual Fees +Axis Bank Credit Card +Earn cashback, enjoy airport lounges, fuel surcharge waivers, insurance coverage, dining discounts +Card Type +Annual Fees +## Top Credit Cards Comparison +Credit cards are essential financial tools that offer convenience, rewards, and lifestyle benefits to individuals with a steady income. They offer convenience, rewards, and lifestyle benefits. There's a card for everyone, whether you're a salaried professional, a student, a frequent traveller, or a smart saver. CreditKaro is the best credit card comparison website in India, it helps you find the best card for travel, shopping, dining, cashback, and more. You can easily compare credit cards online and apply that match your lifestyle and spending needs. +## Compare, Choose & Apply for Best Credit Cards in India Online +### Compare HDFC Credit Cards +HDFC Bank offers credit cards that cater to diverse individual needs, including shopping, travel, fuel, entertainment, dining, and more. There are both basic and premium cards with various benefits. The bank currently offers around 35 credit cards, including business credit cards. These cards enable users to make greater savings on their spending and purchases. Each card has benefits, fees, and charges that match your income and requirements. +### Compare ICICI Credit Cards +CICI Bank offers credit cards for every expense like fuel, travel, shopping, dining, and more, with options ranging from lifetime-free to premium cards. A card can be co-branded with popular partners like Amazon, HPCL, Adani, MakeMyTrip, etc. +ICICI Bank Credit Cards offer attractive welcome benefits such as Reward Points, gift vouchers, free premium memberships, and expedited cashback. Each time you swipe your card, you earn ICICI Reward Points, cashback, InterMiles, or other loyalty benefits, which can be redeemed against many items. You can unlock rewards like bonus points, gift cards, or memberships by meeting spending milestones. Enjoy travel-related perks such as free airport lounge access and discounts on flights and hotels. ICICI Credit Cards also provide free movie tickets, dining discounts, free insurance, and fuel surcharge waivers, making them an excellent choice for all your needs. You can compare credit cards India and apply for the ICICI cards that best fits your spending habits and financial needs. +### Compare SBI Credit Cards +The SBI Credit Card offers a diverse range of cards, ranging from lifestyle, rewards, shopping, travel, and fuel cards to business cards. Some of the most popular SBI credit cards are the SBI Simply Save, SBI Simply Click, Cashback SBI Card, SBI Card ELITE, and BPCL SBI Card. Each card caters to distinct needs, such as shopping, travelling, gas, groceries, and other similar ones. +SBI credit cards offer a range of benefits tailored to suit your needs. Depending on the card type, you may receive welcome bonuses such as reward points, cashback, or free memberships. You will receive points or cashback every time you use your SBI Card. Get milestone benefits such as bonus points, gift vouchers, etc. by achieving spend limits. SBI cards also add super sops for frequent travellers such as free flight tickets, access to airport lounges, and airline club memberships. You can also avail of complimentary movie tickets, discounts on BookMyShow, and discounts of up to 50% on dining. Additional benefits include golf privileges, insurance coverage, fuel surcharge waivers, zero liability protection, and annual fee waivers based on spending. +### Compare Axis Bank Credit Cards +Axis Bank offers a variety of credit cards, which offer various benefits, such as cashback, perks, and even discounts on specific brands. The cards are divided into premium, featured, co-branded, and various other segments. This variety enables cardholders to select a card that best suits their spending habits and lifestyle. +### Compare IndusInd Credit Cards +IndusInd Bank credit cards provide benefits on shopping, travel, entertainment, dining, and more. IndusInd Platinum Aura Edge, IndusInd Legend and IndusInd Platinum are some popular cards. The bank offers credit cards to everyone, whether you are new to credit or have been using it for a while. One may select any of these credit cards based on their eligibility, spending habits, and repayment capacity. +### Compare Kotak Credit Cards +Kotak Mahindra Bank offers various credit cards from basic to premium and gives rewards, cash back, free travel, cinema tickets, and more. It also offers a variety of credit cards, including lifestyle, premium, cashback, fuel, and co-branded. The popular credit cards are the Kotak PVR INOX Credit Card, the Kotak Indigo Ka-Ching 6E Rewards Credit Card, and the Kotak White Credit Card each offering unique benefits tailored to different needs. +With so many options available, a credit card comparison is a must. Use CreditKaro to choose the right card to maximise your rewards based on how much you spend. +Card Type +Annual Fee0 +Credit Score0 +Find +Compare Credit Card +Compare Credit Card +* [Apply Credit Cards](https://www.creditkaro.com/credit-card) diff --git a/cortex_on/agents/research_data/ea15e211-2645-476f-bdff-070ad4c941d7/final_report.md b/cortex_on/agents/research_data/ea15e211-2645-476f-bdff-070ad4c941d7/final_report.md new file mode 100644 index 0000000..a8efb34 --- /dev/null +++ b/cortex_on/agents/research_data/ea15e211-2645-476f-bdff-070ad4c941d7/final_report.md @@ -0,0 +1,83 @@ +# Analysis of Credit Cards in India for ₹1,00,000 Monthly Income Focusing on Shopping Rewards and Cashback: Research Report + +```markdown +# Analysis of Credit Cards in India for ₹1,00,000 Monthly Income Focusing on Shopping Rewards and Cashback + +## Executive Summary + +This research provides a comprehensive analysis of credit card options available in India for individuals earning ₹1,00,000 monthly. The focus is specifically on cards that offer rewarding shopping experiences and cashback benefits. By cataloguing offerings from major banks, the study identifies the best credit card options, evaluates their costs and benefits, and conducts a detailed cost-benefit analysis for a typical spending pattern of ₹10,000 per month. The insights aim to guide potential cardholders in maximizing benefits and rewards based on their spending habits. + +## Introduction + +With the rise in digital transactions and e-commerce, credit cards have become an essential financial tool for individuals in India. For high-income earners, especially those with a monthly income of ₹1,00,000, the choice of a credit card can significantly influence their financial returns. Cards that provide shopping rewards and cashback can offer significant savings when used optimally. This research aims to analyze such credit card options and provide a detailed comparison to help users make informed decisions. + +## Main Findings + +### Credit Card Options for ₹1,00,000 Monthly Income + +For individuals with a ₹1,00,000 monthly income, we identified five primary credit card options from major Indian banks, focusing on shopping rewards and cashback benefits. These cards meet the eligibility criteria and provide significant rewards: + +1. **HDFC Millennia Credit Card** + - **Annual Fee:** ₹1,000 + - **Key Benefits:** + - 5% cashback on online spending + - 2.5% cashback on offline retail spending + - Welcome benefits worth ₹1,000 + - **Net Annual Value:** ₹3,800 (₹4,800 in the first year) + +2. **SBI SimplyCLICK Credit Card** + - **Annual Fee:** ₹999 (waived on spending ₹1,00,000/year) + - **Key Benefits:** + - 5% cashback on online shopping + - 1% cashback on other spends + - Welcome e-gift voucher worth ₹500 + - **Net Annual Value:** ₹3,600 (₹4,100 in the first year) + +3. **ICICI Amazon Pay Credit Card** + - **Annual Fee:** ₹500 + - **Key Benefits:** + - 5% rewards on Amazon for Prime members + - 3% rewards on all other spends + - **Net Annual Value:** ₹3,820 + +4. **Axis Bank ACE Credit Card** + - **Annual Fee:** ₹499 (waived on spending ₹2,00,000/year) + - **Key Benefits:** + - 5% cashback on utilities and bill payments + - 4% cashback on Swiggy, Zomato + - 2% cashback on all other spends + - **Net Annual Value:** ₹3,701 + +5. **OneCard Credit Card** + - **Annual Fee:** Lifetime free + - **Key Benefits:** + - 5x rewards on top 2 spending categories each month + - Zero forex markup + - **Net Annual Value:** ₹3,600 + +### Cost-Benefit Analysis + +Based on a monthly spending of ₹10,000, a detailed cost-benefit analysis demonstrates the potential annual returns of each card: + +- **Best Overall Value:** OneCard Credit Card (Lifetime free, flexible reward categories) +- **Best for Online Shopping:** HDFC Millennia Credit Card (Highest absolute returns, consistent rewards) +- **Best for Amazon Shoppers:** ICICI Amazon Pay Credit Card (Strong rewards on Amazon) +- **Best for Bill Payments:** Axis Bank ACE Credit Card (Strong returns on utilities) +- **Best for Pure Online Shopping:** SBI SimplyCLICK Credit Card (Strong online shopping rewards) + +## Knowledge Gaps Identified + +Despite comprehensive analysis, several knowledge gaps were identified: +- Actual reward point conversion rates for some cards. +- Current welcome bonus offers may vary. +- Detailed terms and conditions for reward categories. +- Processing time for card approval. +- Specific merchant tie-ups and ongoing offers. + +## Conclusion + +The analysis reveals a variety of credit card options that offer substantial benefits through shopping rewards and cashback for individuals with a ₹1,00,000 monthly income. The cost-benefit analysis underscores the importance of aligning card selection with personal spending habits to maximize returns. Knowledge gaps are noted, indicating areas for further research or clarification with card issuers. + +By considering both the identified credit card features and the detailed cost-benefit findings, individuals can make informed decisions on selecting credit cards that best suit their financial goals and lifestyle. + +``` diff --git a/cortex_on/agents/research_data/ea15e211-2645-476f-bdff-070ad4c941d7/findings_task1_20250423_105913.md b/cortex_on/agents/research_data/ea15e211-2645-476f-bdff-070ad4c941d7/findings_task1_20250423_105913.md new file mode 100644 index 0000000..5ba62cb --- /dev/null +++ b/cortex_on/agents/research_data/ea15e211-2645-476f-bdff-070ad4c941d7/findings_task1_20250423_105913.md @@ -0,0 +1,293 @@ +# Findings for Task task1: Identify and catalog credit cards offered by major banks in India designed for individuals with ₹1,00,000 monthly income, focusing specifically on shopping rewards and cashback benefits. + +Based on the research for credit cards suitable for ₹1,00,000 monthly income with focus on shopping rewards and cashback, here are the key findings: + +1. HDFC Millennia Credit Card +- Annual fee: ₹1,000 +- Key benefits: + * 5% cashback on online spending + * 2.5% cashback on offline retail spending + * Welcome benefits worth ₹1,000 + * Minimum income requirement: ₹75,000 + * Good for regular online and offline shopping + +2. SBI SimplyCLICK Credit Card +- Annual fee: ₹999 (waived on spending ₹1,00,000/year) +- Key benefits: + * 5% cashback on online shopping (Amazon, Flipkart, etc.) + * 1% cashback on all other spends + * Welcome e-gift voucher worth ₹500 + * Minimum income requirement: ₹75,000 + +3. ICICI Amazon Pay Credit Card +- Annual fee: ₹500 +- Key benefits: + * 5% rewards on Amazon for Prime members + * 3% rewards on all other spends + * No joining fee + * Minimum income requirement: ₹75,000 + +4. Axis Bank ACE Credit Card +- Annual fee: ₹499 (waived on spending ₹2,00,000/year) +- Key benefits: + * 5% cashback on utilities and bill payments + * 4% cashback on Swiggy, Zomato + * 2% cashback on all other spends + * Minimum income requirement: ₹75,000 + +5. OneCard Credit Card +- Annual fee: Lifetime free +- Key benefits: + * 5x rewards on top 2 spending categories each month + * 1% cashback on all other spends + * Zero forex markup + * Minimum income requirement: ₹75,000 + * Metal card with smart app integration + +## Knowledge Gaps Identified + +- 1 +- . +- +- A +- c +- t +- u +- a +- l +- +- r +- e +- w +- a +- r +- d +- +- p +- o +- i +- n +- t +- +- c +- o +- n +- v +- e +- r +- s +- i +- o +- n +- +- r +- a +- t +- e +- s +- +- f +- o +- r +- +- s +- o +- m +- e +- +- c +- a +- r +- d +- s +- + +- 2 +- . +- +- C +- u +- r +- r +- e +- n +- t +- +- w +- e +- l +- c +- o +- m +- e +- +- b +- o +- n +- u +- s +- +- o +- f +- f +- e +- r +- s +- +- w +- h +- i +- c +- h +- +- m +- a +- y +- +- v +- a +- r +- y +- + +- 3 +- . +- +- D +- e +- t +- a +- i +- l +- e +- d +- +- t +- e +- r +- m +- s +- +- a +- n +- d +- +- c +- o +- n +- d +- i +- t +- i +- o +- n +- s +- +- f +- o +- r +- +- r +- e +- w +- a +- r +- d +- +- c +- a +- t +- e +- g +- o +- r +- i +- e +- s +- + +- 4 +- . +- +- P +- r +- o +- c +- e +- s +- s +- i +- n +- g +- +- t +- i +- m +- e +- +- f +- o +- r +- +- c +- a +- r +- d +- +- a +- p +- p +- r +- o +- v +- a +- l +- + +- 5 +- . +- +- S +- p +- e +- c +- i +- f +- i +- c +- +- m +- e +- r +- c +- h +- a +- n +- t +- +- t +- i +- e +- - +- u +- p +- s +- +- a +- n +- d +- +- o +- n +- g +- o +- i +- n +- g +- +- o +- f +- f +- e +- r +- s diff --git a/cortex_on/agents/research_data/ea15e211-2645-476f-bdff-070ad4c941d7/findings_task2_20250423_105933.md b/cortex_on/agents/research_data/ea15e211-2645-476f-bdff-070ad4c941d7/findings_task2_20250423_105933.md new file mode 100644 index 0000000..f903b0c --- /dev/null +++ b/cortex_on/agents/research_data/ea15e211-2645-476f-bdff-070ad4c941d7/findings_task2_20250423_105933.md @@ -0,0 +1,304 @@ +# Findings for Task task2: Conduct a detailed cost-benefit analysis of the identified credit cards based on a monthly spending of ₹10,000, evaluating fees, reward programs, and overall value. + +Cost-Benefit Analysis for ₹10,000 Monthly Spending + +Assuming a monthly spending of ₹10,000 (₹1,20,000 annually), here's the detailed cost-benefit analysis for each card: + +1. HDFC Millennia Credit Card +Annual Cost: ₹1,000 +Potential Annual Returns: +- Assuming 60% online (₹72,000/year) and 40% offline (₹48,000/year) spending +- Online cashback (5%): ₹3,600 +- Offline cashback (2.5%): ₹1,200 +- Welcome benefit: ₹1,000 (first year) +Net Annual Value: ₹3,800 (₹4,800 in first year) +Cost-Benefit Ratio: 1:4.8 + +2. SBI SimplyCLICK Credit Card +Annual Cost: ₹999 (waived on ₹1,00,000 spending) +Potential Annual Returns: +- Assuming 50% online shopping (₹60,000/year) +- Online shopping cashback (5%): ₹3,000 +- Other spends cashback (1%): ₹600 +- Welcome voucher: ₹500 (first year) +Net Annual Value: ₹3,600 (₹4,100 in first year) +Cost-Benefit Ratio: 1:3.6 (Effectively higher as fee is waivable) + +3. ICICI Amazon Pay Credit Card +Annual Cost: ₹500 +Potential Annual Returns: +- Amazon Prime rewards (5%): ₹1,800 (assuming 30% Amazon spending) +- Other spends rewards (3%): ₹2,520 +Net Annual Value: ₹3,820 +Cost-Benefit Ratio: 1:7.6 + +4. Axis Bank ACE Credit Card +Annual Cost: ₹499 (waivable) +Potential Annual Returns: +- Utilities/bills (5%): ₹1,800 (assuming 30% utility spending) +- Food delivery (4%): ₹1,440 (assuming 30% food delivery) +- Other spends (2%): ₹960 +Net Annual Value: ₹3,701 +Cost-Benefit Ratio: 1:7.4 (Effectively higher as fee is waivable) + +5. OneCard Credit Card +Annual Cost: Lifetime free +Potential Annual Returns: +- Top 2 categories (5x rewards ≈ 5%): ₹3,000 (assuming 50% spending in top categories) +- Other spends (1%): ₹600 +Net Annual Value: ₹3,600 +Cost-Benefit Ratio: Highest (No annual cost) + +Best Value Propositions: + +1. Best Overall Value: OneCard Credit Card +- No annual fee +- Flexible reward categories +- Good returns with no cost + +2. Best for Online Shopping: HDFC Millennia Credit Card +- Highest absolute returns +- Consistent rewards across categories +- Good welcome benefits + +3. Best for Amazon Shoppers: ICICI Amazon Pay Credit Card +- Excellent cost-benefit ratio +- Strong rewards on Amazon +- Good returns on other spends + +4. Best for Bill Payments: Axis Bank ACE Credit Card +- Strong returns on utilities and food +- Waivable annual fee +- Good all-round benefits + +5. Best for Pure Online Shopping: SBI SimplyCLICK +- Waivable annual fee +- Strong online shopping rewards +- Good welcome benefits + +## Knowledge Gaps Identified + +- 1 +- . +- +- A +- c +- t +- u +- a +- l +- +- r +- e +- w +- a +- r +- d +- +- p +- o +- i +- n +- t +- +- r +- e +- d +- e +- m +- p +- t +- i +- o +- n +- +- v +- a +- l +- u +- e +- s +- +- m +- a +- y +- +- v +- a +- r +- y +- + +- 2 +- . +- +- S +- p +- e +- c +- i +- a +- l +- +- s +- e +- a +- s +- o +- n +- a +- l +- +- o +- f +- f +- e +- r +- s +- +- n +- o +- t +- +- i +- n +- c +- l +- u +- d +- e +- d +- +- i +- n +- +- a +- n +- a +- l +- y +- s +- i +- s +- + +- 3 +- . +- +- P +- a +- r +- t +- n +- e +- r +- +- m +- e +- r +- c +- h +- a +- n +- t +- +- s +- p +- e +- c +- i +- f +- i +- c +- +- a +- d +- d +- i +- t +- i +- o +- n +- a +- l +- +- b +- e +- n +- e +- f +- i +- t +- s +- + +- 4 +- . +- +- I +- m +- p +- a +- c +- t +- +- o +- f +- +- G +- S +- T +- +- o +- n +- +- a +- n +- n +- u +- a +- l +- +- f +- e +- e +- s +- + +- 5 +- . +- +- M +- a +- x +- i +- m +- u +- m +- +- r +- e +- w +- a +- r +- d +- +- e +- a +- r +- n +- i +- n +- g +- +- c +- a +- p +- s +- +- i +- f +- +- a +- n +- y diff --git a/cortex_on/agents/research_data/ea15e211-2645-476f-bdff-070ad4c941d7/todo.json b/cortex_on/agents/research_data/ea15e211-2645-476f-bdff-070ad4c941d7/todo.json new file mode 100644 index 0000000..23d3181 --- /dev/null +++ b/cortex_on/agents/research_data/ea15e211-2645-476f-bdff-070ad4c941d7/todo.json @@ -0,0 +1,81 @@ +{ + "title": "Analysis of Credit Cards in India for ₹1,00,000 Monthly Income Focusing on Shopping Rewards and Cashback", + "description": "Comprehensive analysis of credit cards available in India for individuals with a ₹1,00,000 monthly income, focusing on cards with shopping rewards and cashback benefits, comparing their benefits, fees, and conducting a cost-benefit analysis for a monthly spending of ₹10,000.", + "todo_items": [ + { + "id": "task1", + "description": "Identify and catalog credit cards offered by major banks in India designed for individuals with ₹1,00,000 monthly income, focusing specifically on shopping rewards and cashback benefits.", + "completed": true, + "dependencies": [], + "priority": 1, + "findings_path": "/app/agents/research_data/ea15e211-2645-476f-bdff-070ad4c941d7/findings_task1_20250423_105913.md", + "completion_time": "2025-04-23T10:59:13.073788", + "knowledge_gaps": [ + "1. Actual reward point conversion rates for some cards\n2. Current welcome bonus offers which may vary\n3. Detailed terms and conditions for reward categories\n4. Processing time for card approval\n5. Specific merchant tie-ups and ongoing offers" + ] + }, + { + "id": "task2", + "description": "Conduct a detailed cost-benefit analysis of the identified credit cards based on a monthly spending of ₹10,000, evaluating fees, reward programs, and overall value.", + "completed": true, + "dependencies": [ + "task1" + ], + "priority": 2, + "findings_path": "/app/agents/research_data/ea15e211-2645-476f-bdff-070ad4c941d7/findings_task2_20250423_105933.md", + "completion_time": "2025-04-23T10:59:33.615436", + "knowledge_gaps": "1. Actual reward point redemption values may vary\n2. Special seasonal offers not included in analysis\n3. Partner merchant specific additional benefits\n4. Impact of GST on annual fees\n5. Maximum reward earning caps if any" + } + ], + "current_item_id": null, + "completed_items": [ + "task1", + "task2" + ], + "last_completed_item_id": "task2", + "knowledge_gaps": [ + "1", + ".", + " ", + "A", + "c", + "t", + "u", + "a", + "l", + "r", + "e", + "w", + "d", + "p", + "o", + "i", + "n", + "v", + "s", + "f", + "m", + "\n", + "2", + "C", + "b", + "h", + "y", + "3", + "D", + "g", + "4", + "P", + "5", + "S", + "-", + "I", + "G", + "T", + "M", + "x" + ], + "report_sections": { + "task1": "Credit Card Options Analysis for ₹1,00,000 Monthly Income\n\nFor individuals with a monthly income of ₹1,00,000, several attractive credit card options are available from major Indian banks, focusing on shopping rewards and cashback benefits. The research has identified five primary options that offer excellent value for a monthly spending of ₹10,000.\n\nThese cards have been selected based on:\n- Eligibility criteria matching the income requirement\n- Strong focus on shopping rewards and cashback\n- Reasonable annual fees with waiver options\n- Reputation of the issuing banks\n- Overall reward earning potential\n\nEach card offers unique benefits catering to different spending patterns, from online shopping to everyday purchases, with annual fees ranging from lifetime free to ₹1,000, making them suitable for various user preferences." + } +} \ No newline at end of file diff --git a/cortex_on/agents/research_data/ea15e211-2645-476f-bdff-070ad4c941d7/todo.md b/cortex_on/agents/research_data/ea15e211-2645-476f-bdff-070ad4c941d7/todo.md new file mode 100644 index 0000000..40c94d3 --- /dev/null +++ b/cortex_on/agents/research_data/ea15e211-2645-476f-bdff-070ad4c941d7/todo.md @@ -0,0 +1,55 @@ +# Research Plan: Analysis of Credit Cards in India for ₹1,00,000 Monthly Income Focusing on Shopping Rewards and Cashback + +## Description +Comprehensive analysis of credit cards available in India for individuals with a ₹1,00,000 monthly income, focusing on cards with shopping rewards and cashback benefits, comparing their benefits, fees, and conducting a cost-benefit analysis for a monthly spending of ₹10,000. + +## Progress: 2/2 tasks completed + +## Todo Items + +- [x] **Task task1** (Priority: 1): Identify and catalog credit cards offered by major banks in India designed for individuals with ₹1,00,000 monthly income, focusing specifically on shopping rewards and cashback benefits. - Completed: 2025-04-23 10:59 +- [x] **Task task2** (Priority: 2) (Depends on: task1): Conduct a detailed cost-benefit analysis of the identified credit cards based on a monthly spending of ₹10,000, evaluating fees, reward programs, and overall value. - Completed: 2025-04-23 10:59 + +## Knowledge Gaps Identified + +- 1 +- . +- +- A +- c +- t +- u +- a +- l +- r +- e +- w +- d +- p +- o +- i +- n +- v +- s +- f +- m +- + +- 2 +- C +- b +- h +- y +- 3 +- D +- g +- 4 +- P +- 5 +- S +- - +- I +- G +- T +- M +- x diff --git a/cortex_on/requirements.txt b/cortex_on/requirements.txt index 4b91dc5..e656aee 100644 --- a/cortex_on/requirements.txt +++ b/cortex_on/requirements.txt @@ -65,25 +65,12 @@ pyasn1_modules==0.4.1 pycparser==2.22 pycryptodome==3.21.0 pydantic==2.10.4 -<<<<<<< HEAD -<<<<<<< HEAD -<<<<<<< HEAD pydantic-ai==0.1.2 pydantic-ai-slim==0.1.2 -======= -pydantic-ai==0.1.0 -pydantic-ai-slim==0.1.0 ->>>>>>> 7ae43c2 (fix(pydantic_ai): Consistent code according to updated pydantic library) -======= -pydantic-ai==0.1.2 -pydantic-ai-slim==0.1.2 ->>>>>>> 53a00a5 (fix(mcp + pydantic_ai): Added proper MCP integration and server initialization) -======= +pydantic_core==2.27.2 pydantic-ai==0.1.2 pydantic-ai-slim==0.1.2 ->>>>>>> 2a9552b3398209051b836d6db168cd6209502126 mcp==1.6.0 -pydantic_core==2.27.2 Pygments==2.18.0 python-dateutil==2.9.0.post0 python-dotenv==1.0.1 From 43330d60280a3ffc0cc9d7bb4bcbd99a51d92b03 Mon Sep 17 00:00:00 2001 From: Sakalya100 Date: Wed, 23 Apr 2025 20:46:19 +0530 Subject: [PATCH 11/35] Added HITL + Planning in Phases to Server --- .../final_report.md | 57 --- .../findings_task1_20250423_085923.md | 380 ------------------ .../findings_task2_20250423_090023.md | 337 ---------------- .../findings_task3_20250423_090124.md | 312 -------------- .../findings_task4_20250423_090151.md | 2 - .../findings_task5_20250423_090220.md | 314 --------------- .../todo.json | 130 ------ .../todo.md | 64 --- .../final_report.md | 109 ----- .../findings_task1_20250423_090903.md | 224 ----------- .../findings_task2_20250423_090946.md | 244 ----------- .../findings_task3_20250423_091014.md | 216 ---------- .../findings_task4_20250423_091041.md | 216 ---------- .../findings_task5_20250423_091112.md | 236 ----------- .../findings_task6_20250423_091142.md | 194 --------- .../todo.json | 155 ------- .../todo.md | 68 ---- ...s_1fae9cf_fuel_credit_cards_comparison_.md | 4 - ...it-card_bank-of-baroda-easy-credit-card.md | 144 ------- .../final_report.md | 83 ---- .../findings_task1_20250423_105913.md | 293 -------------- .../findings_task2_20250423_105933.md | 304 -------------- .../todo.json | 81 ---- .../todo.md | 55 --- cortex_on/instructor.py | 33 +- 25 files changed, 23 insertions(+), 4232 deletions(-) delete mode 100644 cortex_on/agents/research_data/6a513487-cb89-49b9-b6b8-e75d22d661c1/final_report.md delete mode 100644 cortex_on/agents/research_data/6a513487-cb89-49b9-b6b8-e75d22d661c1/findings_task1_20250423_085923.md delete mode 100644 cortex_on/agents/research_data/6a513487-cb89-49b9-b6b8-e75d22d661c1/findings_task2_20250423_090023.md delete mode 100644 cortex_on/agents/research_data/6a513487-cb89-49b9-b6b8-e75d22d661c1/findings_task3_20250423_090124.md delete mode 100644 cortex_on/agents/research_data/6a513487-cb89-49b9-b6b8-e75d22d661c1/findings_task4_20250423_090151.md delete mode 100644 cortex_on/agents/research_data/6a513487-cb89-49b9-b6b8-e75d22d661c1/findings_task5_20250423_090220.md delete mode 100644 cortex_on/agents/research_data/6a513487-cb89-49b9-b6b8-e75d22d661c1/todo.json delete mode 100644 cortex_on/agents/research_data/6a513487-cb89-49b9-b6b8-e75d22d661c1/todo.md delete mode 100644 cortex_on/agents/research_data/e927c664-4a03-488c-852e-be747de0a3d3/final_report.md delete mode 100644 cortex_on/agents/research_data/e927c664-4a03-488c-852e-be747de0a3d3/findings_task1_20250423_090903.md delete mode 100644 cortex_on/agents/research_data/e927c664-4a03-488c-852e-be747de0a3d3/findings_task2_20250423_090946.md delete mode 100644 cortex_on/agents/research_data/e927c664-4a03-488c-852e-be747de0a3d3/findings_task3_20250423_091014.md delete mode 100644 cortex_on/agents/research_data/e927c664-4a03-488c-852e-be747de0a3d3/findings_task4_20250423_091041.md delete mode 100644 cortex_on/agents/research_data/e927c664-4a03-488c-852e-be747de0a3d3/findings_task5_20250423_091112.md delete mode 100644 cortex_on/agents/research_data/e927c664-4a03-488c-852e-be747de0a3d3/findings_task6_20250423_091142.md delete mode 100644 cortex_on/agents/research_data/e927c664-4a03-488c-852e-be747de0a3d3/todo.json delete mode 100644 cortex_on/agents/research_data/e927c664-4a03-488c-852e-be747de0a3d3/todo.md delete mode 100644 cortex_on/agents/research_data/ea15e211-2645-476f-bdff-070ad4c941d7/batch_extraction_20250423_105854/1_https___www_reddit_com_r_CreditCardsIndia_comments_1fae9cf_fuel_credit_cards_comparison_.md delete mode 100644 cortex_on/agents/research_data/ea15e211-2645-476f-bdff-070ad4c941d7/batch_extraction_20250423_105854/2_https___www_creditkaro_com_credit-card_bank-of-baroda-easy-credit-card.md delete mode 100644 cortex_on/agents/research_data/ea15e211-2645-476f-bdff-070ad4c941d7/final_report.md delete mode 100644 cortex_on/agents/research_data/ea15e211-2645-476f-bdff-070ad4c941d7/findings_task1_20250423_105913.md delete mode 100644 cortex_on/agents/research_data/ea15e211-2645-476f-bdff-070ad4c941d7/findings_task2_20250423_105933.md delete mode 100644 cortex_on/agents/research_data/ea15e211-2645-476f-bdff-070ad4c941d7/todo.json delete mode 100644 cortex_on/agents/research_data/ea15e211-2645-476f-bdff-070ad4c941d7/todo.md diff --git a/cortex_on/agents/research_data/6a513487-cb89-49b9-b6b8-e75d22d661c1/final_report.md b/cortex_on/agents/research_data/6a513487-cb89-49b9-b6b8-e75d22d661c1/final_report.md deleted file mode 100644 index 7e8e65f..0000000 --- a/cortex_on/agents/research_data/6a513487-cb89-49b9-b6b8-e75d22d661c1/final_report.md +++ /dev/null @@ -1,57 +0,0 @@ -# Best Credit Cards in India for ₹1,00,000 Monthly Income with Shopping Rewards Focus: Research Report - -# Research Report: Best Credit Cards in India for ₹1,00,000 Monthly Income with Shopping Rewards Focus - -## Executive Summary - -With the rise in consumer spending and the need for maximizing returns on purchases, selecting the right credit card is crucial for individuals in India with a monthly income of ₹1,00,000, particularly those who have shopping and travel at the core of their expenditure patterns. This report provides a comprehensive analysis of credit card options available in India, emphasizing shopping rewards, cashback benefits, and secondary emphasis on domestic travel perks. The key findings demonstrate that cards such as the YES Bank Paisabazaar PaisaSave Credit Card, Cashback SBI Card, Amazon Pay ICICI Credit Card, and Flipkart Axis Bank Credit Card offer substantial benefits based on varying user profiles and spending habits. - -## Introduction - -The Indian credit card market is burgeoning with a plethora of options tailored to diverse consumer needs. As more individuals become financially savvy, the demand for credit cards that maximize shopping rewards and cashback benefits is increasing. This report focuses on identifying the best credit cards for individuals with a monthly income of ₹1,00,000, specifically those spending approximately ₹10,000 monthly. The secondary focus is on cards offering domestic travel perks. - -## Main Findings - -### Major Banks and Credit Cards for Shopping Rewards and Cashback - -Several banks in India provide credit cards that cater to the shopping patterns of consumers seeking rewards. Key players in this space include YES Bank, State Bank of India (SBI), HDFC Bank, ICICI Bank, and Axis Bank. The cards are assessed based on reward rates, annual fees, and overall value propositions. Key highlights include: - -- **YES Bank Paisabazaar PaisaSave Credit Card** offers a holistic package of 3% cashback on online spending and 1.5% on offline purchases, with additional travel perks like lounge access. -- **Cashback SBI Card** stands out with 5% cashback on all online transactions, ideal for users with high online expenditure, although capped at ₹5,000 monthly. -- **Amazon Pay ICICI Credit Card** and **Flipkart Axis Bank Credit Card** are excellent for brand-specific shoppers, offering significant cashback on respective platforms. - -### Detailed Analysis of Reward Structures and Benefits - -The reward structures are essential in evaluating a credit card's value. Each of the identified cards provides distinct advantages: - -- **YES Bank Paisabazaar PaisaSave Credit Card** includes 3% cashback on online and 1.5% offline, complemented by lounge access and an affordable fee waiver. -- **Cashback SBI Card** provides an enticing 5% cashback online, though capped, with additional travel benefits. -- **HDFC Millennia Credit Card** supports multitier shopping with 5% cashback on select platforms and affords robust travel benefits. -- **Amazon Pay ICICI Credit Card** carries no annual fee and maximizes Amazon purchases. -- **Flipkart Axis Bank Credit Card** provides 5% cashback on Flipkart and superior travel perks. - -### Assessment of Fees, Interest Rates, and Customer Service - -Analyzing the financial aspects of these credit cards reveals the following: - -- **YES Bank Paisabazaar PaisaSave** has a nominal annual fee with a transparent fee waiver policy, but a high interest rate of 3.99% per month. -- **Cashback SBI Card** requires a ₹999 fee, waivable, with customer service rated well digitally. -- Fee waivers come into effect on meeting certain spending thresholds, making these cards efficient for regular users. - -### User Profiles: Best Card Recommendations - -For a diversified consumer base, specific credit cards deliver better across various expenditure scenarios: - -1. **Primary Online Shoppers** benefit most from the **Cashback SBI Card**, with its high cashback rate. -2. **Amazon- and Flipkart-Focused Shoppers** will maximize benefits with the **Amazon Pay ICICI Credit Card** and **Flipkart Axis Bank Credit Card**, respectively. -3. **Mixed Online-Offline Shoppers** find value in the **HDFC Millennia Credit Card** offering balanced benefits. -4. **Travel-Focused Shoppers** gain significant advantage from the **Flipkart Axis Bank Credit Card** and **HDFC Millennia Credit Card** for travel perks. - -## Conclusion - -The selection of a credit card within India's dynamic market should align with individual spending habits and lifestyle preferences. The cards identified in this report present a balanced offering of rewards, travel benefits, and manageable fees for the proposed user profile of ₹1,00,000 monthly income. Future research should aim to fill the knowledge gaps, particularly around approval criteria, customer satisfaction, points redemption timing, and partner lists to offer users a more comprehensive understanding of each card's value proposition. - ---- - -### References -The data within this report is compiled from the respective banks’ official credit card information pages and reputable financial comparison websites. Further personal investigations and consumer feedback were considered where applicable. \ No newline at end of file diff --git a/cortex_on/agents/research_data/6a513487-cb89-49b9-b6b8-e75d22d661c1/findings_task1_20250423_085923.md b/cortex_on/agents/research_data/6a513487-cb89-49b9-b6b8-e75d22d661c1/findings_task1_20250423_085923.md deleted file mode 100644 index 996b0bd..0000000 --- a/cortex_on/agents/research_data/6a513487-cb89-49b9-b6b8-e75d22d661c1/findings_task1_20250423_085923.md +++ /dev/null @@ -1,380 +0,0 @@ -# Findings for Task task1: Identify the major banks in India that offer credit cards with a focus on shopping rewards and cashback benefits. - -For a monthly income of ₹1,00,000 and monthly spend of ₹10,000 with focus on shopping rewards and cashback, the following credit cards emerge as top contenders: - -1. YES Bank Paisabazaar PaisaSave Credit Card -- 3% cashback on all online spends (capped at 5,000 points/month) -- 1.5% unlimited cashback on all other spends -- Annual fee: ₹499 (waived on spending ₹1.2 lakh/year) -- Best for unrestricted online shopping benefits - -2. Cashback SBI Card -- 5% cashback on all online spends -- 1% cashback on offline spends -- Annual fee: ₹999 (waived on spending ₹2 lakh/year) -- Monthly cashback cap of ₹5,000 - -3. HDFC Millennia Credit Card -- 5% cashback on popular platforms (Amazon, Flipkart, etc.) -- 1% cashback on other spends -- Annual fee: ₹1,000 (waived on spending ₹1 lakh/year) -- Good for multi-brand benefits - -4. Amazon Pay ICICI Credit Card -- 5% cashback on Amazon (Prime members) -- 3% cashback on Amazon (non-Prime) -- 2% cashback on partner merchants -- No annual fee -- Best for Amazon-focused shopping - -5. Flipkart Axis Bank Credit Card -- 5% cashback on Flipkart and Cleartrip -- 4% cashback on partner merchants -- 1% unlimited cashback on other spends -- Annual fee: ₹500 (waived on spending ₹3.5 lakh/year) - -## Knowledge Gaps Identified - -- 1 -- . -- -- E -- x -- a -- c -- t -- -- a -- p -- p -- r -- o -- v -- a -- l -- -- c -- r -- i -- t -- e -- r -- i -- a -- -- a -- n -- d -- -- d -- o -- c -- u -- m -- e -- n -- t -- a -- t -- i -- o -- n -- -- r -- e -- q -- u -- i -- r -- e -- m -- e -- n -- t -- s -- -- f -- o -- r -- -- e -- a -- c -- h -- -- c -- a -- r -- d -- -- i -- s -- s -- u -- e -- r -- - -- 2 -- . -- -- C -- u -- r -- r -- e -- n -- t -- -- w -- e -- l -- c -- o -- m -- e -- -- b -- o -- n -- u -- s -- -- o -- f -- f -- e -- r -- s -- -- a -- n -- d -- -- s -- e -- a -- s -- o -- n -- a -- l -- -- p -- r -- o -- m -- o -- t -- i -- o -- n -- s -- - -- 3 -- . -- -- D -- e -- t -- a -- i -- l -- e -- d -- -- r -- e -- d -- e -- m -- p -- t -- i -- o -- n -- -- p -- r -- o -- c -- e -- s -- s -- e -- s -- -- a -- n -- d -- -- r -- e -- s -- t -- r -- i -- c -- t -- i -- o -- n -- s -- -- f -- o -- r -- -- e -- a -- c -- h -- -- r -- e -- w -- a -- r -- d -- -- p -- r -- o -- g -- r -- a -- m -- - -- 4 -- . -- -- R -- e -- c -- e -- n -- t -- -- c -- u -- s -- t -- o -- m -- e -- r -- -- s -- e -- r -- v -- i -- c -- e -- -- r -- a -- t -- i -- n -- g -- s -- -- a -- n -- d -- -- c -- o -- m -- p -- l -- a -- i -- n -- t -- -- r -- e -- s -- o -- l -- u -- t -- i -- o -- n -- -- m -- e -- t -- r -- i -- c -- s -- - -- 5 -- . -- -- C -- u -- r -- r -- e -- n -- t -- -- p -- r -- o -- c -- e -- s -- s -- i -- n -- g -- -- t -- i -- m -- e -- s -- -- f -- o -- r -- -- c -- a -- r -- d -- -- a -- p -- p -- l -- i -- c -- a -- t -- i -- o -- n -- s -- -- a -- n -- d -- -- d -- e -- l -- i -- v -- e -- r -- y diff --git a/cortex_on/agents/research_data/6a513487-cb89-49b9-b6b8-e75d22d661c1/findings_task2_20250423_090023.md b/cortex_on/agents/research_data/6a513487-cb89-49b9-b6b8-e75d22d661c1/findings_task2_20250423_090023.md deleted file mode 100644 index 2de5673..0000000 --- a/cortex_on/agents/research_data/6a513487-cb89-49b9-b6b8-e75d22d661c1/findings_task2_20250423_090023.md +++ /dev/null @@ -1,337 +0,0 @@ -# Findings for Task task2: Collect detailed data on reward structures, cashback rates, welcome benefits, and domestic travel perks of credit cards from the identified banks. - -Detailed analysis of reward structures and benefits for the identified credit cards: - -1. YES Bank Paisabazaar PaisaSave Credit Card -Reward Structure: -- 3% cashback on all online spends (max 5,000 points/month) -- 1.5% unlimited cashback on offline spends -- 1% fuel surcharge waiver -Welcome Benefits: -- Zero joining fee -- Welcome bonus points on first spend -Travel Benefits: -- Complimentary domestic airport lounge access (2 per quarter) -- Travel insurance coverage - -2. Cashback SBI Card -Reward Structure: -- 5% cashback on all online spends -- 1% cashback on offline spends -- Monthly cashback cap of ₹5,000 -Welcome Benefits: -- Welcome points worth ₹500 on first spend -Travel Benefits: -- Domestic airport lounge access (4 visits per year) -- 1% fuel surcharge waiver (up to ₹100 per month) -- Basic travel insurance - -3. HDFC Millennia Credit Card -Reward Structure: -- 5% cashback on Amazon, Flipkart, and other select platforms -- 1% cashback on other spends -- Cash points redemption in 1:1 ratio -Welcome Benefits: -- 1,000 bonus cash points on joining fee payment -Travel Benefits: -- 4 domestic airport lounge visits per year -- Travel insurance coverage -- Fuel surcharge waiver - -4. Amazon Pay ICICI Credit Card -Reward Structure: -- 5% cashback for Prime members on Amazon -- 3% cashback for non-Prime members on Amazon -- 2% cashback on partner merchants -- 1% cashback on other spends -Welcome Benefits: -- No joining fee -- Amazon gift voucher on card activation -Travel Benefits: -- Basic travel insurance -- Fuel surcharge waiver at all fuel stations - -5. Flipkart Axis Bank Credit Card -Reward Structure: -- 5% cashback on Flipkart and Cleartrip -- 4% cashback on preferred partners -- 1% unlimited cashback on other spends -Welcome Benefits: -- Flipkart gift voucher worth ₹500 -- Zero joining fee for select customers -Travel Benefits: -- 4 domestic airport lounge visits per year -- Complimentary travel insurance -- 1% fuel surcharge waiver (up to ₹400/month) - -## Knowledge Gaps Identified - -- 1 -- . -- -- E -- x -- a -- c -- t -- -- p -- o -- i -- n -- t -- - -- t -- o -- - -- r -- u -- p -- e -- e -- -- c -- o -- n -- v -- e -- r -- s -- i -- o -- n -- -- r -- a -- t -- e -- s -- -- f -- o -- r -- -- r -- e -- w -- a -- r -- d -- -- p -- o -- i -- n -- t -- s -- - -- 2 -- . -- -- D -- e -- t -- a -- i -- l -- e -- d -- -- t -- e -- r -- m -- s -- -- a -- n -- d -- -- c -- o -- n -- d -- i -- t -- i -- o -- n -- s -- -- f -- o -- r -- -- l -- o -- u -- n -- g -- e -- -- a -- c -- c -- e -- s -- s -- - -- 3 -- . -- -- S -- p -- e -- c -- i -- f -- i -- c -- -- e -- x -- c -- l -- u -- s -- i -- o -- n -- s -- -- i -- n -- -- c -- a -- s -- h -- b -- a -- c -- k -- -- c -- a -- t -- e -- g -- o -- r -- i -- e -- s -- - -- 4 -- . -- -- C -- u -- r -- r -- e -- n -- t -- -- w -- e -- l -- c -- o -- m -- e -- -- b -- o -- n -- u -- s -- -- a -- m -- o -- u -- n -- t -- s -- -- ( -- m -- a -- y -- -- v -- a -- r -- y -- -- w -- i -- t -- h -- -- o -- n -- g -- o -- i -- n -- g -- -- p -- r -- o -- m -- o -- t -- i -- o -- n -- s -- ) -- - -- 5 -- . -- -- E -- x -- a -- c -- t -- -- t -- r -- a -- v -- e -- l -- -- i -- n -- s -- u -- r -- a -- n -- c -- e -- -- c -- o -- v -- e -- r -- a -- g -- e -- -- l -- i -- m -- i -- t -- s diff --git a/cortex_on/agents/research_data/6a513487-cb89-49b9-b6b8-e75d22d661c1/findings_task3_20250423_090124.md b/cortex_on/agents/research_data/6a513487-cb89-49b9-b6b8-e75d22d661c1/findings_task3_20250423_090124.md deleted file mode 100644 index d12b7e0..0000000 --- a/cortex_on/agents/research_data/6a513487-cb89-49b9-b6b8-e75d22d661c1/findings_task3_20250423_090124.md +++ /dev/null @@ -1,312 +0,0 @@ -# Findings for Task task3: Analyze the annual fees, interest rates, customer service quality, and potential hidden charges of the credit cards identified. - -Analysis of fees, interest rates, and charges for the identified credit cards: - -1. YES Bank Paisabazaar PaisaSave Credit Card -Fees: -- Annual fee: ₹499 -- Fee waiver: On spending ₹1.2 lakh/year -- Joining fee: Nil -Interest & Charges: -- Interest rate: 3.99% per month (47.88% p.a.) -- Late payment fee: Up to ₹1,000 -- Cash advance fee: 2.5% or ₹500 (whichever is higher) -Customer Service: -- 24/7 customer support -- Digital self-service options available - -2. Cashback SBI Card -Fees: -- Annual fee: ₹999 -- Fee waiver: On spending ₹2 lakh/year -- Joining fee: ₹999 -Interest & Charges: -- Interest rate: 3.35% per month (40.2% p.a.) -- Late payment fee: ₹700-1,000 based on balance -- Cash advance fee: 2.5% or ₹500 -Customer Service: -- Good digital service infrastructure -- Multiple customer support channels - -3. HDFC Millennia Credit Card -Fees: -- Annual fee: ₹1,000 -- Fee waiver: On spending ₹1 lakh/year -- Joining fee: ₹1,000 -Interest & Charges: -- Interest rate: 3.49% per month (41.88% p.a.) -- Late payment charges: ₹700-1,000 -- Cash withdrawal fee: 2.5% (min ₹500) -Customer Service: -- Strong digital banking support -- 24/7 dedicated helpline - -4. Amazon Pay ICICI Credit Card -Fees: -- Annual fee: Nil -- Joining fee: Nil -Interest & Charges: -- Interest rate: 3.50% per month (42% p.a.) -- Late payment fee: Up to ₹1,000 -- Cash advance fee: 2.5% (min ₹500) -Customer Service: -- Integration with Amazon customer service -- Digital-first support approach - -5. Flipkart Axis Bank Credit Card -Fees: -- Annual fee: ₹500 -- Fee waiver: On spending ₹3.5 lakh/year -- Joining fee: ₹500 -Interest & Charges: -- Interest rate: 3.4% per month (40.8% p.a.) -- Late payment penalty: ₹500-1,000 -- Cash withdrawal fee: 2.5% or ₹500 -Customer Service: -- Multiple support channels -- Online dispute resolution - -Hidden Charges & Important Notes: -1. Fuel surcharge waiver typically has minimum transaction requirements -2. GST applies on all fees and charges (18%) -3. EMI conversion charges vary by tenure -4. Foreign currency markup fees range from 2-3.5% -5. Card replacement charges apply for loss/damage - -## Knowledge Gaps Identified - -- 1 -- . -- -- E -- x -- a -- c -- t -- -- E -- M -- I -- -- c -- o -- n -- v -- e -- r -- s -- i -- o -- n -- -- r -- a -- t -- e -- s -- -- f -- o -- r -- -- d -- i -- f -- f -- e -- r -- e -- n -- t -- -- t -- e -- n -- u -- r -- e -- s -- - -- 2 -- . -- -- S -- p -- e -- c -- i -- f -- i -- c -- -- f -- o -- r -- e -- i -- g -- n -- -- c -- u -- r -- r -- e -- n -- c -- y -- -- t -- r -- a -- n -- s -- a -- c -- t -- i -- o -- n -- -- m -- a -- r -- k -- u -- p -- -- r -- a -- t -- e -- s -- - -- 3 -- . -- -- I -- n -- s -- u -- r -- a -- n -- c -- e -- -- c -- l -- a -- i -- m -- -- s -- e -- t -- t -- l -- e -- m -- e -- n -- t -- -- r -- a -- t -- i -- o -- s -- - -- 4 -- . -- -- C -- u -- r -- r -- e -- n -- t -- -- a -- v -- e -- r -- a -- g -- e -- -- c -- u -- s -- t -- o -- m -- e -- r -- -- s -- e -- r -- v -- i -- c -- e -- -- r -- e -- s -- p -- o -- n -- s -- e -- -- t -- i -- m -- e -- s -- - -- 5 -- . -- -- D -- e -- t -- a -- i -- l -- e -- d -- -- r -- e -- w -- a -- r -- d -- -- p -- o -- i -- n -- t -- -- e -- x -- p -- i -- r -- y -- -- t -- e -- r -- m -- s diff --git a/cortex_on/agents/research_data/6a513487-cb89-49b9-b6b8-e75d22d661c1/findings_task4_20250423_090151.md b/cortex_on/agents/research_data/6a513487-cb89-49b9-b6b8-e75d22d661c1/findings_task4_20250423_090151.md deleted file mode 100644 index b95a466..0000000 --- a/cortex_on/agents/research_data/6a513487-cb89-49b9-b6b8-e75d22d661c1/findings_task4_20250423_090151.md +++ /dev/null @@ -1,2 +0,0 @@ -# Findings for Task task4: Assess and rank the credit cards based on value provided for shopping rewards and cashback, relative to the annual fee and the user's monthly income and spending profile. - diff --git a/cortex_on/agents/research_data/6a513487-cb89-49b9-b6b8-e75d22d661c1/findings_task5_20250423_090220.md b/cortex_on/agents/research_data/6a513487-cb89-49b9-b6b8-e75d22d661c1/findings_task5_20250423_090220.md deleted file mode 100644 index 09cd3e5..0000000 --- a/cortex_on/agents/research_data/6a513487-cb89-49b9-b6b8-e75d22d661c1/findings_task5_20250423_090220.md +++ /dev/null @@ -1,314 +0,0 @@ -# Findings for Task task5: Create user profiles and scenarios (e.g., high shopping volume or frequent domestic travel) to evaluate which credit cards offer the best overall benefits. - -Analysis of credit cards for different user scenarios with ₹1,00,000 monthly income: - -1. Primary Online Shopper -Best Cards: -a) YES Bank Paisabazaar PaisaSave Credit Card -- Ideal for users who shop across multiple platforms -- 3% cashback on all online spends with no merchant restrictions -- Annual value on ₹10,000 monthly spend: ₹3,600 (minus ₹499 fee) = ₹3,101 net benefit - -b) Cashback SBI Card -- Best for high online spending -- 5% cashback on all online transactions -- Annual value on ₹10,000 monthly spend: ₹6,000 (minus ₹999 fee) = ₹5,001 net benefit - -2. Amazon-Focused Shopper -Best Card: Amazon Pay ICICI Credit Card -- Perfect for Amazon Prime members -- 5% cashback on Amazon + 1% on other spends -- No annual fee -- Annual value on ₹10,000 monthly spend (70% Amazon): ₹4,800 net benefit - -3. Flipkart-Focused Shopper -Best Card: Flipkart Axis Bank Credit Card -- Ideal for Flipkart loyal customers -- 5% cashback on Flipkart + 4% on partner merchants -- Annual value on ₹10,000 monthly spend (60% Flipkart): ₹4,920 (minus ₹500 fee) = ₹4,420 net benefit - -4. Mixed Online-Offline Shopper -Best Cards: -a) HDFC Millennia Credit Card -- Good for balanced spending -- 5% cashback on select platforms + 1% on other spends -- Annual value on mixed spending: ₹3,600 (minus ₹1,000 fee) = ₹2,600 net benefit - -b) YES Bank Paisabazaar PaisaSave -- 3% online + 1.5% offline cashback -- Better for higher offline spending ratio -- Annual value on mixed spending: ₹2,700 (minus ₹499 fee) = ₹2,201 net benefit - -5. Travel-Focused Shopper -Best Cards: -a) Flipkart Axis Bank Credit Card -- 5% cashback on Cleartrip bookings -- 4 complimentary domestic lounge visits -- Good travel insurance coverage - -b) HDFC Millennia Credit Card -- Travel insurance benefits -- Good reward rate on travel bookings -- Comprehensive lounge access - -Recommended Card Based on ₹10,000 Monthly Spend Pattern: -1. For 70% Online + 30% Offline spending: -- Best Overall: Cashback SBI Card -- Net Annual Benefit: ₹5,001 -- Additional travel benefits included - -2. For 50% Online + 50% Offline spending: -- Best Overall: YES Bank Paisabazaar PaisaSave -- Net Annual Benefit: ₹2,201 -- Most balanced reward structure - -3. For Platform-Specific Shopping: -- Amazon Dominant: Amazon Pay ICICI Card -- Flipkart Dominant: Flipkart Axis Bank Card -- Mixed Platform: YES Bank Paisabazaar PaisaSave - -Important Considerations: -1. Fee waiver thresholds are achievable with higher spending -2. Travel benefits add significant value for frequent travelers -3. Partner merchant benefits can increase overall value -4. Customer service quality varies by bank -5. Card approval likelihood varies based on credit history - -## Knowledge Gaps Identified - -- 1 -- . -- -- A -- c -- t -- u -- a -- l -- -- a -- p -- p -- r -- o -- v -- a -- l -- -- r -- a -- t -- e -- s -- -- f -- o -- r -- -- e -- a -- c -- h -- -- c -- a -- r -- d -- -- a -- t -- -- ₹ -- 1 -- , -- 0 -- 0 -- , -- 0 -- 0 -- 0 -- -- i -- n -- c -- o -- m -- e -- -- l -- e -- v -- e -- l -- - -- 2 -- . -- -- R -- e -- c -- e -- n -- t -- -- c -- u -- s -- t -- o -- m -- e -- r -- -- s -- a -- t -- i -- s -- f -- a -- c -- t -- i -- o -- n -- -- s -- c -- o -- r -- e -- s -- - -- 3 -- . -- -- A -- v -- e -- r -- a -- g -- e -- -- r -- e -- w -- a -- r -- d -- -- p -- o -- i -- n -- t -- -- r -- e -- d -- e -- m -- p -- t -- i -- o -- n -- -- t -- i -- m -- e -- - -- 4 -- . -- -- S -- p -- e -- c -- i -- f -- i -- c -- -- p -- a -- r -- t -- n -- e -- r -- -- m -- e -- r -- c -- h -- a -- n -- t -- -- l -- i -- s -- t -- s -- -- a -- n -- d -- -- o -- f -- f -- e -- r -- s -- - -- 5 -- . -- -- C -- a -- r -- d -- -- d -- e -- l -- i -- v -- e -- r -- y -- -- a -- n -- d -- -- a -- c -- t -- i -- v -- a -- t -- i -- o -- n -- -- t -- i -- m -- e -- f -- r -- a -- m -- e -- s diff --git a/cortex_on/agents/research_data/6a513487-cb89-49b9-b6b8-e75d22d661c1/todo.json b/cortex_on/agents/research_data/6a513487-cb89-49b9-b6b8-e75d22d661c1/todo.json deleted file mode 100644 index ed3ca87..0000000 --- a/cortex_on/agents/research_data/6a513487-cb89-49b9-b6b8-e75d22d661c1/todo.json +++ /dev/null @@ -1,130 +0,0 @@ -{ - "title": "Best Credit Cards in India for ₹1,00,000 Monthly Income with Shopping Rewards Focus", - "description": "Comprehensive analysis of credit cards in India focusing on shopping rewards and cashback benefits, with secondary emphasis on domestic travel perks, aimed at users with a monthly income of ₹1,00,000 and a monthly spend of ₹10,000.", - "todo_items": [ - { - "id": "task1", - "description": "Identify the major banks in India that offer credit cards with a focus on shopping rewards and cashback benefits.", - "completed": true, - "dependencies": [], - "priority": 1, - "findings_path": "/app/agents/research_data/6a513487-cb89-49b9-b6b8-e75d22d661c1/findings_task1_20250423_085923.md", - "completion_time": "2025-04-23T08:59:23.607148", - "knowledge_gaps": [ - "1. Exact approval criteria and documentation requirements for each card issuer\n2. Current welcome bonus offers and seasonal promotions\n3. Detailed redemption processes and restrictions for each reward program\n4. Recent customer service ratings and complaint resolution metrics\n5. Current processing times for card applications and delivery" - ] - }, - { - "id": "task2", - "description": "Collect detailed data on reward structures, cashback rates, welcome benefits, and domestic travel perks of credit cards from the identified banks.", - "completed": true, - "dependencies": [ - "task1" - ], - "priority": 2, - "findings_path": "/app/agents/research_data/6a513487-cb89-49b9-b6b8-e75d22d661c1/findings_task2_20250423_090023.md", - "completion_time": "2025-04-23T09:00:23.075904", - "knowledge_gaps": [ - "1. Exact point-to-rupee conversion rates for reward points\n2. Detailed terms and conditions for lounge access\n3. Specific exclusions in cashback categories\n4. Current welcome bonus amounts (may vary with ongoing promotions)\n5. Exact travel insurance coverage limits" - ] - }, - { - "id": "task3", - "description": "Analyze the annual fees, interest rates, customer service quality, and potential hidden charges of the credit cards identified.", - "completed": true, - "dependencies": [ - "task2" - ], - "priority": 3, - "findings_path": "/app/agents/research_data/6a513487-cb89-49b9-b6b8-e75d22d661c1/findings_task3_20250423_090124.md", - "completion_time": "2025-04-23T09:01:24.345512", - "knowledge_gaps": [ - "1. Exact EMI conversion rates for different tenures\n2. Specific foreign currency transaction markup rates\n3. Insurance claim settlement ratios\n4. Current average customer service response times\n5. Detailed reward point expiry terms" - ] - }, - { - "id": "task4", - "description": "Assess and rank the credit cards based on value provided for shopping rewards and cashback, relative to the annual fee and the user's monthly income and spending profile.", - "completed": true, - "dependencies": [ - "task3" - ], - "priority": 4, - "findings_path": "/app/agents/research_data/6a513487-cb89-49b9-b6b8-e75d22d661c1/findings_task4_20250423_090151.md", - "completion_time": "2025-04-23T09:01:51.548804", - "knowledge_gaps": [] - }, - { - "id": "task5", - "description": "Create user profiles and scenarios (e.g., high shopping volume or frequent domestic travel) to evaluate which credit cards offer the best overall benefits.", - "completed": true, - "dependencies": [ - "task4" - ], - "priority": 5, - "findings_path": "/app/agents/research_data/6a513487-cb89-49b9-b6b8-e75d22d661c1/findings_task5_20250423_090220.md", - "completion_time": "2025-04-23T09:02:20.343600", - "knowledge_gaps": "1. Actual approval rates for each card at ₹1,00,000 income level\n2. Recent customer satisfaction scores\n3. Average reward point redemption time\n4. Specific partner merchant lists and offers\n5. Card delivery and activation timeframes" - } - ], - "current_item_id": null, - "completed_items": [ - "task1", - "task2", - "task3", - "task4", - "task5" - ], - "last_completed_item_id": "task5", - "knowledge_gaps": [ - "1", - ".", - " ", - "E", - "x", - "a", - "c", - "t", - "p", - "r", - "o", - "v", - "l", - "i", - "e", - "n", - "d", - "u", - "m", - "q", - "s", - "f", - "h", - "\n", - "2", - "C", - "w", - "b", - "3", - "D", - "g", - "4", - "R", - "5", - "y", - "-", - "S", - "k", - "(", - ")", - "M", - "I", - "A", - "₹", - ",", - "0" - ], - "report_sections": { - "task1": "Based on the research, for a person with ₹1,00,000 monthly income and ₹10,000 monthly spend focusing on shopping rewards and cashback, several credit cards offer compelling benefits. The analysis considered reward rates, annual fees, welcome benefits, and overall value proposition.\n\nKey Findings:\n1. Best Overall Value: YES Bank Paisabazaar PaisaSave Credit Card offers the most balanced benefits with 3% cashback on all online spends and 1.5% on other transactions, making it suitable for diverse shopping needs.\n\n2. Highest Online Rewards: Cashback SBI Card provides the highest flat rate of 5% cashback on all online spending, though with a monthly cap of ₹5,000.\n\n3. Brand-Specific Benefits: Amazon Pay ICICI and Flipkart Axis Bank cards offer excellent value for loyal customers of these platforms, with up to 5% cashback and additional partner benefits.\n\n4. Annual Fee Considerations: Most cards offer fee waivers based on annual spending thresholds, ranging from ₹1 lakh to ₹3.5 lakh, making them effectively free for regular users.\n\n5. Additional Benefits: Many cards include complementary features like domestic lounge access, fuel surcharge waivers, and welcome bonuses, adding to their overall value." - } -} \ No newline at end of file diff --git a/cortex_on/agents/research_data/6a513487-cb89-49b9-b6b8-e75d22d661c1/todo.md b/cortex_on/agents/research_data/6a513487-cb89-49b9-b6b8-e75d22d661c1/todo.md deleted file mode 100644 index 28b4bee..0000000 --- a/cortex_on/agents/research_data/6a513487-cb89-49b9-b6b8-e75d22d661c1/todo.md +++ /dev/null @@ -1,64 +0,0 @@ -# Research Plan: Best Credit Cards in India for ₹1,00,000 Monthly Income with Shopping Rewards Focus - -## Description -Comprehensive analysis of credit cards in India focusing on shopping rewards and cashback benefits, with secondary emphasis on domestic travel perks, aimed at users with a monthly income of ₹1,00,000 and a monthly spend of ₹10,000. - -## Progress: 5/5 tasks completed - -## Todo Items - -- [x] **Task task1** (Priority: 1): Identify the major banks in India that offer credit cards with a focus on shopping rewards and cashback benefits. - Completed: 2025-04-23 08:59 -- [x] **Task task2** (Priority: 2) (Depends on: task1): Collect detailed data on reward structures, cashback rates, welcome benefits, and domestic travel perks of credit cards from the identified banks. - Completed: 2025-04-23 09:00 -- [x] **Task task3** (Priority: 3) (Depends on: task2): Analyze the annual fees, interest rates, customer service quality, and potential hidden charges of the credit cards identified. - Completed: 2025-04-23 09:01 -- [x] **Task task4** (Priority: 4) (Depends on: task3): Assess and rank the credit cards based on value provided for shopping rewards and cashback, relative to the annual fee and the user's monthly income and spending profile. - Completed: 2025-04-23 09:01 -- [x] **Task task5** (Priority: 5) (Depends on: task4): Create user profiles and scenarios (e.g., high shopping volume or frequent domestic travel) to evaluate which credit cards offer the best overall benefits. - Completed: 2025-04-23 09:02 - -## Knowledge Gaps Identified - -- 1 -- . -- -- E -- x -- a -- c -- t -- p -- r -- o -- v -- l -- i -- e -- n -- d -- u -- m -- q -- s -- f -- h -- - -- 2 -- C -- w -- b -- 3 -- D -- g -- 4 -- R -- 5 -- y -- - -- S -- k -- ( -- ) -- M -- I -- A -- ₹ -- , -- 0 diff --git a/cortex_on/agents/research_data/e927c664-4a03-488c-852e-be747de0a3d3/final_report.md b/cortex_on/agents/research_data/e927c664-4a03-488c-852e-be747de0a3d3/final_report.md deleted file mode 100644 index eea9621..0000000 --- a/cortex_on/agents/research_data/e927c664-4a03-488c-852e-be747de0a3d3/final_report.md +++ /dev/null @@ -1,109 +0,0 @@ -# Best Credit Cards in India for ₹1,00,000 Monthly Income: Cashback and Premium Category Analysis: Research Report - -# Research Report: Best Credit Cards in India for ₹1,00,000 Monthly Income: Cashback and Premium Category Analysis - -## Executive Summary - -This report provides a comprehensive analysis of the best premium credit cards available in India specifically designed for individuals earning ₹1,00,000 per month. The study focuses on three primary cards – SBI Cashback Credit Card, Axis Bank Ace Credit Card, and SBI SimplyCLICK Card – evaluating them based on their cashback rewards, welcome benefits, annual fees, additional perks, and user experiences. This analysis is intended to assist consumers in making informed decisions regarding credit card selection based on cashback benefits and premium features. - -## Introduction - -In the dynamic financial landscape, credit cards have become a pivotal tool for managing personal finances, offering both practicality and rewards. For individuals earning ₹1,00,000 per month, selecting a credit card that aligns with their spending habits and lifestyle is crucial. This report delves into the intricacies of three premium credit cards – SBI Cashback, Axis Bank Ace, and SBI SimplyCLICK – which are popular among this income bracket, offering robust cashback schemes and premium perks. - -## Main Findings - -### 1. Premium Credit Cards Suitable for ₹1,00,000 Monthly Income - -After identifying suitable options for the specified demographic, three credit cards emerged as ideal for individuals earning ₹1,00,000 monthly, with a spending pattern of ₹10,000 monthly. These cards provide a balanced blend of benefits, annual fees, and cashback rewards: - -- **SBI Cashback Credit Card** - - Annual Fee: ₹999 + GST, waived on ₹2 Lakh annual spend - - Cashback: 5% on online transactions, 1% on offline spends - - Best suited for online shoppers - -- **Axis Bank Ace Credit Card** - - Annual Fee: ₹499, waived on ₹2,00,000 annual spend - - Cashback: 5% on bill payments - - Offers travel benefits with lounge access - -- **SBI SimplyCLICK Card** - - Annual Fee: ₹499 + GST, waived on ₹1 Lakh annual spend - - Welcome Bonus: Amazon gift card worth ₹500 - - Benefits tailored for online shopping - -### 2. Welcome Benefits and Reward Rates Analysis - -The rewards structure of each card offers unique advantages tailored for distinct spending habits: - -- **SBI Cashback Credit Card** - - Lacks a specific welcome bonus but compensates with a robust 5% online cashback and a monthly cap of ₹5,000. -- **Axis Bank Ace Credit Card** - - Provides 2,000 reward points as a welcome bonus. Key for utility payments with a cashback cap of ₹2,000 monthly. -- **SBI SimplyCLICK Card** - - Offers lucrative online rewards: 10X points for partner merchant online purchases. - -### 3. Annual Fees and Cost Analysis - -Understanding the financial commitments involved in each card: - -- **SBI Cashback Credit Card** - - Total First Year Cost: ₹2,358 (including GST) - - Higher cost but offers significantly on cashback. -- **Axis Bank Ace Credit Card** - - Total First Year Cost: ₹499 - - Provides economical value with feasible waiver conditions. -- **SBI SimplyCLICK Card** - - Total First Year Cost: ₹1,178 (including GST) - - Most attainable fee waiver at ₹1 Lakh annual spending. - -### 4. Additional Benefits Analysis - -Each card offers various additional benefits to enhance user experience: - -- **Insurance Coverage** - - All cards offer basic fraud and purchase protection; unique offers like air accident coverage on Axis Bank Ace. -- **Travel and Lifestyle** - - Lounge access on Axis Bank Ace and dining offers across all cards, while SBI Cashback focuses on purchase protection. - -### 5. Cashback Terms and Conditions Analysis - -Evaluation of cashback programs reveals important insights: - -- **SBI Cashback Credit Card** - - Transaction exclusions impact high-frequency usage categories like fuel and government services. -- **Axis Bank Ace Credit Card** - - Notable for utility cashback but limited high-value transaction benefits. -- **SBI SimplyCLICK Card** - - Emphasizes powerful online rewards but involves complex redemption processes. - -### 6. User Experiences and Reviews Analysis - -User feedback highlights strengths and areas for improvement: - -- **SBI Cashback Credit Card** - - Favorable for online cashback but offset by high annual fees. -- **Axis Bank Ace Credit Card** - - Praised for utility bill efficiency, tempered by merchant exclusions. -- **SBI SimplyCLICK Card** - - Appreciated for online spend perks but criticized for complex rewards. - -## Conclusion - -This analysis showed that selecting a credit card is highly conditional on spending behavior and financial priorities. Each card offers distinct advantages, making them suitable for different consumer needs. The SBI Cashback Card is ideal for those focused on maximizing online shopping cashback, while Axis Bank Ace benefits utility bill payers. SBI SimplyCLICK stands out for online rewards. Careful evaluation of one's spending habits against the features of these cards will ensure the optimal choice. - -## Knowledge Gap Address and Future Research Directions - -The gaps identified during this research are acknowledged: - -- Detailed exploration of exact rewards across all merchant categories -- Current welcome bonuses and seasonal offers -- Exact cashback limits and redemption policies - -Future investigations should address customer service quality and long-term reliability of rewards programs. Monitoring updates in reward structures and customer feedback is essential for maintaining relevant consumer recommendations. - -## References - -This report synthesizes information from anonymous customer reviews, official bank documents, and financial product comparisons. Specific citations would be available upon review of source materials. - ---- -This Markdown formatted report provides an organized and accessible overview of the findings from the various research tasks undertaken. \ No newline at end of file diff --git a/cortex_on/agents/research_data/e927c664-4a03-488c-852e-be747de0a3d3/findings_task1_20250423_090903.md b/cortex_on/agents/research_data/e927c664-4a03-488c-852e-be747de0a3d3/findings_task1_20250423_090903.md deleted file mode 100644 index a063638..0000000 --- a/cortex_on/agents/research_data/e927c664-4a03-488c-852e-be747de0a3d3/findings_task1_20250423_090903.md +++ /dev/null @@ -1,224 +0,0 @@ -# Findings for Task task1: Identify the top premium credit cards available in India that are suitable for individuals with a ₹1,00,000 monthly income. - -Based on analysis of premium credit cards suitable for ₹1,00,000 monthly income and ₹10,000 monthly spending, the following cards emerge as top contenders: - -1. SBI Cashback Credit Card -- Annual Fee: ₹999 + GST (waived on ₹2 Lakh annual spend) -- 5% cashback on online spends -- 1% cashback on offline spends -- 1% fuel surcharge waiver - -2. Axis Bank Ace Credit Card -- Annual Fee: ₹499 (waived on ₹2,00,000 annual spend) -- 5% cashback on bill payments -- 4 complimentary lounge visits per year - -3. SBI SimplyCLICK Card -- Annual Fee: ₹499 + GST (waived on ₹1 Lakh annual spend) -- Amazon gift card worth ₹500 as welcome benefit -- Good rewards on online spending - -These cards are well-suited for the specified income range and spending pattern, offering good cashback benefits with reasonable annual fees that can be waived through spending. - -## Knowledge Gaps Identified - -- 1 -- . -- -- E -- x -- a -- c -- t -- -- r -- e -- w -- a -- r -- d -- -- r -- a -- t -- e -- s -- -- o -- n -- -- s -- p -- e -- c -- i -- f -- i -- c -- -- m -- e -- r -- c -- h -- a -- n -- t -- -- c -- a -- t -- e -- g -- o -- r -- i -- e -- s -- - -- 2 -- . -- -- C -- u -- r -- r -- e -- n -- t -- -- w -- e -- l -- c -- o -- m -- e -- -- b -- o -- n -- u -- s -- -- o -- f -- f -- e -- r -- s -- -- f -- o -- r -- -- Q -- 2 -- -- 2 -- 0 -- 2 -- 5 -- - -- 3 -- . -- -- D -- e -- t -- a -- i -- l -- e -- d -- -- t -- e -- r -- m -- s -- -- a -- n -- d -- -- c -- o -- n -- d -- i -- t -- i -- o -- n -- s -- -- f -- o -- r -- -- c -- a -- s -- h -- b -- a -- c -- k -- -- c -- a -- p -- s -- - -- 4 -- . -- -- R -- e -- c -- e -- n -- t -- -- u -- s -- e -- r -- -- e -- x -- p -- e -- r -- i -- e -- n -- c -- e -- s -- -- w -- i -- t -- h -- -- c -- u -- s -- t -- o -- m -- e -- r -- -- s -- e -- r -- v -- i -- c -- e diff --git a/cortex_on/agents/research_data/e927c664-4a03-488c-852e-be747de0a3d3/findings_task2_20250423_090946.md b/cortex_on/agents/research_data/e927c664-4a03-488c-852e-be747de0a3d3/findings_task2_20250423_090946.md deleted file mode 100644 index 6f28e24..0000000 --- a/cortex_on/agents/research_data/e927c664-4a03-488c-852e-be747de0a3d3/findings_task2_20250423_090946.md +++ /dev/null @@ -1,244 +0,0 @@ -# Findings for Task task2: Analyze the welcome benefits and reward rates offered by these identified premium credit cards. - -Detailed analysis of welcome benefits and reward rates for the recommended cards: - -1. SBI Cashback Credit Card: -Welcome Benefits: -- No specific welcome benefits offered -Reward Rates: -- 5% cashback on all online spends (no merchant restrictions) -- 1% cashback on offline spends -- 1% fuel surcharge waiver at all petrol pumps -- Monthly cashback cap of ₹5,000 -- Cashback credited within 2 working days of statement generation - -2. Axis Bank Ace Credit Card: -Welcome Benefits: -- 2,000 welcome reward points on first transaction -Reward Rates: -- 5% cashback on utility bill payments -- Up to 4% cashback on selected merchant categories -- 1% cashback on all other spends -- 4 complimentary domestic airport lounge visits per year - -3. SBI SimplyCLICK Card: -Welcome Benefits: -- Amazon gift card worth ₹500 -Reward Rates: -- 10X reward points on online spends at partner merchants -- 5X reward points on other online spends -- 1 reward point per ₹100 on other spends -- ₹2000 e-vouchers for spends of ₹1 lakh+ -- 1% fuel surcharge waiver - -## Knowledge Gaps Identified - -- 1 -- . -- -- M -- o -- n -- t -- h -- l -- y -- / -- a -- n -- n -- u -- a -- l -- -- c -- a -- p -- s -- -- o -- n -- -- r -- e -- w -- a -- r -- d -- s -- -- f -- o -- r -- -- A -- x -- i -- s -- -- B -- a -- n -- k -- -- A -- c -- e -- -- C -- a -- r -- d -- - -- 2 -- . -- -- S -- p -- e -- c -- i -- f -- i -- c -- -- t -- e -- r -- m -- s -- -- f -- o -- r -- -- r -- e -- w -- a -- r -- d -- -- p -- o -- i -- n -- t -- -- r -- e -- d -- e -- m -- p -- t -- i -- o -- n -- - -- 3 -- . -- -- S -- e -- a -- s -- o -- n -- a -- l -- -- p -- r -- o -- m -- o -- t -- i -- o -- n -- a -- l -- -- o -- f -- f -- e -- r -- s -- -- i -- f -- -- a -- n -- y -- - -- 4 -- . -- -- P -- a -- r -- t -- n -- e -- r -- -- m -- e -- r -- c -- h -- a -- n -- t -- -- l -- i -- s -- t -- -- f -- o -- r -- -- e -- n -- h -- a -- n -- c -- e -- d -- -- r -- e -- w -- a -- r -- d -- s -- -- o -- n -- -- S -- i -- m -- p -- l -- y -- C -- L -- I -- C -- K -- -- C -- a -- r -- d diff --git a/cortex_on/agents/research_data/e927c664-4a03-488c-852e-be747de0a3d3/findings_task3_20250423_091014.md b/cortex_on/agents/research_data/e927c664-4a03-488c-852e-be747de0a3d3/findings_task3_20250423_091014.md deleted file mode 100644 index 0621bee..0000000 --- a/cortex_on/agents/research_data/e927c664-4a03-488c-852e-be747de0a3d3/findings_task3_20250423_091014.md +++ /dev/null @@ -1,216 +0,0 @@ -# Findings for Task task3: Evaluate the annual fees associated with each of the identified premium credit cards. - -Detailed analysis of annual fees and associated conditions for each card: - -1. SBI Cashback Credit Card: -- Joining fee: ₹999 + 18% GST -- Annual fee: ₹999 + 18% GST -- Total first-year cost: ₹2,358 (including GST) -- Renewal fee waiver: Available on spending ₹2 Lakh annually -- Fee structure suitable for regular spenders who can meet waiver criteria - -2. Axis Bank Ace Credit Card: -- Joining fee: ₹499 -- Annual fee: ₹499 -- Total first-year cost: ₹499 -- Renewal fee waiver: Available on spending ₹2,00,000 annually -- Most economical option among the three with lower fees and achievable waiver threshold - -3. SBI SimplyCLICK Card: -- Joining fee: ₹499 + GST -- Annual fee: ₹499 + GST -- Total first-year cost: ₹1,178 (including GST) -- Renewal fee waiver: Available on spending ₹1 Lakh annually -- Most accessible waiver threshold, making it suitable for moderate spenders - -Comparative Analysis: -- Lowest joining fee: Axis Bank Ace Card (₹499) -- Lowest spending requirement for fee waiver: SBI SimplyCLICK (₹1 Lakh) -- Highest total fees: SBI Cashback Card (₹2,358 with GST) -- Most value for money: Axis Bank Ace Card (considering fees and benefits) - -## Knowledge Gaps Identified - -- 1 -- . -- -- P -- r -- o -- - -- r -- a -- t -- a -- -- f -- e -- e -- -- r -- e -- f -- u -- n -- d -- -- p -- o -- l -- i -- c -- i -- e -- s -- - -- 2 -- . -- -- F -- e -- e -- -- r -- e -- v -- e -- r -- s -- a -- l -- -- t -- i -- m -- e -- f -- r -- a -- m -- e -- s -- -- a -- f -- t -- e -- r -- -- m -- e -- e -- t -- i -- n -- g -- -- s -- p -- e -- n -- d -- -- c -- r -- i -- t -- e -- r -- i -- a -- - -- 3 -- . -- -- I -- m -- p -- a -- c -- t -- -- o -- f -- -- c -- a -- r -- d -- -- u -- p -- g -- r -- a -- d -- e -- s -- -- o -- n -- -- a -- n -- n -- u -- a -- l -- -- f -- e -- e -- s -- - -- 4 -- . -- -- S -- p -- e -- c -- i -- a -- l -- -- f -- e -- e -- -- w -- a -- i -- v -- e -- r -- -- p -- r -- o -- m -- o -- t -- i -- o -- n -- s -- -- f -- o -- r -- -- n -- e -- w -- -- c -- u -- s -- t -- o -- m -- e -- r -- s diff --git a/cortex_on/agents/research_data/e927c664-4a03-488c-852e-be747de0a3d3/findings_task4_20250423_091041.md b/cortex_on/agents/research_data/e927c664-4a03-488c-852e-be747de0a3d3/findings_task4_20250423_091041.md deleted file mode 100644 index b75b33d..0000000 --- a/cortex_on/agents/research_data/e927c664-4a03-488c-852e-be747de0a3d3/findings_task4_20250423_091041.md +++ /dev/null @@ -1,216 +0,0 @@ -# Findings for Task task4: Investigate additional benefits such as insurance coverage, lounge access, and concierge services provided by the credit cards. - -Analysis of additional benefits for each credit card: - -1. SBI Cashback Credit Card: -Insurance Benefits: -- Purchase protection coverage up to ₹1 lakh -- Credit shield coverage of ₹1 lakh -- Zero liability on lost card -- Fraud protection insurance - -Travel & Lifestyle: -- 1% fuel surcharge waiver nationwide -- Contactless transactions up to ₹5,000 -- Wide merchant acceptance (24+ million outlets worldwide) -- No international lounge access - -2. Axis Bank Ace Credit Card: -Insurance Benefits: -- Purchase protection -- Card liability coverage -- Air accident coverage - -Travel & Lifestyle: -- 4 complimentary domestic airport lounge visits per year -- Dining discounts at partner restaurants -- Movie ticket offers -- EMI conversion facility -- Fuel surcharge waiver at select outlets - -3. SBI SimplyCLICK Card: -Insurance Benefits: -- Purchase protection -- Lost card liability -- Fraud protection - -Travel & Lifestyle: -- 1% fuel surcharge waiver -- EasyEMI option for large purchases -- Partner merchant discounts -- Movie and entertainment offers -- Online shopping deals - -Common Features Across Cards: -- EMI conversion options -- Online transaction security -- Mobile banking apps -- 24/7 customer support -- Zero liability on fraud transactions -- SMS and email alerts - -## Knowledge Gaps Identified - -- 1 -- . -- -- E -- x -- a -- c -- t -- -- t -- e -- r -- m -- s -- -- o -- f -- -- i -- n -- s -- u -- r -- a -- n -- c -- e -- -- c -- o -- v -- e -- r -- a -- g -- e -- - -- 2 -- . -- -- S -- p -- e -- c -- i -- f -- i -- c -- -- p -- a -- r -- t -- n -- e -- r -- -- m -- e -- r -- c -- h -- a -- n -- t -- s -- -- f -- o -- r -- -- d -- i -- n -- i -- n -- g -- -- d -- i -- s -- c -- o -- u -- n -- t -- s -- - -- 3 -- . -- -- I -- n -- t -- e -- r -- n -- a -- t -- i -- o -- n -- a -- l -- -- a -- c -- c -- e -- p -- t -- a -- n -- c -- e -- -- r -- a -- t -- e -- s -- - -- 4 -- . -- -- E -- m -- e -- r -- g -- e -- n -- c -- y -- -- c -- a -- r -- d -- -- r -- e -- p -- l -- a -- c -- e -- m -- e -- n -- t -- -- s -- e -- r -- v -- i -- c -- e -- s diff --git a/cortex_on/agents/research_data/e927c664-4a03-488c-852e-be747de0a3d3/findings_task5_20250423_091112.md b/cortex_on/agents/research_data/e927c664-4a03-488c-852e-be747de0a3d3/findings_task5_20250423_091112.md deleted file mode 100644 index 80dcd71..0000000 --- a/cortex_on/agents/research_data/e927c664-4a03-488c-852e-be747de0a3d3/findings_task5_20250423_091112.md +++ /dev/null @@ -1,236 +0,0 @@ -# Findings for Task task5: Examine the terms and conditions related to cashback rewards for these credit cards, focusing on any exclusions or caps. - -Detailed analysis of cashback terms, conditions, and restrictions for each card: - -1. SBI Cashback Credit Card: -Cashback Caps: -- Monthly cashback cap: ₹5,000 -- Minimum transaction amount: None specified - -Exclusions: -- Insurance premium payments -- Fuel transactions (except 1% surcharge waiver) -- Rent payments -- Wallet loads -- Educational services -- Government services -- Jewelry purchases -- Railway bookings -- EMI transactions - -Terms: -- Cashback credited within 2 working days of statement generation -- No minimum spending requirement for cashback eligibility -- Online transactions must be in INR -- Cashback non-transferable and non-refundable - -2. Axis Bank Ace Credit Card: -Cashback Caps: -- Monthly cap on utility bill cashback: ₹2,000 -- Maximum cashback per transaction: ₹500 - -Exclusions: -- Corporate bill payments -- Government payments -- Insurance premiums -- Mutual fund investments -- School fee payments -- EMI transactions - -Terms: -- Cashback credited in next billing cycle -- Minimum transaction amount: ₹100 -- Valid only on successful transactions -- Cashback program can be modified with notice - -3. SBI SimplyCLICK Card: -Reward Points Caps: -- Maximum 10X reward points up to ₹50,000 monthly spend -- Maximum 5X reward points up to ₹25,000 monthly spend - -Exclusions: -- Cash withdrawals -- Fuel transactions (except surcharge waiver) -- Insurance payments -- Government services -- EMI conversions - -Terms: -- Points validity: 3 years -- Minimum redemption: 2,000 points -- Points forfeited on card closure -- Program terms subject to change - -## Knowledge Gaps Identified - -- 1 -- . -- -- E -- x -- a -- c -- t -- -- p -- o -- i -- n -- t -- - -- t -- o -- - -- r -- u -- p -- e -- e -- -- c -- o -- n -- v -- e -- r -- s -- i -- o -- n -- -- r -- a -- t -- e -- s -- - -- 2 -- . -- -- S -- p -- e -- c -- i -- a -- l -- -- s -- e -- a -- s -- o -- n -- a -- l -- -- e -- x -- c -- l -- u -- s -- i -- o -- n -- s -- - -- 3 -- . -- -- C -- h -- a -- n -- g -- e -- s -- -- t -- o -- -- t -- e -- r -- m -- s -- -- p -- l -- a -- n -- n -- e -- d -- -- f -- o -- r -- -- u -- p -- c -- o -- m -- i -- n -- g -- -- q -- u -- a -- r -- t -- e -- r -- s -- - -- 4 -- . -- -- I -- m -- p -- a -- c -- t -- -- o -- f -- -- c -- a -- r -- d -- -- u -- p -- g -- r -- a -- d -- e -- s -- -- o -- n -- -- e -- x -- i -- s -- t -- i -- n -- g -- -- r -- e -- w -- a -- r -- d -- s diff --git a/cortex_on/agents/research_data/e927c664-4a03-488c-852e-be747de0a3d3/findings_task6_20250423_091142.md b/cortex_on/agents/research_data/e927c664-4a03-488c-852e-be747de0a3d3/findings_task6_20250423_091142.md deleted file mode 100644 index 71c12d9..0000000 --- a/cortex_on/agents/research_data/e927c664-4a03-488c-852e-be747de0a3d3/findings_task6_20250423_091142.md +++ /dev/null @@ -1,194 +0,0 @@ -# Findings for Task task6: Gather and synthesize user experiences and reviews concerning the cashback and premium features of these credit cards. - -Analysis of user experiences and reviews for the three credit cards: - -1. SBI Cashback Credit Card: -Positive Feedback: -- Consistent 5% cashback on online transactions -- Quick cashback credit (within 2 days) -- No merchant restrictions for online purchases -- Reliable customer service - -Common Complaints: -- High annual fee compared to benefits -- Exclusion of popular payment categories -- Limited offline benefits -- No welcome benefits - -2. Axis Bank Ace Credit Card: -Positive Feedback: -- Excellent for utility bill payments -- Good customer support -- Easy fee waiver conditions -- Quick reward processing - -Common Complaints: -- Limited merchant partnerships -- Transaction caps on cashback -- Inconsistent reward rates for some categories -- App interface issues reported - -3. SBI SimplyCLICK Card: -Positive Feedback: -- Good welcome benefits -- Strong online shopping rewards -- Lower annual fee -- Easy spend-based fee waiver - -Common Complaints: -- Complex reward point system -- Limited offline benefits -- Partner merchant restrictions -- Redemption process could be simpler - -General User Sentiments: -- All cards rated good for specific use cases -- Value depends heavily on spending patterns -- Customer service varies by location -- Online transaction focus appreciated -- Fee waiver thresholds considered reasonable - -## Knowledge Gaps Identified - -- 1 -- . -- -- L -- o -- n -- g -- - -- t -- e -- r -- m -- -- r -- e -- l -- i -- a -- b -- i -- l -- i -- t -- y -- -- o -- f -- -- r -- e -- w -- a -- r -- d -- s -- -- p -- r -- o -- g -- r -- a -- m -- s -- - -- 2 -- . -- -- C -- u -- s -- t -- o -- m -- e -- r -- -- s -- e -- r -- v -- i -- c -- e -- -- r -- e -- s -- p -- o -- n -- s -- e -- -- t -- i -- m -- e -- s -- - -- 3 -- . -- -- C -- a -- r -- d -- -- u -- p -- g -- r -- a -- d -- e -- -- e -- x -- p -- e -- r -- i -- e -- n -- c -- e -- s -- - -- 4 -- . -- -- D -- i -- g -- i -- t -- a -- l -- -- p -- l -- a -- t -- f -- o -- r -- m -- -- r -- e -- l -- i -- a -- b -- i -- l -- i -- t -- y diff --git a/cortex_on/agents/research_data/e927c664-4a03-488c-852e-be747de0a3d3/todo.json b/cortex_on/agents/research_data/e927c664-4a03-488c-852e-be747de0a3d3/todo.json deleted file mode 100644 index 06166f0..0000000 --- a/cortex_on/agents/research_data/e927c664-4a03-488c-852e-be747de0a3d3/todo.json +++ /dev/null @@ -1,155 +0,0 @@ -{ - "title": "Best Credit Cards in India for ₹1,00,000 Monthly Income: Cashback and Premium Category Analysis", - "description": "Comprehensive analysis of premium credit cards in India focusing on cashback rewards for individuals with ₹1,00,000 monthly income and ₹10,000 monthly spending.", - "todo_items": [ - { - "id": "task1", - "description": "Identify the top premium credit cards available in India that are suitable for individuals with a ₹1,00,000 monthly income.", - "completed": true, - "dependencies": [], - "priority": 1, - "findings_path": "/app/agents/research_data/e927c664-4a03-488c-852e-be747de0a3d3/findings_task1_20250423_090903.md", - "completion_time": "2025-04-23T09:09:03.851871", - "knowledge_gaps": [ - "1. Exact reward rates on specific merchant categories\n2. Current welcome bonus offers for Q2 2025\n3. Detailed terms and conditions for cashback caps\n4. Recent user experiences with customer service" - ] - }, - { - "id": "task2", - "description": "Analyze the welcome benefits and reward rates offered by these identified premium credit cards.", - "completed": true, - "dependencies": [ - "task1" - ], - "priority": 2, - "findings_path": "/app/agents/research_data/e927c664-4a03-488c-852e-be747de0a3d3/findings_task2_20250423_090946.md", - "completion_time": "2025-04-23T09:09:46.028680", - "knowledge_gaps": [ - "1. Monthly/annual caps on rewards for Axis Bank Ace Card\n2. Specific terms for reward point redemption\n3. Seasonal promotional offers if any\n4. Partner merchant list for enhanced rewards on SimplyCLICK Card" - ] - }, - { - "id": "task3", - "description": "Evaluate the annual fees associated with each of the identified premium credit cards.", - "completed": true, - "dependencies": [ - "task1" - ], - "priority": 3, - "findings_path": "/app/agents/research_data/e927c664-4a03-488c-852e-be747de0a3d3/findings_task3_20250423_091014.md", - "completion_time": "2025-04-23T09:10:14.668916", - "knowledge_gaps": [ - "1. Pro-rata fee refund policies\n2. Fee reversal timeframes after meeting spend criteria\n3. Impact of card upgrades on annual fees\n4. Special fee waiver promotions for new customers" - ] - }, - { - "id": "task4", - "description": "Investigate additional benefits such as insurance coverage, lounge access, and concierge services provided by the credit cards.", - "completed": true, - "dependencies": [ - "task1" - ], - "priority": 4, - "findings_path": "/app/agents/research_data/e927c664-4a03-488c-852e-be747de0a3d3/findings_task4_20250423_091041.md", - "completion_time": "2025-04-23T09:10:41.671050", - "knowledge_gaps": [ - "1. Exact terms of insurance coverage\n2. Specific partner merchants for dining discounts\n3. International acceptance rates\n4. Emergency card replacement services" - ] - }, - { - "id": "task5", - "description": "Examine the terms and conditions related to cashback rewards for these credit cards, focusing on any exclusions or caps.", - "completed": true, - "dependencies": [ - "task2" - ], - "priority": 5, - "findings_path": "/app/agents/research_data/e927c664-4a03-488c-852e-be747de0a3d3/findings_task5_20250423_091112.md", - "completion_time": "2025-04-23T09:11:12.501090", - "knowledge_gaps": [ - "1. Exact point-to-rupee conversion rates\n2. Special seasonal exclusions\n3. Changes to terms planned for upcoming quarters\n4. Impact of card upgrades on existing rewards" - ] - }, - { - "id": "task6", - "description": "Gather and synthesize user experiences and reviews concerning the cashback and premium features of these credit cards.", - "completed": true, - "dependencies": [ - "task5" - ], - "priority": 6, - "findings_path": "/app/agents/research_data/e927c664-4a03-488c-852e-be747de0a3d3/findings_task6_20250423_091142.md", - "completion_time": "2025-04-23T09:11:42.202383", - "knowledge_gaps": "1. Long-term reliability of rewards programs\n2. Customer service response times\n3. Card upgrade experiences\n4. Digital platform reliability" - } - ], - "current_item_id": null, - "completed_items": [ - "task1", - "task2", - "task3", - "task4", - "task5", - "task6" - ], - "last_completed_item_id": "task6", - "knowledge_gaps": [ - "1", - ".", - " ", - "E", - "x", - "a", - "c", - "t", - "r", - "e", - "w", - "d", - "s", - "o", - "n", - "p", - "i", - "f", - "m", - "h", - "g", - "\n", - "2", - "C", - "u", - "l", - "b", - "Q", - "0", - "5", - "3", - "D", - "k", - "4", - "R", - "v", - "M", - "y", - "/", - "A", - "B", - "S", - "P", - "L", - "I", - "K", - "-", - "F", - "q" - ], - "report_sections": { - "task1": "## Premium Credit Cards Suitable for ₹1,00,000 Monthly Income\n\nAfter analyzing various premium credit cards available in India, we have identified several options that are well-suited for individuals with a monthly income of ₹1,00,000 and monthly spending of ₹10,000. These cards offer a good balance of cashback rewards, reasonable annual fees, and additional benefits while being accessible for the specified income range.\n\n### Top Recommendations:\n\n1. **SBI Cashback Credit Card**\n - Ideal for online shoppers\n - Strong cashback program with no merchant restrictions\n - Reasonable annual fee with waiver option\n - Comprehensive reward structure for both online and offline spending\n\n2. **Axis Bank Ace Credit Card**\n - Excellent for bill payments\n - Moderate annual fee with achievable waiver criteria\n - Additional travel benefits included\n - Good balance of rewards and utility\n\n3. **SBI SimplyCLICK Card**\n - Lower annual fee requirement\n - Attractive welcome benefits\n - Suitable for online shopping\n - Easy fee waiver threshold\n\nThese cards have been selected based on:\n- Accessibility for the specified income range\n- Strong cashback rewards aligned with spending patterns\n- Reasonable annual fees with waiver options\n- Additional benefits that add value\n- Suitable credit limit ranges", - "task2": "## Welcome Benefits and Reward Rates Analysis\n\nThe analysis of welcome benefits and reward rates reveals distinct advantages for each card:\n\n### SBI Cashback Credit Card\nThe card focuses on straightforward cashback benefits rather than welcome bonuses:\n- No welcome benefits, but strong ongoing rewards\n- Industry-leading 5% cashback on all online transactions\n- Practical 1% cashback on offline spends\n- Monthly cashback automatically credited\n- Clear caps and no complicated point systems\n\n### Axis Bank Ace Credit Card\nBalanced mix of welcome benefits and ongoing rewards:\n- Modest welcome bonus of 2,000 reward points\n- Strong focus on utility bill payments with 5% cashback\n- Tiered reward structure based on merchant categories\n- Additional travel benefits with lounge access\n- Flexible reward redemption options\n\n### SBI SimplyCLICK Card\nFocused on online shopping benefits:\n- Attractive welcome gift of ₹500 Amazon voucher\n- Enhanced rewards for online shopping\n- Accelerated reward points at partner merchants\n- Additional milestone benefits\n- E-voucher benefits for high spenders\n\nEach card offers unique reward structures suited for different spending patterns:\n- SBI Cashback: Best for heavy online spenders\n- Axis Bank Ace: Ideal for bill payments and varied spending\n- SimplyCLICK: Optimal for online shopping at partner merchants", - "task3": "## Annual Fees and Cost Analysis\n\nA detailed examination of the annual fees and associated costs reveals important considerations for each card:\n\n### SBI Cashback Credit Card\n- **Total First Year Cost**: ₹2,358 (including GST)\n - Joining fee: ₹999 + 18% GST\n - Annual fee: ₹999 + 18% GST\n- **Fee Waiver**: Available on ₹2 Lakh annual spend\n- **Cost-Benefit Analysis**: Higher fees but justified by unlimited 5% online cashback\n\n### Axis Bank Ace Credit Card\n- **Total First Year Cost**: ₹499\n - Joining fee: ₹499\n - Annual fee: ₹499\n- **Fee Waiver**: Available on ₹2,00,000 annual spend\n- **Cost-Benefit Analysis**: Most economical option with good benefits\n\n### SBI SimplyCLICK Card\n- **Total First Year Cost**: ₹1,178 (including GST)\n - Joining fee: ₹499 + GST\n - Annual fee: ₹499 + GST\n- **Fee Waiver**: Available on ₹1 Lakh annual spend\n- **Cost-Benefit Analysis**: Balanced fees with lowest spend requirement for waiver\n\n### Optimal Choice Based on Spending:\n- For ₹10,000 monthly spending (₹1.2 Lakh annually):\n - SBI SimplyCLICK Card offers guaranteed fee waiver\n - Axis Bank Ace Card requires additional ₹80,000 annual spend\n - SBI Cashback Card requires additional ₹80,000 annual spend\n\nThe fee structures are designed to encourage higher spending while offering reasonable waiver thresholds for regular users.", - "task4": "## Additional Benefits Analysis\n\nA comprehensive evaluation of additional benefits reveals varying levels of coverage and lifestyle perks across the three cards:\n\n### Insurance Coverage\nEach card offers essential protection with some variations:\n- SBI Cashback Card leads in purchase protection (₹1 lakh) and credit shield\n- Axis Bank Ace includes air accident coverage\n- All cards provide fraud protection and lost card liability\n\n### Travel Benefits\nThe cards offer different travel-related perks:\n- Axis Bank Ace stands out with 4 domestic lounge visits\n- All cards provide fuel surcharge waiver benefits\n- Wide international acceptance for all cards\n\n### Lifestyle Benefits\nEach card caters to different lifestyle needs:\n- SBI Cashback: Focus on shopping protection\n- Axis Bank Ace: Strong dining and entertainment benefits\n- SimplyCLICK: Enhanced online shopping benefits\n\n### Security Features\nAll cards maintain high security standards:\n- Zero liability on fraud\n- Real-time transaction alerts\n- Online transaction security\n- 24/7 customer support\n\nThe additional benefits complement each card's primary rewards structure, providing comprehensive coverage for different user needs.", - "task5": "## Cashback Terms and Conditions Analysis\n\nA detailed examination of the terms, conditions, and restrictions reveals important considerations for each card's reward structure:\n\n### SBI Cashback Credit Card\n**Cashback Structure:**\n- 5% online / 1% offline cashback model\n- Monthly cap: ₹5,000\n- No minimum transaction requirement\n- Quick crediting (2 working days)\n\n**Key Exclusions:**\n- Insurance and fuel transactions\n- Rent and educational payments\n- Government services\n- Jewelry purchases\n- EMI transactions\n\n### Axis Bank Ace Credit Card\n**Cashback Structure:**\n- 5% on utility bills (capped at ₹2,000/month)\n- Per-transaction cap: ₹500\n- Minimum transaction: ₹100\n\n**Key Exclusions:**\n- Corporate/Government payments\n- Insurance and investments\n- School fees\n- EMI transactions\n\n### SBI SimplyCLICK Card\n**Rewards Structure:**\n- 10X points (up to ₹50,000 monthly)\n- 5X points (up to ₹25,000 monthly)\n- Points valid for 3 years\n- Minimum redemption: 2,000 points\n\n**Key Exclusions:**\n- Cash withdrawals\n- Fuel transactions\n- Insurance payments\n- Government services\n\n### Important Considerations\n- All cards exclude essential service payments\n- Monthly caps affect high-value transactions\n- Regular spending patterns crucial for maximizing benefits\n- Terms subject to periodic review and changes", - "task6": "## User Experiences and Reviews Analysis\n\nThe analysis of user feedback provides valuable insights into real-world performance of these cards:\n\n### SBI Cashback Credit Card\n**Strengths Highlighted:**\n- Reliable 5% online cashback program\n- Quick cashback crediting\n- Straightforward rewards structure\n- Wide online acceptance\n\n**User Concerns:**\n- Annual fee considered high\n- Limited offline benefits\n- Some key category exclusions\n\n### Axis Bank Ace Credit Card\n**Strengths Highlighted:**\n- Superior for bill payments\n- Responsive customer service\n- Achievable fee waiver\n- Quick reward processing\n\n**User Concerns:**\n- Transaction caps limitation\n- Limited merchant partnerships\n- App functionality issues\n\n### SBI SimplyCLICK Card\n**Strengths Highlighted:**\n- Attractive welcome benefits\n- Strong online rewards\n- Lower fee structure\n- Easy fee waiver\n\n**User Concerns:**\n- Complex point system\n- Limited offline value\n- Redemption complexity\n\n### Overall User Sentiment\n- Cards well-suited for digital-first users\n- Value proposition depends on spending habits\n- Fee structures generally considered fair\n- Customer service experiences vary\n- Online focus appreciated by users" - } -} \ No newline at end of file diff --git a/cortex_on/agents/research_data/e927c664-4a03-488c-852e-be747de0a3d3/todo.md b/cortex_on/agents/research_data/e927c664-4a03-488c-852e-be747de0a3d3/todo.md deleted file mode 100644 index c7b0791..0000000 --- a/cortex_on/agents/research_data/e927c664-4a03-488c-852e-be747de0a3d3/todo.md +++ /dev/null @@ -1,68 +0,0 @@ -# Research Plan: Best Credit Cards in India for ₹1,00,000 Monthly Income: Cashback and Premium Category Analysis - -## Description -Comprehensive analysis of premium credit cards in India focusing on cashback rewards for individuals with ₹1,00,000 monthly income and ₹10,000 monthly spending. - -## Progress: 6/6 tasks completed - -## Todo Items - -- [x] **Task task1** (Priority: 1): Identify the top premium credit cards available in India that are suitable for individuals with a ₹1,00,000 monthly income. - Completed: 2025-04-23 09:09 -- [x] **Task task2** (Priority: 2) (Depends on: task1): Analyze the welcome benefits and reward rates offered by these identified premium credit cards. - Completed: 2025-04-23 09:09 -- [x] **Task task3** (Priority: 3) (Depends on: task1): Evaluate the annual fees associated with each of the identified premium credit cards. - Completed: 2025-04-23 09:10 -- [x] **Task task4** (Priority: 4) (Depends on: task1): Investigate additional benefits such as insurance coverage, lounge access, and concierge services provided by the credit cards. - Completed: 2025-04-23 09:10 -- [x] **Task task5** (Priority: 5) (Depends on: task2): Examine the terms and conditions related to cashback rewards for these credit cards, focusing on any exclusions or caps. - Completed: 2025-04-23 09:11 -- [x] **Task task6** (Priority: 6) (Depends on: task5): Gather and synthesize user experiences and reviews concerning the cashback and premium features of these credit cards. - Completed: 2025-04-23 09:11 - -## Knowledge Gaps Identified - -- 1 -- . -- -- E -- x -- a -- c -- t -- r -- e -- w -- d -- s -- o -- n -- p -- i -- f -- m -- h -- g -- - -- 2 -- C -- u -- l -- b -- Q -- 0 -- 5 -- 3 -- D -- k -- 4 -- R -- v -- M -- y -- / -- A -- B -- S -- P -- L -- I -- K -- - -- F -- q diff --git a/cortex_on/agents/research_data/ea15e211-2645-476f-bdff-070ad4c941d7/batch_extraction_20250423_105854/1_https___www_reddit_com_r_CreditCardsIndia_comments_1fae9cf_fuel_credit_cards_comparison_.md b/cortex_on/agents/research_data/ea15e211-2645-476f-bdff-070ad4c941d7/batch_extraction_20250423_105854/1_https___www_reddit_com_r_CreditCardsIndia_comments_1fae9cf_fuel_credit_cards_comparison_.md deleted file mode 100644 index 4d90677..0000000 --- a/cortex_on/agents/research_data/ea15e211-2645-476f-bdff-070ad4c941d7/batch_extraction_20250423_105854/1_https___www_reddit_com_r_CreditCardsIndia_comments_1fae9cf_fuel_credit_cards_comparison_.md +++ /dev/null @@ -1,4 +0,0 @@ -# Content from https://www.reddit.com/r/CreditCardsIndia/comments/1fae9cf/fuel_credit_cards_comparison/ - -r/CreditCardsIndia is a community for discussing credit cards in India—rewards, benefits, bank policies, and more. Stay informed and make smarter financial decisions. -# Fuel credit cards comparison diff --git a/cortex_on/agents/research_data/ea15e211-2645-476f-bdff-070ad4c941d7/batch_extraction_20250423_105854/2_https___www_creditkaro_com_credit-card_bank-of-baroda-easy-credit-card.md b/cortex_on/agents/research_data/ea15e211-2645-476f-bdff-070ad4c941d7/batch_extraction_20250423_105854/2_https___www_creditkaro_com_credit-card_bank-of-baroda-easy-credit-card.md deleted file mode 100644 index 2438398..0000000 --- a/cortex_on/agents/research_data/ea15e211-2645-476f-bdff-070ad4c941d7/batch_extraction_20250423_105854/2_https___www_creditkaro_com_credit-card_bank-of-baroda-easy-credit-card.md +++ /dev/null @@ -1,144 +0,0 @@ -# Content from https://www.creditkaro.com/credit-card/bank-of-baroda-easy-credit-card - -* [Credit Card](https://www.creditkaro.com/credit-card) -* [Credit Card](https://www.creditkaro.com/credit-card) -# CompareBest Credit Cards in India 2025 for Smart Choices -Select’s Card Comparison tool combines advanced tech with credible data to fuel your choice of a card that best fits your needs. -Find Card -* [ICICI Bank Credit CardView](https://www.creditkaro.com/credit-card/icici-bank/icici-bank-credit-card) -* [AU Lit Credit CardView](https://www.creditkaro.com/credit-card/au-small-finance-bank/au-lit-credit-card) -* [HDFC Bank RuPay Credit CardView](https://www.creditkaro.com/credit-card/hdfc-bank/hdfc-rupay-credit-card) -* [IDFC First Credit CardView](https://www.creditkaro.com/credit-card/idfc-first-bank/idfc-first-card) -* [BOB VIKRAM Credit CardView](https://www.creditkaro.com/credit-card/bank-of-baroda/vikram-credit-card) -* [BOB YODDHA Credit CardView](https://www.creditkaro.com/credit-card/bank-of-baroda/yoddha-credit-card) -* [HDFC Freedom Credit CardView](https://www.creditkaro.com/credit-card/hdfc-bank/freedom-credit-card) -* [HDFC Millennia Credit CardView](https://www.creditkaro.com/credit-card/hdfc-bank/millennia-credit-card) -* [HDFC Bank IRCTC Credit CardView](https://www.creditkaro.com/credit-card/hdfc-bank/irctc-credit-card) -* [HDFC Bank Tata Neu Credit CardView](https://www.creditkaro.com/credit-card/hdfc-bank/tata-neu-credit-card) -* [IDFC FIRST SWYP Credit CardView](https://www.creditkaro.com/credit-card/idfc-first-bank/idfc-swyp-credit-card) -* [IDFC FIRST Select Credit CardView](https://www.creditkaro.com/credit-card/idfc-first-bank/idfc-first-credit-card) -* [IndusInd Bank Credit CardView](https://www.creditkaro.com/credit-card/indusind-bank/indusind-credit-card) -* [Bank of Baroda Easy Credit CardView](https://www.creditkaro.com/credit-card/bank-of-baroda/easy-credit-card) -* [Bank of Baroda Select Credit CardView](https://www.creditkaro.com/credit-card/bank-of-baroda/bob-select-credit-card) -* [HDFC Swiggy Credit CardView](https://www.creditkaro.com/credit-card/hdfc-bank/swiggy-hdfc-bank-credit-card) -* [AU SwipeUp Credit CardView](https://www.creditkaro.com/credit-card/au-small-finance-bank/au-swipe-up-card) -* [SBI SimplySAVE credit cardView](https://www.creditkaro.com/credit-card/state-bank-of-india/sbi-credit-card) -* [IDFC First Bank Wow credit cardView](https://www.creditkaro.com/credit-card/idfc-first-bank/wow-credit-card) -* [Axis Bank LIC Credit CardView](https://www.creditkaro.com/credit-card/axis-bank/lic-credit-card) -* [SBI Cashback Credit CardView](https://www.creditkaro.com/credit-card/state-bank-of-india/cashback-credit-card) -* [SBI IRCTC Credit CardView](https://www.creditkaro.com/credit-card/state-bank-of-india/sbi-irctc-credit-card) -* [SimplyCLICK SBI Credit CardView](https://www.creditkaro.com/credit-card/state-bank-of-india/simplyclick-credit-card) -* [Axis Bank Credit CardView](https://www.creditkaro.com/credit-card/axis-bank/axis-bank-credit-card) -ICICI Bank Credit Card -From 50+ Options, Choose a card matching your lifestyle & needs -Card Type -Annual Fees -AU Lit Credit Card -From 50+ Options, Choose a card matching your lifestyle & needs -Card Type -Annual Fees -HDFC Bank RuPay Credit Card -Card Type -Annual Fees -IDFC First Credit Card -Card Type -Annual Fees -BOB VIKRAM Credit Card -Card Type -Annual Fees -BOB YODDHA Credit Card -Card Type -Annual Fees -BoB Varunah Premium Card -BOB Varunah Credit Card offers exclusive benefits. You can get lounge access, high credit limits, and rewards. For a premium banking experience, apply online. -Card Type -Annual Fees -HDFC Freedom Credit Card -Card Type -Annual Fees -HDFC Millennia Credit Card -The HDFC Millennia Credit Card has contactless payments, milestone rewards, lounge access, and cashback rewards for dining, entertainment, and online shopping. -Card Type -Annual Fees -HDFC Bank IRCTC Credit Card -Card Type -Annual Fees -HDFC Bank Tata Neu Credit Card -Card Type -Annual Fees -IDFC FIRST SWYP Credit Card -Get your IDFC FIRST SWYP Credit Card and enjoy a plethora of exclusive advantages! Enjoy convenient EMI choices, incredible rewards on monthly purchases, and exclusive privileges on your favourite brands. Apply now to make the most of your purchases! -Card Type -Annual Fees -IDFC FIRST Select Credit Card -Card Type -Annual Fees -IndusInd Bank Credit Card -Card Type -Annual Fees -Bank of Baroda Easy Credit Card -Card Type -Annual Fees -Bank of Baroda Select Credit Card -Card Type -Annual Fees -Niyo Global International Travel Card -Card Type -Annual Fees -HDFC Swiggy Credit Card -Save up to Rs. 42,000 anually with Swiggy HDFC Bank Credit Card -Card Type -Annual Fees -AU SwipeUp Credit Card -Card that match your upgrade lifestyle -Card Type -Annual Fees -SBI SimplySAVE credit card -Card Type -Annual Fees -IDFC First Bank Wow credit card -Apply for a WOW Credit Card which is an FD-backed Credit Card -Card Type -Cashback, Rewards -Annual Fees -Axis Bank LIC Credit Card -Card Type -Annual Fees -SBI Cashback Credit Card -Card Type -Annual Fees -SBI IRCTC Credit Card -Card Type -Annual Fees -SimplyCLICK SBI Credit Card -Card Type -Annual Fees -Axis Bank Credit Card -Earn cashback, enjoy airport lounges, fuel surcharge waivers, insurance coverage, dining discounts -Card Type -Annual Fees -## Top Credit Cards Comparison -Credit cards are essential financial tools that offer convenience, rewards, and lifestyle benefits to individuals with a steady income. They offer convenience, rewards, and lifestyle benefits. There's a card for everyone, whether you're a salaried professional, a student, a frequent traveller, or a smart saver. CreditKaro is the best credit card comparison website in India, it helps you find the best card for travel, shopping, dining, cashback, and more. You can easily compare credit cards online and apply that match your lifestyle and spending needs. -## Compare, Choose & Apply for Best Credit Cards in India Online -### Compare HDFC Credit Cards -HDFC Bank offers credit cards that cater to diverse individual needs, including shopping, travel, fuel, entertainment, dining, and more. There are both basic and premium cards with various benefits. The bank currently offers around 35 credit cards, including business credit cards. These cards enable users to make greater savings on their spending and purchases. Each card has benefits, fees, and charges that match your income and requirements. -### Compare ICICI Credit Cards -CICI Bank offers credit cards for every expense like fuel, travel, shopping, dining, and more, with options ranging from lifetime-free to premium cards. A card can be co-branded with popular partners like Amazon, HPCL, Adani, MakeMyTrip, etc. -ICICI Bank Credit Cards offer attractive welcome benefits such as Reward Points, gift vouchers, free premium memberships, and expedited cashback. Each time you swipe your card, you earn ICICI Reward Points, cashback, InterMiles, or other loyalty benefits, which can be redeemed against many items. You can unlock rewards like bonus points, gift cards, or memberships by meeting spending milestones. Enjoy travel-related perks such as free airport lounge access and discounts on flights and hotels. ICICI Credit Cards also provide free movie tickets, dining discounts, free insurance, and fuel surcharge waivers, making them an excellent choice for all your needs. You can compare credit cards India and apply for the ICICI cards that best fits your spending habits and financial needs. -### Compare SBI Credit Cards -The SBI Credit Card offers a diverse range of cards, ranging from lifestyle, rewards, shopping, travel, and fuel cards to business cards. Some of the most popular SBI credit cards are the SBI Simply Save, SBI Simply Click, Cashback SBI Card, SBI Card ELITE, and BPCL SBI Card. Each card caters to distinct needs, such as shopping, travelling, gas, groceries, and other similar ones. -SBI credit cards offer a range of benefits tailored to suit your needs. Depending on the card type, you may receive welcome bonuses such as reward points, cashback, or free memberships. You will receive points or cashback every time you use your SBI Card. Get milestone benefits such as bonus points, gift vouchers, etc. by achieving spend limits. SBI cards also add super sops for frequent travellers such as free flight tickets, access to airport lounges, and airline club memberships. You can also avail of complimentary movie tickets, discounts on BookMyShow, and discounts of up to 50% on dining. Additional benefits include golf privileges, insurance coverage, fuel surcharge waivers, zero liability protection, and annual fee waivers based on spending. -### Compare Axis Bank Credit Cards -Axis Bank offers a variety of credit cards, which offer various benefits, such as cashback, perks, and even discounts on specific brands. The cards are divided into premium, featured, co-branded, and various other segments. This variety enables cardholders to select a card that best suits their spending habits and lifestyle. -### Compare IndusInd Credit Cards -IndusInd Bank credit cards provide benefits on shopping, travel, entertainment, dining, and more. IndusInd Platinum Aura Edge, IndusInd Legend and IndusInd Platinum are some popular cards. The bank offers credit cards to everyone, whether you are new to credit or have been using it for a while. One may select any of these credit cards based on their eligibility, spending habits, and repayment capacity. -### Compare Kotak Credit Cards -Kotak Mahindra Bank offers various credit cards from basic to premium and gives rewards, cash back, free travel, cinema tickets, and more. It also offers a variety of credit cards, including lifestyle, premium, cashback, fuel, and co-branded. The popular credit cards are the Kotak PVR INOX Credit Card, the Kotak Indigo Ka-Ching 6E Rewards Credit Card, and the Kotak White Credit Card each offering unique benefits tailored to different needs. -With so many options available, a credit card comparison is a must. Use CreditKaro to choose the right card to maximise your rewards based on how much you spend. -Card Type -Annual Fee0 -Credit Score0 -Find -Compare Credit Card -Compare Credit Card -* [Apply Credit Cards](https://www.creditkaro.com/credit-card) diff --git a/cortex_on/agents/research_data/ea15e211-2645-476f-bdff-070ad4c941d7/final_report.md b/cortex_on/agents/research_data/ea15e211-2645-476f-bdff-070ad4c941d7/final_report.md deleted file mode 100644 index a8efb34..0000000 --- a/cortex_on/agents/research_data/ea15e211-2645-476f-bdff-070ad4c941d7/final_report.md +++ /dev/null @@ -1,83 +0,0 @@ -# Analysis of Credit Cards in India for ₹1,00,000 Monthly Income Focusing on Shopping Rewards and Cashback: Research Report - -```markdown -# Analysis of Credit Cards in India for ₹1,00,000 Monthly Income Focusing on Shopping Rewards and Cashback - -## Executive Summary - -This research provides a comprehensive analysis of credit card options available in India for individuals earning ₹1,00,000 monthly. The focus is specifically on cards that offer rewarding shopping experiences and cashback benefits. By cataloguing offerings from major banks, the study identifies the best credit card options, evaluates their costs and benefits, and conducts a detailed cost-benefit analysis for a typical spending pattern of ₹10,000 per month. The insights aim to guide potential cardholders in maximizing benefits and rewards based on their spending habits. - -## Introduction - -With the rise in digital transactions and e-commerce, credit cards have become an essential financial tool for individuals in India. For high-income earners, especially those with a monthly income of ₹1,00,000, the choice of a credit card can significantly influence their financial returns. Cards that provide shopping rewards and cashback can offer significant savings when used optimally. This research aims to analyze such credit card options and provide a detailed comparison to help users make informed decisions. - -## Main Findings - -### Credit Card Options for ₹1,00,000 Monthly Income - -For individuals with a ₹1,00,000 monthly income, we identified five primary credit card options from major Indian banks, focusing on shopping rewards and cashback benefits. These cards meet the eligibility criteria and provide significant rewards: - -1. **HDFC Millennia Credit Card** - - **Annual Fee:** ₹1,000 - - **Key Benefits:** - - 5% cashback on online spending - - 2.5% cashback on offline retail spending - - Welcome benefits worth ₹1,000 - - **Net Annual Value:** ₹3,800 (₹4,800 in the first year) - -2. **SBI SimplyCLICK Credit Card** - - **Annual Fee:** ₹999 (waived on spending ₹1,00,000/year) - - **Key Benefits:** - - 5% cashback on online shopping - - 1% cashback on other spends - - Welcome e-gift voucher worth ₹500 - - **Net Annual Value:** ₹3,600 (₹4,100 in the first year) - -3. **ICICI Amazon Pay Credit Card** - - **Annual Fee:** ₹500 - - **Key Benefits:** - - 5% rewards on Amazon for Prime members - - 3% rewards on all other spends - - **Net Annual Value:** ₹3,820 - -4. **Axis Bank ACE Credit Card** - - **Annual Fee:** ₹499 (waived on spending ₹2,00,000/year) - - **Key Benefits:** - - 5% cashback on utilities and bill payments - - 4% cashback on Swiggy, Zomato - - 2% cashback on all other spends - - **Net Annual Value:** ₹3,701 - -5. **OneCard Credit Card** - - **Annual Fee:** Lifetime free - - **Key Benefits:** - - 5x rewards on top 2 spending categories each month - - Zero forex markup - - **Net Annual Value:** ₹3,600 - -### Cost-Benefit Analysis - -Based on a monthly spending of ₹10,000, a detailed cost-benefit analysis demonstrates the potential annual returns of each card: - -- **Best Overall Value:** OneCard Credit Card (Lifetime free, flexible reward categories) -- **Best for Online Shopping:** HDFC Millennia Credit Card (Highest absolute returns, consistent rewards) -- **Best for Amazon Shoppers:** ICICI Amazon Pay Credit Card (Strong rewards on Amazon) -- **Best for Bill Payments:** Axis Bank ACE Credit Card (Strong returns on utilities) -- **Best for Pure Online Shopping:** SBI SimplyCLICK Credit Card (Strong online shopping rewards) - -## Knowledge Gaps Identified - -Despite comprehensive analysis, several knowledge gaps were identified: -- Actual reward point conversion rates for some cards. -- Current welcome bonus offers may vary. -- Detailed terms and conditions for reward categories. -- Processing time for card approval. -- Specific merchant tie-ups and ongoing offers. - -## Conclusion - -The analysis reveals a variety of credit card options that offer substantial benefits through shopping rewards and cashback for individuals with a ₹1,00,000 monthly income. The cost-benefit analysis underscores the importance of aligning card selection with personal spending habits to maximize returns. Knowledge gaps are noted, indicating areas for further research or clarification with card issuers. - -By considering both the identified credit card features and the detailed cost-benefit findings, individuals can make informed decisions on selecting credit cards that best suit their financial goals and lifestyle. - -``` diff --git a/cortex_on/agents/research_data/ea15e211-2645-476f-bdff-070ad4c941d7/findings_task1_20250423_105913.md b/cortex_on/agents/research_data/ea15e211-2645-476f-bdff-070ad4c941d7/findings_task1_20250423_105913.md deleted file mode 100644 index 5ba62cb..0000000 --- a/cortex_on/agents/research_data/ea15e211-2645-476f-bdff-070ad4c941d7/findings_task1_20250423_105913.md +++ /dev/null @@ -1,293 +0,0 @@ -# Findings for Task task1: Identify and catalog credit cards offered by major banks in India designed for individuals with ₹1,00,000 monthly income, focusing specifically on shopping rewards and cashback benefits. - -Based on the research for credit cards suitable for ₹1,00,000 monthly income with focus on shopping rewards and cashback, here are the key findings: - -1. HDFC Millennia Credit Card -- Annual fee: ₹1,000 -- Key benefits: - * 5% cashback on online spending - * 2.5% cashback on offline retail spending - * Welcome benefits worth ₹1,000 - * Minimum income requirement: ₹75,000 - * Good for regular online and offline shopping - -2. SBI SimplyCLICK Credit Card -- Annual fee: ₹999 (waived on spending ₹1,00,000/year) -- Key benefits: - * 5% cashback on online shopping (Amazon, Flipkart, etc.) - * 1% cashback on all other spends - * Welcome e-gift voucher worth ₹500 - * Minimum income requirement: ₹75,000 - -3. ICICI Amazon Pay Credit Card -- Annual fee: ₹500 -- Key benefits: - * 5% rewards on Amazon for Prime members - * 3% rewards on all other spends - * No joining fee - * Minimum income requirement: ₹75,000 - -4. Axis Bank ACE Credit Card -- Annual fee: ₹499 (waived on spending ₹2,00,000/year) -- Key benefits: - * 5% cashback on utilities and bill payments - * 4% cashback on Swiggy, Zomato - * 2% cashback on all other spends - * Minimum income requirement: ₹75,000 - -5. OneCard Credit Card -- Annual fee: Lifetime free -- Key benefits: - * 5x rewards on top 2 spending categories each month - * 1% cashback on all other spends - * Zero forex markup - * Minimum income requirement: ₹75,000 - * Metal card with smart app integration - -## Knowledge Gaps Identified - -- 1 -- . -- -- A -- c -- t -- u -- a -- l -- -- r -- e -- w -- a -- r -- d -- -- p -- o -- i -- n -- t -- -- c -- o -- n -- v -- e -- r -- s -- i -- o -- n -- -- r -- a -- t -- e -- s -- -- f -- o -- r -- -- s -- o -- m -- e -- -- c -- a -- r -- d -- s -- - -- 2 -- . -- -- C -- u -- r -- r -- e -- n -- t -- -- w -- e -- l -- c -- o -- m -- e -- -- b -- o -- n -- u -- s -- -- o -- f -- f -- e -- r -- s -- -- w -- h -- i -- c -- h -- -- m -- a -- y -- -- v -- a -- r -- y -- - -- 3 -- . -- -- D -- e -- t -- a -- i -- l -- e -- d -- -- t -- e -- r -- m -- s -- -- a -- n -- d -- -- c -- o -- n -- d -- i -- t -- i -- o -- n -- s -- -- f -- o -- r -- -- r -- e -- w -- a -- r -- d -- -- c -- a -- t -- e -- g -- o -- r -- i -- e -- s -- - -- 4 -- . -- -- P -- r -- o -- c -- e -- s -- s -- i -- n -- g -- -- t -- i -- m -- e -- -- f -- o -- r -- -- c -- a -- r -- d -- -- a -- p -- p -- r -- o -- v -- a -- l -- - -- 5 -- . -- -- S -- p -- e -- c -- i -- f -- i -- c -- -- m -- e -- r -- c -- h -- a -- n -- t -- -- t -- i -- e -- - -- u -- p -- s -- -- a -- n -- d -- -- o -- n -- g -- o -- i -- n -- g -- -- o -- f -- f -- e -- r -- s diff --git a/cortex_on/agents/research_data/ea15e211-2645-476f-bdff-070ad4c941d7/findings_task2_20250423_105933.md b/cortex_on/agents/research_data/ea15e211-2645-476f-bdff-070ad4c941d7/findings_task2_20250423_105933.md deleted file mode 100644 index f903b0c..0000000 --- a/cortex_on/agents/research_data/ea15e211-2645-476f-bdff-070ad4c941d7/findings_task2_20250423_105933.md +++ /dev/null @@ -1,304 +0,0 @@ -# Findings for Task task2: Conduct a detailed cost-benefit analysis of the identified credit cards based on a monthly spending of ₹10,000, evaluating fees, reward programs, and overall value. - -Cost-Benefit Analysis for ₹10,000 Monthly Spending - -Assuming a monthly spending of ₹10,000 (₹1,20,000 annually), here's the detailed cost-benefit analysis for each card: - -1. HDFC Millennia Credit Card -Annual Cost: ₹1,000 -Potential Annual Returns: -- Assuming 60% online (₹72,000/year) and 40% offline (₹48,000/year) spending -- Online cashback (5%): ₹3,600 -- Offline cashback (2.5%): ₹1,200 -- Welcome benefit: ₹1,000 (first year) -Net Annual Value: ₹3,800 (₹4,800 in first year) -Cost-Benefit Ratio: 1:4.8 - -2. SBI SimplyCLICK Credit Card -Annual Cost: ₹999 (waived on ₹1,00,000 spending) -Potential Annual Returns: -- Assuming 50% online shopping (₹60,000/year) -- Online shopping cashback (5%): ₹3,000 -- Other spends cashback (1%): ₹600 -- Welcome voucher: ₹500 (first year) -Net Annual Value: ₹3,600 (₹4,100 in first year) -Cost-Benefit Ratio: 1:3.6 (Effectively higher as fee is waivable) - -3. ICICI Amazon Pay Credit Card -Annual Cost: ₹500 -Potential Annual Returns: -- Amazon Prime rewards (5%): ₹1,800 (assuming 30% Amazon spending) -- Other spends rewards (3%): ₹2,520 -Net Annual Value: ₹3,820 -Cost-Benefit Ratio: 1:7.6 - -4. Axis Bank ACE Credit Card -Annual Cost: ₹499 (waivable) -Potential Annual Returns: -- Utilities/bills (5%): ₹1,800 (assuming 30% utility spending) -- Food delivery (4%): ₹1,440 (assuming 30% food delivery) -- Other spends (2%): ₹960 -Net Annual Value: ₹3,701 -Cost-Benefit Ratio: 1:7.4 (Effectively higher as fee is waivable) - -5. OneCard Credit Card -Annual Cost: Lifetime free -Potential Annual Returns: -- Top 2 categories (5x rewards ≈ 5%): ₹3,000 (assuming 50% spending in top categories) -- Other spends (1%): ₹600 -Net Annual Value: ₹3,600 -Cost-Benefit Ratio: Highest (No annual cost) - -Best Value Propositions: - -1. Best Overall Value: OneCard Credit Card -- No annual fee -- Flexible reward categories -- Good returns with no cost - -2. Best for Online Shopping: HDFC Millennia Credit Card -- Highest absolute returns -- Consistent rewards across categories -- Good welcome benefits - -3. Best for Amazon Shoppers: ICICI Amazon Pay Credit Card -- Excellent cost-benefit ratio -- Strong rewards on Amazon -- Good returns on other spends - -4. Best for Bill Payments: Axis Bank ACE Credit Card -- Strong returns on utilities and food -- Waivable annual fee -- Good all-round benefits - -5. Best for Pure Online Shopping: SBI SimplyCLICK -- Waivable annual fee -- Strong online shopping rewards -- Good welcome benefits - -## Knowledge Gaps Identified - -- 1 -- . -- -- A -- c -- t -- u -- a -- l -- -- r -- e -- w -- a -- r -- d -- -- p -- o -- i -- n -- t -- -- r -- e -- d -- e -- m -- p -- t -- i -- o -- n -- -- v -- a -- l -- u -- e -- s -- -- m -- a -- y -- -- v -- a -- r -- y -- - -- 2 -- . -- -- S -- p -- e -- c -- i -- a -- l -- -- s -- e -- a -- s -- o -- n -- a -- l -- -- o -- f -- f -- e -- r -- s -- -- n -- o -- t -- -- i -- n -- c -- l -- u -- d -- e -- d -- -- i -- n -- -- a -- n -- a -- l -- y -- s -- i -- s -- - -- 3 -- . -- -- P -- a -- r -- t -- n -- e -- r -- -- m -- e -- r -- c -- h -- a -- n -- t -- -- s -- p -- e -- c -- i -- f -- i -- c -- -- a -- d -- d -- i -- t -- i -- o -- n -- a -- l -- -- b -- e -- n -- e -- f -- i -- t -- s -- - -- 4 -- . -- -- I -- m -- p -- a -- c -- t -- -- o -- f -- -- G -- S -- T -- -- o -- n -- -- a -- n -- n -- u -- a -- l -- -- f -- e -- e -- s -- - -- 5 -- . -- -- M -- a -- x -- i -- m -- u -- m -- -- r -- e -- w -- a -- r -- d -- -- e -- a -- r -- n -- i -- n -- g -- -- c -- a -- p -- s -- -- i -- f -- -- a -- n -- y diff --git a/cortex_on/agents/research_data/ea15e211-2645-476f-bdff-070ad4c941d7/todo.json b/cortex_on/agents/research_data/ea15e211-2645-476f-bdff-070ad4c941d7/todo.json deleted file mode 100644 index 23d3181..0000000 --- a/cortex_on/agents/research_data/ea15e211-2645-476f-bdff-070ad4c941d7/todo.json +++ /dev/null @@ -1,81 +0,0 @@ -{ - "title": "Analysis of Credit Cards in India for ₹1,00,000 Monthly Income Focusing on Shopping Rewards and Cashback", - "description": "Comprehensive analysis of credit cards available in India for individuals with a ₹1,00,000 monthly income, focusing on cards with shopping rewards and cashback benefits, comparing their benefits, fees, and conducting a cost-benefit analysis for a monthly spending of ₹10,000.", - "todo_items": [ - { - "id": "task1", - "description": "Identify and catalog credit cards offered by major banks in India designed for individuals with ₹1,00,000 monthly income, focusing specifically on shopping rewards and cashback benefits.", - "completed": true, - "dependencies": [], - "priority": 1, - "findings_path": "/app/agents/research_data/ea15e211-2645-476f-bdff-070ad4c941d7/findings_task1_20250423_105913.md", - "completion_time": "2025-04-23T10:59:13.073788", - "knowledge_gaps": [ - "1. Actual reward point conversion rates for some cards\n2. Current welcome bonus offers which may vary\n3. Detailed terms and conditions for reward categories\n4. Processing time for card approval\n5. Specific merchant tie-ups and ongoing offers" - ] - }, - { - "id": "task2", - "description": "Conduct a detailed cost-benefit analysis of the identified credit cards based on a monthly spending of ₹10,000, evaluating fees, reward programs, and overall value.", - "completed": true, - "dependencies": [ - "task1" - ], - "priority": 2, - "findings_path": "/app/agents/research_data/ea15e211-2645-476f-bdff-070ad4c941d7/findings_task2_20250423_105933.md", - "completion_time": "2025-04-23T10:59:33.615436", - "knowledge_gaps": "1. Actual reward point redemption values may vary\n2. Special seasonal offers not included in analysis\n3. Partner merchant specific additional benefits\n4. Impact of GST on annual fees\n5. Maximum reward earning caps if any" - } - ], - "current_item_id": null, - "completed_items": [ - "task1", - "task2" - ], - "last_completed_item_id": "task2", - "knowledge_gaps": [ - "1", - ".", - " ", - "A", - "c", - "t", - "u", - "a", - "l", - "r", - "e", - "w", - "d", - "p", - "o", - "i", - "n", - "v", - "s", - "f", - "m", - "\n", - "2", - "C", - "b", - "h", - "y", - "3", - "D", - "g", - "4", - "P", - "5", - "S", - "-", - "I", - "G", - "T", - "M", - "x" - ], - "report_sections": { - "task1": "Credit Card Options Analysis for ₹1,00,000 Monthly Income\n\nFor individuals with a monthly income of ₹1,00,000, several attractive credit card options are available from major Indian banks, focusing on shopping rewards and cashback benefits. The research has identified five primary options that offer excellent value for a monthly spending of ₹10,000.\n\nThese cards have been selected based on:\n- Eligibility criteria matching the income requirement\n- Strong focus on shopping rewards and cashback\n- Reasonable annual fees with waiver options\n- Reputation of the issuing banks\n- Overall reward earning potential\n\nEach card offers unique benefits catering to different spending patterns, from online shopping to everyday purchases, with annual fees ranging from lifetime free to ₹1,000, making them suitable for various user preferences." - } -} \ No newline at end of file diff --git a/cortex_on/agents/research_data/ea15e211-2645-476f-bdff-070ad4c941d7/todo.md b/cortex_on/agents/research_data/ea15e211-2645-476f-bdff-070ad4c941d7/todo.md deleted file mode 100644 index 40c94d3..0000000 --- a/cortex_on/agents/research_data/ea15e211-2645-476f-bdff-070ad4c941d7/todo.md +++ /dev/null @@ -1,55 +0,0 @@ -# Research Plan: Analysis of Credit Cards in India for ₹1,00,000 Monthly Income Focusing on Shopping Rewards and Cashback - -## Description -Comprehensive analysis of credit cards available in India for individuals with a ₹1,00,000 monthly income, focusing on cards with shopping rewards and cashback benefits, comparing their benefits, fees, and conducting a cost-benefit analysis for a monthly spending of ₹10,000. - -## Progress: 2/2 tasks completed - -## Todo Items - -- [x] **Task task1** (Priority: 1): Identify and catalog credit cards offered by major banks in India designed for individuals with ₹1,00,000 monthly income, focusing specifically on shopping rewards and cashback benefits. - Completed: 2025-04-23 10:59 -- [x] **Task task2** (Priority: 2) (Depends on: task1): Conduct a detailed cost-benefit analysis of the identified credit cards based on a monthly spending of ₹10,000, evaluating fees, reward programs, and overall value. - Completed: 2025-04-23 10:59 - -## Knowledge Gaps Identified - -- 1 -- . -- -- A -- c -- t -- u -- a -- l -- r -- e -- w -- d -- p -- o -- i -- n -- v -- s -- f -- m -- - -- 2 -- C -- b -- h -- y -- 3 -- D -- g -- 4 -- P -- 5 -- S -- - -- I -- G -- T -- M -- x diff --git a/cortex_on/instructor.py b/cortex_on/instructor.py index f455265..cfb8dba 100644 --- a/cortex_on/instructor.py +++ b/cortex_on/instructor.py @@ -6,6 +6,7 @@ from dataclasses import asdict from datetime import datetime from typing import Any, Dict, List, Optional, Tuple, Union +import uuid # Third-party imports from dotenv import load_dotenv @@ -98,20 +99,31 @@ async def run(self, task: str, websocket: WebSocket) -> List[Dict[str, Any]]: stream_output.steps.append("Agents initialized successfully") await self._safe_websocket_send(stream_output) - async with orchestrator_agent.run_mcp_servers(): - orchestrator_response = await orchestrator_agent.run( - user_prompt=task, - deps=deps_for_orchestrator - ) - stream_output.output = orchestrator_response.output - stream_output.status_code = 200 - logfire.debug(f"Orchestrator response: {orchestrator_response.output}") - await self._safe_websocket_send(stream_output) + # Generate a unique request ID + request_id = str(uuid.uuid4()) + + # Store the dependencies in the MCP server's request context + from agents.mcp_server import request_contexts + request_contexts[request_id] = deps_for_orchestrator + + try: + async with orchestrator_agent.run_mcp_servers(): + orchestrator_response = await orchestrator_agent.run( + user_prompt=task, + deps=deps_for_orchestrator + ) + stream_output.output = orchestrator_response.output + stream_output.status_code = 200 + logfire.debug(f"Orchestrator response: {orchestrator_response.output}") + await self._safe_websocket_send(stream_output) + finally: + # Clean up the request context + if request_id in request_contexts: + del request_contexts[request_id] logfire.info("Task completed successfully") return [json.loads(json.dumps(asdict(i), cls=DateTimeEncoder)) for i in self.orchestrator_response] - except Exception as e: error_msg = f"Critical orchestration error: {str(e)}\n{traceback.format_exc()}" logfire.error(error_msg) @@ -133,6 +145,7 @@ async def run(self, task: str, websocket: WebSocket) -> List[Dict[str, Any]]: finally: logfire.info("Orchestration process complete") # Clear any sensitive data + async def shutdown(self): """Clean shutdown of orchestrator""" try: From 5f78eea8a6cd7109462ccf4421f021c8503b1f79 Mon Sep 17 00:00:00 2001 From: Sakalya100 Date: Wed, 23 Apr 2025 21:03:16 +0530 Subject: [PATCH 12/35] Revert previous commit --- cortex_on/agents/mcp_server.py | 211 ++++++++++++--------------------- cortex_on/instructor.py | 32 ++--- 2 files changed, 89 insertions(+), 154 deletions(-) diff --git a/cortex_on/agents/mcp_server.py b/cortex_on/agents/mcp_server.py index 891282f..a3925da 100644 --- a/cortex_on/agents/mcp_server.py +++ b/cortex_on/agents/mcp_server.py @@ -1,68 +1,26 @@ -from mcp.server.stdio import StdioServer -from pydantic_ai import Agent, RunContext +from mcp.server.fastmcp import FastMCP +from pydantic_ai import Agent from pydantic_ai.models.anthropic import AnthropicModel -from fastapi import WebSocket -from dataclasses import asdict +import os from typing import List, Optional, Dict, Any, Union, Tuple import json -import os +from dataclasses import asdict from utils.ant_client import get_client from utils.stream_response_format import StreamResponse from agents.planner_agent import planner_agent +from agents.code_agent import coder_agent from agents.code_agent import coder_agent, CoderAgentDeps from agents.orchestrator_agent import orchestrator_deps from agents.web_surfer import WebSurfer import logfire -from pydantic import BaseModel, Field -from typing import Dict, Optional - -# Initialize the MCP server -server = StdioServer("CortexON MCP Server") - -class PlanTaskInput(BaseModel): - task: str - request_id: Optional[str] = None - websocket_id: Optional[str] = None - stream_output_id: Optional[str] = None -class CodeTaskInput(BaseModel): - task: str - request_id: Optional[str] = None - websocket_id: Optional[str] = None - stream_output_id: Optional[str] = None +# Initialize the single MCP server +server = FastMCP("CortexON MCP Server", host="0.0.0.0", port=3001) -class WebSurfTaskInput(BaseModel): - task: str - request_id: Optional[str] = None - websocket_id: Optional[str] = None - stream_output_id: Optional[str] = None -class AskHumanInput(BaseModel): - question: str - request_id: Optional[str] = None - websocket_id: Optional[str] = None - stream_output_id: Optional[str] = None - -class PlannerAgentUpdateInput(BaseModel): - completed_task: str - request_id: Optional[str] = None - websocket_id: Optional[str] = None - stream_output_id: Optional[str] = None - -# Store request context -request_contexts: Dict[str, orchestrator_deps] = {} - -def get_request_context(request_id: str) -> Optional[orchestrator_deps]: - """Get the request context for a given request ID""" - return request_contexts.get(request_id) - -@server.tool(input_model=PlanTaskInput) -async def plan_task(task: str, request_id: Optional[str] = None, websocket_id: Optional[str] = None, stream_output_id: Optional[str] = None) -> str: +@server.tool() +async def plan_task(task: str) -> str: """Plans the task and assigns it to the appropriate agents""" - deps = get_request_context(request_id) if request_id else None - if not deps: - raise ValueError("Request context not found") - try: logfire.info(f"Planning task: {task}") @@ -76,14 +34,14 @@ async def plan_task(task: str, request_id: Optional[str] = None, websocket_id: O ) # Add to orchestrator's response collection if available - if deps.agent_responses is not None: - deps.agent_responses.append(planner_stream_output) + if orchestrator_deps.agent_responses is not None: + orchestrator_deps.agent_responses.append(planner_stream_output) - await _safe_websocket_send(deps.websocket, planner_stream_output) + await _safe_websocket_send(orchestrator_deps.websocket, planner_stream_output) # Update planner stream planner_stream_output.steps.append("Planning task...") - await _safe_websocket_send(deps.websocket, planner_stream_output) + await _safe_websocket_send(orchestrator_deps.websocket, planner_stream_output) # Run planner agent planner_response = await planner_agent.run(user_prompt=task) @@ -93,12 +51,11 @@ async def plan_task(task: str, request_id: Optional[str] = None, websocket_id: O planner_stream_output.steps.append("Task planned successfully") planner_stream_output.output = plan_text planner_stream_output.status_code = 200 - await _safe_websocket_send(deps.websocket, planner_stream_output) + await _safe_websocket_send(orchestrator_deps.websocket, planner_stream_output) # Also update orchestrator stream - if deps.stream_output: - deps.stream_output.steps.append("Task planned successfully") - await _safe_websocket_send(deps.websocket, deps.stream_output) + orchestrator_deps.stream_output.steps.append("Task planned successfully") + await _safe_websocket_send(orchestrator_deps.websocket, orchestrator_deps.stream_output) return f"Task planned successfully\nTask: {plan_text}" except Exception as e: @@ -109,22 +66,19 @@ async def plan_task(task: str, request_id: Optional[str] = None, websocket_id: O if planner_stream_output: planner_stream_output.steps.append(f"Planning failed: {str(e)}") planner_stream_output.status_code = 500 - await _safe_websocket_send(deps.websocket, planner_stream_output) + await _safe_websocket_send(orchestrator_deps.websocket, planner_stream_output) # Also update orchestrator stream - if deps.stream_output: - deps.stream_output.steps.append(f"Planning failed: {str(e)}") - await _safe_websocket_send(deps.websocket, deps.stream_output) + if orchestrator_deps.stream_output: + orchestrator_deps.stream_output.steps.append(f"Planning failed: {str(e)}") + await _safe_websocket_send(orchestrator_deps.websocket, orchestrator_deps.stream_output) return f"Failed to plan task: {error_msg}" -@server.tool(input_model=CodeTaskInput) -async def code_task(task: str, request_id: Optional[str] = None, websocket_id: Optional[str] = None, stream_output_id: Optional[str] = None) -> str: - """Assigns coding tasks to the coder agent""" - deps = get_request_context(request_id) if request_id else None - if not deps: - raise ValueError("Request context not found") +@server.tool() +async def code_task(task: str) -> str: + """Assigns coding tasks to the coder agent""" try: logfire.info(f"Assigning coding task: {task}") @@ -138,15 +92,15 @@ async def code_task(task: str, request_id: Optional[str] = None, websocket_id: O ) # Add to orchestrator's response collection if available - if deps.agent_responses is not None: - deps.agent_responses.append(coder_stream_output) + if orchestrator_deps.agent_responses is not None: + orchestrator_deps.agent_responses.append(coder_stream_output) # Send initial update for Coder Agent - await _safe_websocket_send(deps.websocket, coder_stream_output) + await _safe_websocket_send(orchestrator_deps.websocket, coder_stream_output) # Create deps with the new stream_output deps_for_coder_agent = CoderAgentDeps( - websocket=deps.websocket, + websocket=orchestrator_deps.websocket, stream_output=coder_stream_output ) @@ -163,7 +117,7 @@ async def code_task(task: str, request_id: Optional[str] = None, websocket_id: O coder_stream_output.output = response_data coder_stream_output.status_code = 200 coder_stream_output.steps.append("Coding task completed successfully") - await _safe_websocket_send(deps.websocket, coder_stream_output) + await _safe_websocket_send(orchestrator_deps.websocket, coder_stream_output) # Add a reminder in the result message to update the plan using planner_agent_update response_with_reminder = f"{response_data}\n\nReminder: You must now call planner_agent_update with the completed task description: \"{task} (coder_agent)\"" @@ -176,17 +130,14 @@ async def code_task(task: str, request_id: Optional[str] = None, websocket_id: O # Update coder_stream_output with error coder_stream_output.steps.append(f"Coding task failed: {str(e)}") coder_stream_output.status_code = 500 - await _safe_websocket_send(deps.websocket, coder_stream_output) + await _safe_websocket_send(orchestrator_deps.websocket, coder_stream_output) return f"Failed to assign coding task: {error_msg}" -@server.tool(input_model=WebSurfTaskInput) -async def web_surf_task(task: str, request_id: Optional[str] = None, websocket_id: Optional[str] = None, stream_output_id: Optional[str] = None) -> str: - """Assigns web surfing tasks to the web surfer agent""" - deps = get_request_context(request_id) if request_id else None - if not deps: - raise ValueError("Request context not found") +@server.tool() +async def web_surf_task(task: str) -> str: + """Assigns web surfing tasks to the web surfer agent""" try: logfire.info(f"Assigning web surfing task: {task}") @@ -201,10 +152,10 @@ async def web_surf_task(task: str, request_id: Optional[str] = None, websocket_i ) # Add to orchestrator's response collection if available - if deps.agent_responses is not None: - deps.agent_responses.append(web_surfer_stream_output) + if orchestrator_deps.agent_responses is not None: + orchestrator_deps.agent_responses.append(web_surfer_stream_output) - await _safe_websocket_send(deps.websocket, web_surfer_stream_output) + await _safe_websocket_send(orchestrator_deps.websocket, web_surfer_stream_output) # Initialize WebSurfer agent web_surfer_agent = WebSurfer(api_url="http://localhost:8000/api/v1/web/stream") @@ -212,7 +163,7 @@ async def web_surf_task(task: str, request_id: Optional[str] = None, websocket_i # Run WebSurfer with its own stream_output success, message, messages = await web_surfer_agent.generate_reply( instruction=task, - websocket=deps.websocket, + websocket=orchestrator_deps.websocket, stream_output=web_surfer_stream_output ) @@ -229,10 +180,10 @@ async def web_surf_task(task: str, request_id: Optional[str] = None, websocket_i web_surfer_stream_output.status_code = 500 message_with_reminder = message - await _safe_websocket_send(deps.websocket, web_surfer_stream_output) + await _safe_websocket_send(orchestrator_deps.websocket, web_surfer_stream_output) web_surfer_stream_output.steps.append(f"WebSurfer completed: {'Success' if success else 'Failed'}") - await _safe_websocket_send(deps.websocket, web_surfer_stream_output) + await _safe_websocket_send(orchestrator_deps.websocket, web_surfer_stream_output) return message_with_reminder except Exception as e: @@ -242,16 +193,12 @@ async def web_surf_task(task: str, request_id: Optional[str] = None, websocket_i # Update WebSurfer's stream_output with error web_surfer_stream_output.steps.append(f"Web search failed: {str(e)}") web_surfer_stream_output.status_code = 500 - await _safe_websocket_send(deps.websocket, web_surfer_stream_output) + await _safe_websocket_send(orchestrator_deps.websocket, web_surfer_stream_output) return f"Failed to assign web surfing task: {error_msg}" -@server.tool(input_model=AskHumanInput) -async def ask_human(question: str, request_id: Optional[str] = None, websocket_id: Optional[str] = None, stream_output_id: Optional[str] = None) -> str: +@server.tool() +async def ask_human(question: str) -> str: """Sends a question to the frontend and waits for human input""" - deps = get_request_context(request_id) if request_id else None - if not deps: - raise ValueError("Request context not found") - try: logfire.info(f"Asking human: {question}") @@ -265,24 +212,24 @@ async def ask_human(question: str, request_id: Optional[str] = None, websocket_i ) # Add to orchestrator's response collection if available - if deps.agent_responses is not None: - deps.agent_responses.append(human_stream_output) + if orchestrator_deps.agent_responses is not None: + orchestrator_deps.agent_responses.append(human_stream_output) # Send the question to frontend - await _safe_websocket_send(deps.websocket, human_stream_output) + await _safe_websocket_send(orchestrator_deps.websocket, human_stream_output) # Update stream with waiting message human_stream_output.steps.append("Waiting for human input...") - await _safe_websocket_send(deps.websocket, human_stream_output) + await _safe_websocket_send(orchestrator_deps.websocket, human_stream_output) # Wait for response from frontend - response = await deps.websocket.receive_text() + response = await orchestrator_deps.websocket.receive_text() # Update stream with response human_stream_output.steps.append("Received human input") human_stream_output.output = response human_stream_output.status_code = 200 - await _safe_websocket_send(deps.websocket, human_stream_output) + await _safe_websocket_send(orchestrator_deps.websocket, human_stream_output) return response except Exception as e: @@ -292,12 +239,12 @@ async def ask_human(question: str, request_id: Optional[str] = None, websocket_i # Update stream with error human_stream_output.steps.append(f"Failed to get human input: {str(e)}") human_stream_output.status_code = 500 - await _safe_websocket_send(deps.websocket, human_stream_output) + await _safe_websocket_send(orchestrator_deps.websocket, human_stream_output) return f"Failed to get human input: {error_msg}" -@server.tool(input_model=PlannerAgentUpdateInput) -async def planner_agent_update(completed_task: str, request_id: Optional[str] = None, websocket_id: Optional[str] = None, stream_output_id: Optional[str] = None) -> str: +@server.tool() +async def planner_agent_update(completed_task: str) -> str: """ Updates the todo.md file to mark a task as completed and returns the full updated plan. @@ -307,10 +254,6 @@ async def planner_agent_update(completed_task: str, request_id: Optional[str] = Returns: The complete updated todo.md content with tasks marked as completed """ - deps = get_request_context(request_id) if request_id else None - if not deps: - raise ValueError("Request context not found") - try: logfire.info(f"Updating plan with completed task: {completed_task}") @@ -324,7 +267,7 @@ async def planner_agent_update(completed_task: str, request_id: Optional[str] = ) # Send initial update - await _safe_websocket_send(deps.websocket, planner_stream_output) + await _safe_websocket_send(orchestrator_deps.websocket, planner_stream_output) # Directly read and update the todo.md file base_dir = os.path.abspath(os.path.dirname(os.path.dirname(__file__))) @@ -332,7 +275,7 @@ async def planner_agent_update(completed_task: str, request_id: Optional[str] = todo_path = os.path.join(planner_dir, "todo.md") planner_stream_output.steps.append("Reading current todo.md...") - await _safe_websocket_send(deps.websocket, planner_stream_output) + await _safe_websocket_send(orchestrator_deps.websocket, planner_stream_output) # Make sure the directory exists os.makedirs(planner_dir, exist_ok=True) @@ -341,7 +284,7 @@ async def planner_agent_update(completed_task: str, request_id: Optional[str] = # Check if todo.md exists if not os.path.exists(todo_path): planner_stream_output.steps.append("No todo.md file found. Will create new one after task completion.") - await _safe_websocket_send(deps.websocket, planner_stream_output) + await _safe_websocket_send(orchestrator_deps.websocket, planner_stream_output) # We'll directly call planner_agent.run() to create a new plan first plan_prompt = f"Create a simple task plan based on this completed task: {completed_task}" @@ -352,7 +295,7 @@ async def planner_agent_update(completed_task: str, request_id: Optional[str] = with open(todo_path, "r") as file: current_content = file.read() planner_stream_output.steps.append(f"Found existing todo.md ({len(current_content)} bytes)") - await _safe_websocket_send(deps.websocket, planner_stream_output) + await _safe_websocket_send(orchestrator_deps.websocket, planner_stream_output) # Now call planner_agent.run() with specific instructions to update the plan update_prompt = f""" @@ -365,7 +308,7 @@ async def planner_agent_update(completed_task: str, request_id: Optional[str] = """ planner_stream_output.steps.append("Asking planner to update the plan...") - await _safe_websocket_send(deps.websocket, planner_stream_output) + await _safe_websocket_send(orchestrator_deps.websocket, planner_stream_output) updated_plan_response = await planner_agent.run(user_prompt=update_prompt) updated_plan = updated_plan_response.data.plan @@ -377,12 +320,12 @@ async def planner_agent_update(completed_task: str, request_id: Optional[str] = planner_stream_output.steps.append("Plan updated successfully") planner_stream_output.output = updated_plan planner_stream_output.status_code = 200 - await _safe_websocket_send(deps.websocket, planner_stream_output) + await _safe_websocket_send(orchestrator_deps.websocket, planner_stream_output) # Update orchestrator stream - if deps.stream_output: - deps.stream_output.steps.append(f"Plan updated to mark task as completed: {completed_task}") - await _safe_websocket_send(deps.websocket, deps.stream_output) + if orchestrator_deps.stream_output: + orchestrator_deps.stream_output.steps.append(f"Plan updated to mark task as completed: {completed_task}") + await _safe_websocket_send(orchestrator_deps.websocket, orchestrator_deps.stream_output) return updated_plan @@ -392,7 +335,7 @@ async def planner_agent_update(completed_task: str, request_id: Optional[str] = planner_stream_output.steps.append(f"Plan update failed: {str(e)}") planner_stream_output.status_code = 500 - await _safe_websocket_send(deps.websocket, planner_stream_output) + await _safe_websocket_send(orchestrator_deps.websocket, planner_stream_output) return f"Failed to update the plan: {error_msg}" @@ -401,23 +344,27 @@ async def planner_agent_update(completed_task: str, request_id: Optional[str] = logfire.error(error_msg, exc_info=True) # Update stream output with error - if deps.stream_output: - deps.stream_output.steps.append(f"Failed to update plan: {str(e)}") - await _safe_websocket_send(deps.websocket, deps.stream_output) + if orchestrator_deps.stream_output: + orchestrator_deps.stream_output.steps.append(f"Failed to update plan: {str(e)}") + await _safe_websocket_send(orchestrator_deps.websocket, orchestrator_deps.stream_output) return f"Failed to update plan: {error_msg}" -async def _safe_websocket_send(websocket: Optional[WebSocket], message: Any) -> bool: - """Safely send message through websocket with error handling""" - try: - if websocket and websocket.client_state.CONNECTED: - await websocket.send_text(json.dumps(asdict(message))) - logfire.debug("WebSocket message sent (_safe_websocket_send): {message}", message=message) - return True - return False - except Exception as e: - logfire.error(f"WebSocket send failed: {str(e)}") - return False +async def _safe_websocket_send(self, message: Any) -> bool: + """Safely send message through websocket with error handling""" + try: + if self.websocket and self.websocket.client_state.CONNECTED: + await self.websocket.send_text(json.dumps(asdict(message))) + logfire.debug(f"WebSocket message sent: {message}") + return True + return False + except Exception as e: + logfire.error(f"WebSocket send failed: {str(e)}") + return False + +def run_server(): + """Run the MCP server""" + server.run(transport="sse") if __name__ == "__main__": - server.run() \ No newline at end of file + run_server() \ No newline at end of file diff --git a/cortex_on/instructor.py b/cortex_on/instructor.py index cfb8dba..6731efd 100644 --- a/cortex_on/instructor.py +++ b/cortex_on/instructor.py @@ -98,28 +98,16 @@ async def run(self, task: str, websocket: WebSocket) -> List[Dict[str, Any]]: await self._safe_websocket_send(stream_output) stream_output.steps.append("Agents initialized successfully") await self._safe_websocket_send(stream_output) - - # Generate a unique request ID - request_id = str(uuid.uuid4()) - - # Store the dependencies in the MCP server's request context - from agents.mcp_server import request_contexts - request_contexts[request_id] = deps_for_orchestrator - - try: - async with orchestrator_agent.run_mcp_servers(): - orchestrator_response = await orchestrator_agent.run( - user_prompt=task, - deps=deps_for_orchestrator - ) - stream_output.output = orchestrator_response.output - stream_output.status_code = 200 - logfire.debug(f"Orchestrator response: {orchestrator_response.output}") - await self._safe_websocket_send(stream_output) - finally: - # Clean up the request context - if request_id in request_contexts: - del request_contexts[request_id] + + async with orchestrator_agent.run_mcp_servers(): + orchestrator_response = await orchestrator_agent.run( + user_prompt=task, + deps=deps_for_orchestrator + ) + stream_output.output = orchestrator_response.output + stream_output.status_code = 200 + logfire.debug(f"Orchestrator response: {orchestrator_response.output}") + await self._safe_websocket_send(stream_output) logfire.info("Task completed successfully") return [json.loads(json.dumps(asdict(i), cls=DateTimeEncoder)) for i in self.orchestrator_response] From 7cb628e3eb01edb30cbdad1e37220188866b0f4c Mon Sep 17 00:00:00 2001 From: Sakalya100 Date: Thu, 24 Apr 2025 12:26:47 +0530 Subject: [PATCH 13/35] Added context to mcp server tools --- cortex_on/agents/mcp_server.py | 132 +++++++++++++++++---------------- 1 file changed, 67 insertions(+), 65 deletions(-) diff --git a/cortex_on/agents/mcp_server.py b/cortex_on/agents/mcp_server.py index a3925da..c871aa3 100644 --- a/cortex_on/agents/mcp_server.py +++ b/cortex_on/agents/mcp_server.py @@ -1,6 +1,8 @@ from mcp.server.fastmcp import FastMCP from pydantic_ai import Agent from pydantic_ai.models.anthropic import AnthropicModel +from pydantic_ai.mcp import RunContext +from fastapi import WebSocket import os from typing import List, Optional, Dict, Any, Union, Tuple import json @@ -19,11 +21,11 @@ @server.tool() -async def plan_task(task: str) -> str: +async def plan_task(task: str, ctx: RunContext[orchestrator_deps]) -> str: """Plans the task and assigns it to the appropriate agents""" try: - logfire.info(f"Planning task: {task}") - + logfire.info(f"Planning task: {task} and context: {ctx}") + print(f"Planning task: {task} and context: {ctx}") # Create a new StreamResponse for Planner Agent planner_stream_output = StreamResponse( agent_name="Planner Agent", @@ -34,14 +36,14 @@ async def plan_task(task: str) -> str: ) # Add to orchestrator's response collection if available - if orchestrator_deps.agent_responses is not None: - orchestrator_deps.agent_responses.append(planner_stream_output) + if ctx.deps.agent_responses is not None: + ctx.deps.agent_responses.append(planner_stream_output) - await _safe_websocket_send(orchestrator_deps.websocket, planner_stream_output) + await _safe_websocket_send(ctx.deps.websocket, planner_stream_output) # Update planner stream planner_stream_output.steps.append("Planning task...") - await _safe_websocket_send(orchestrator_deps.websocket, planner_stream_output) + await _safe_websocket_send(ctx.deps.websocket, planner_stream_output) # Run planner agent planner_response = await planner_agent.run(user_prompt=task) @@ -51,11 +53,11 @@ async def plan_task(task: str) -> str: planner_stream_output.steps.append("Task planned successfully") planner_stream_output.output = plan_text planner_stream_output.status_code = 200 - await _safe_websocket_send(orchestrator_deps.websocket, planner_stream_output) + await _safe_websocket_send(ctx.deps.websocket, planner_stream_output) # Also update orchestrator stream - orchestrator_deps.stream_output.steps.append("Task planned successfully") - await _safe_websocket_send(orchestrator_deps.websocket, orchestrator_deps.stream_output) + ctx.deps.stream_output.steps.append("Task planned successfully") + await _safe_websocket_send(ctx.deps.websocket, ctx.deps.stream_output) return f"Task planned successfully\nTask: {plan_text}" except Exception as e: @@ -66,22 +68,22 @@ async def plan_task(task: str) -> str: if planner_stream_output: planner_stream_output.steps.append(f"Planning failed: {str(e)}") planner_stream_output.status_code = 500 - await _safe_websocket_send(orchestrator_deps.websocket, planner_stream_output) + await _safe_websocket_send(ctx.deps.websocket, planner_stream_output) # Also update orchestrator stream - if orchestrator_deps.stream_output: - orchestrator_deps.stream_output.steps.append(f"Planning failed: {str(e)}") - await _safe_websocket_send(orchestrator_deps.websocket, orchestrator_deps.stream_output) + if ctx.deps.stream_output: + ctx.deps.stream_output.steps.append(f"Planning failed: {str(e)}") + await _safe_websocket_send(ctx.deps.websocket, ctx.deps.stream_output) return f"Failed to plan task: {error_msg}" @server.tool() -async def code_task(task: str) -> str: +async def code_task(task: str, ctx: RunContext[orchestrator_deps]) -> str: """Assigns coding tasks to the coder agent""" try: logfire.info(f"Assigning coding task: {task}") - + print(f"Assigning coding task: {task} and context: {ctx}") # Create a new StreamResponse for Coder Agent coder_stream_output = StreamResponse( agent_name="Coder Agent", @@ -92,15 +94,15 @@ async def code_task(task: str) -> str: ) # Add to orchestrator's response collection if available - if orchestrator_deps.agent_responses is not None: - orchestrator_deps.agent_responses.append(coder_stream_output) + if ctx.deps.agent_responses is not None: + ctx.deps.agent_responses.append(coder_stream_output) # Send initial update for Coder Agent - await _safe_websocket_send(orchestrator_deps.websocket, coder_stream_output) + await _safe_websocket_send(ctx.deps.websocket, coder_stream_output) # Create deps with the new stream_output deps_for_coder_agent = CoderAgentDeps( - websocket=orchestrator_deps.websocket, + websocket=ctx.deps.websocket, stream_output=coder_stream_output ) @@ -117,7 +119,7 @@ async def code_task(task: str) -> str: coder_stream_output.output = response_data coder_stream_output.status_code = 200 coder_stream_output.steps.append("Coding task completed successfully") - await _safe_websocket_send(orchestrator_deps.websocket, coder_stream_output) + await _safe_websocket_send(ctx.deps.websocket, coder_stream_output) # Add a reminder in the result message to update the plan using planner_agent_update response_with_reminder = f"{response_data}\n\nReminder: You must now call planner_agent_update with the completed task description: \"{task} (coder_agent)\"" @@ -130,13 +132,13 @@ async def code_task(task: str) -> str: # Update coder_stream_output with error coder_stream_output.steps.append(f"Coding task failed: {str(e)}") coder_stream_output.status_code = 500 - await _safe_websocket_send(orchestrator_deps.websocket, coder_stream_output) + await _safe_websocket_send(ctx.deps.websocket, coder_stream_output) return f"Failed to assign coding task: {error_msg}" @server.tool() -async def web_surf_task(task: str) -> str: +async def web_surf_task(task: str,ctx: RunContext[orchestrator_deps]) -> str: """Assigns web surfing tasks to the web surfer agent""" try: logfire.info(f"Assigning web surfing task: {task}") @@ -152,10 +154,10 @@ async def web_surf_task(task: str) -> str: ) # Add to orchestrator's response collection if available - if orchestrator_deps.agent_responses is not None: - orchestrator_deps.agent_responses.append(web_surfer_stream_output) + if ctx.deps.agent_responses is not None: + ctx.deps.agent_responses.append(web_surfer_stream_output) - await _safe_websocket_send(orchestrator_deps.websocket, web_surfer_stream_output) + await _safe_websocket_send(ctx.deps.websocket, web_surfer_stream_output) # Initialize WebSurfer agent web_surfer_agent = WebSurfer(api_url="http://localhost:8000/api/v1/web/stream") @@ -163,7 +165,7 @@ async def web_surf_task(task: str) -> str: # Run WebSurfer with its own stream_output success, message, messages = await web_surfer_agent.generate_reply( instruction=task, - websocket=orchestrator_deps.websocket, + websocket=ctx.deps.websocket, stream_output=web_surfer_stream_output ) @@ -180,10 +182,10 @@ async def web_surf_task(task: str) -> str: web_surfer_stream_output.status_code = 500 message_with_reminder = message - await _safe_websocket_send(orchestrator_deps.websocket, web_surfer_stream_output) + await _safe_websocket_send(ctx.deps.websocket, web_surfer_stream_output) web_surfer_stream_output.steps.append(f"WebSurfer completed: {'Success' if success else 'Failed'}") - await _safe_websocket_send(orchestrator_deps.websocket, web_surfer_stream_output) + await _safe_websocket_send(ctx.deps.websocket, web_surfer_stream_output) return message_with_reminder except Exception as e: @@ -193,15 +195,15 @@ async def web_surf_task(task: str) -> str: # Update WebSurfer's stream_output with error web_surfer_stream_output.steps.append(f"Web search failed: {str(e)}") web_surfer_stream_output.status_code = 500 - await _safe_websocket_send(orchestrator_deps.websocket, web_surfer_stream_output) + await _safe_websocket_send(ctx.deps.websocket, web_surfer_stream_output) return f"Failed to assign web surfing task: {error_msg}" @server.tool() -async def ask_human(question: str) -> str: +async def ask_human(question: str, ctx: RunContext[orchestrator_deps]) -> str: """Sends a question to the frontend and waits for human input""" try: logfire.info(f"Asking human: {question}") - + print(f"Asking human: {question} and context: {ctx}") # Create a new StreamResponse for Human Input human_stream_output = StreamResponse( agent_name="Human Input", @@ -212,24 +214,24 @@ async def ask_human(question: str) -> str: ) # Add to orchestrator's response collection if available - if orchestrator_deps.agent_responses is not None: - orchestrator_deps.agent_responses.append(human_stream_output) + if ctx.deps.agent_responses is not None: + ctx.deps.agent_responses.append(human_stream_output) # Send the question to frontend - await _safe_websocket_send(orchestrator_deps.websocket, human_stream_output) + await _safe_websocket_send(ctx.deps.websocket, human_stream_output) # Update stream with waiting message human_stream_output.steps.append("Waiting for human input...") - await _safe_websocket_send(orchestrator_deps.websocket, human_stream_output) + await _safe_websocket_send(ctx.deps.websocket, human_stream_output) # Wait for response from frontend - response = await orchestrator_deps.websocket.receive_text() + response = await ctx.deps.websocket.receive_text() # Update stream with response human_stream_output.steps.append("Received human input") human_stream_output.output = response human_stream_output.status_code = 200 - await _safe_websocket_send(orchestrator_deps.websocket, human_stream_output) + await _safe_websocket_send(ctx.deps.websocket, human_stream_output) return response except Exception as e: @@ -239,12 +241,12 @@ async def ask_human(question: str) -> str: # Update stream with error human_stream_output.steps.append(f"Failed to get human input: {str(e)}") human_stream_output.status_code = 500 - await _safe_websocket_send(orchestrator_deps.websocket, human_stream_output) + await _safe_websocket_send(ctx.deps.websocket, human_stream_output) return f"Failed to get human input: {error_msg}" @server.tool() -async def planner_agent_update(completed_task: str) -> str: +async def planner_agent_update(completed_task: str,ctx: RunContext[orchestrator_deps]) -> str: """ Updates the todo.md file to mark a task as completed and returns the full updated plan. @@ -256,7 +258,7 @@ async def planner_agent_update(completed_task: str) -> str: """ try: logfire.info(f"Updating plan with completed task: {completed_task}") - + print(f"Updating plan with completed task: {completed_task} and context: {ctx}") # Create a new StreamResponse for Planner Agent update planner_stream_output = StreamResponse( agent_name="Planner Agent", @@ -267,7 +269,7 @@ async def planner_agent_update(completed_task: str) -> str: ) # Send initial update - await _safe_websocket_send(orchestrator_deps.websocket, planner_stream_output) + await _safe_websocket_send(ctx.deps.websocket, planner_stream_output) # Directly read and update the todo.md file base_dir = os.path.abspath(os.path.dirname(os.path.dirname(__file__))) @@ -275,7 +277,7 @@ async def planner_agent_update(completed_task: str) -> str: todo_path = os.path.join(planner_dir, "todo.md") planner_stream_output.steps.append("Reading current todo.md...") - await _safe_websocket_send(orchestrator_deps.websocket, planner_stream_output) + await _safe_websocket_send(ctx.deps.websocket, planner_stream_output) # Make sure the directory exists os.makedirs(planner_dir, exist_ok=True) @@ -284,7 +286,7 @@ async def planner_agent_update(completed_task: str) -> str: # Check if todo.md exists if not os.path.exists(todo_path): planner_stream_output.steps.append("No todo.md file found. Will create new one after task completion.") - await _safe_websocket_send(orchestrator_deps.websocket, planner_stream_output) + await _safe_websocket_send(ctx.deps.websocket, planner_stream_output) # We'll directly call planner_agent.run() to create a new plan first plan_prompt = f"Create a simple task plan based on this completed task: {completed_task}" @@ -295,7 +297,7 @@ async def planner_agent_update(completed_task: str) -> str: with open(todo_path, "r") as file: current_content = file.read() planner_stream_output.steps.append(f"Found existing todo.md ({len(current_content)} bytes)") - await _safe_websocket_send(orchestrator_deps.websocket, planner_stream_output) + await _safe_websocket_send(ctx.deps.websocket, planner_stream_output) # Now call planner_agent.run() with specific instructions to update the plan update_prompt = f""" @@ -308,7 +310,7 @@ async def planner_agent_update(completed_task: str) -> str: """ planner_stream_output.steps.append("Asking planner to update the plan...") - await _safe_websocket_send(orchestrator_deps.websocket, planner_stream_output) + await _safe_websocket_send(ctx.deps.websocket, planner_stream_output) updated_plan_response = await planner_agent.run(user_prompt=update_prompt) updated_plan = updated_plan_response.data.plan @@ -320,12 +322,12 @@ async def planner_agent_update(completed_task: str) -> str: planner_stream_output.steps.append("Plan updated successfully") planner_stream_output.output = updated_plan planner_stream_output.status_code = 200 - await _safe_websocket_send(orchestrator_deps.websocket, planner_stream_output) + await _safe_websocket_send(ctx.deps.websocket, planner_stream_output) # Update orchestrator stream - if orchestrator_deps.stream_output: - orchestrator_deps.stream_output.steps.append(f"Plan updated to mark task as completed: {completed_task}") - await _safe_websocket_send(orchestrator_deps.websocket, orchestrator_deps.stream_output) + if ctx.deps.stream_output: + ctx.deps.stream_output.steps.append(f"Plan updated to mark task as completed: {completed_task}") + await _safe_websocket_send(ctx.deps.websocket, ctx.deps.stream_output) return updated_plan @@ -335,7 +337,7 @@ async def planner_agent_update(completed_task: str) -> str: planner_stream_output.steps.append(f"Plan update failed: {str(e)}") planner_stream_output.status_code = 500 - await _safe_websocket_send(orchestrator_deps.websocket, planner_stream_output) + await _safe_websocket_send(ctx.deps.websocket, planner_stream_output) return f"Failed to update the plan: {error_msg}" @@ -344,23 +346,23 @@ async def planner_agent_update(completed_task: str) -> str: logfire.error(error_msg, exc_info=True) # Update stream output with error - if orchestrator_deps.stream_output: - orchestrator_deps.stream_output.steps.append(f"Failed to update plan: {str(e)}") - await _safe_websocket_send(orchestrator_deps.websocket, orchestrator_deps.stream_output) + if ctx.deps.stream_output: + ctx.deps.stream_output.steps.append(f"Failed to update plan: {str(e)}") + await _safe_websocket_send(ctx.deps.websocket, ctx.deps.stream_output) return f"Failed to update plan: {error_msg}" -async def _safe_websocket_send(self, message: Any) -> bool: - """Safely send message through websocket with error handling""" - try: - if self.websocket and self.websocket.client_state.CONNECTED: - await self.websocket.send_text(json.dumps(asdict(message))) - logfire.debug(f"WebSocket message sent: {message}") - return True - return False - except Exception as e: - logfire.error(f"WebSocket send failed: {str(e)}") - return False +async def _safe_websocket_send(websocket: Optional[WebSocket], message: Any) -> bool: + """Safely send message through websocket with error handling""" + try: + if websocket and websocket.client_state.CONNECTED: + await websocket.send_text(json.dumps(asdict(message))) + logfire.debug("WebSocket message sent (_safe_websocket_send): {message}", message=message) + return True + return False + except Exception as e: + logfire.error(f"WebSocket send failed: {str(e)}") + return False def run_server(): """Run the MCP server""" From db31548a2333c730109053ddaac8e63a41d7302c Mon Sep 17 00:00:00 2001 From: aryan Date: Thu, 24 Apr 2025 17:34:49 +0530 Subject: [PATCH 14/35] refactor(MCP server integration and update Dockerfile): - Updated Dockerfile to run only the main API, with the MCP server started programmatically in a separate thread. - Refactored instructor.py to dynamically register MCP server tools and manage their execution, improving modularity and reducing direct dependencies in the MCP server. - Added threading support for MCP server initialization to enhance responsiveness. --- cortex_on/Dockerfile | 4 +- cortex_on/agents/mcp_server.py | 348 +------------------------------ cortex_on/instructor.py | 361 ++++++++++++++++++++++++++++++++- 3 files changed, 364 insertions(+), 349 deletions(-) diff --git a/cortex_on/Dockerfile b/cortex_on/Dockerfile index 5465d68..955808b 100644 --- a/cortex_on/Dockerfile +++ b/cortex_on/Dockerfile @@ -26,5 +26,5 @@ ENV ANTHROPIC_MODEL_NAME=${ANTHROPIC_MODEL_NAME:-claude-3-sonnet-20240229} EXPOSE 8081 EXPOSE 3001 -# Run both the MCP server and the main API -CMD ["sh", "-c", "python -m agents.mcp_server & uvicorn main:app --host 0.0.0.0 --port 8081"] \ No newline at end of file +# Run only the main API - MCP server will be started programmatically +CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8081"] \ No newline at end of file diff --git a/cortex_on/agents/mcp_server.py b/cortex_on/agents/mcp_server.py index c871aa3..2469b7a 100644 --- a/cortex_on/agents/mcp_server.py +++ b/cortex_on/agents/mcp_server.py @@ -1,7 +1,7 @@ from mcp.server.fastmcp import FastMCP from pydantic_ai import Agent from pydantic_ai.models.anthropic import AnthropicModel -from pydantic_ai.mcp import RunContext +from pydantic_ai import RunContext from fastapi import WebSocket import os from typing import List, Optional, Dict, Any, Union, Tuple @@ -19,351 +19,9 @@ # Initialize the single MCP server server = FastMCP("CortexON MCP Server", host="0.0.0.0", port=3001) +# Note: All tools are now dynamically registered in instructor.py +# This avoids the problem of websocket not being available when tools are defined -@server.tool() -async def plan_task(task: str, ctx: RunContext[orchestrator_deps]) -> str: - """Plans the task and assigns it to the appropriate agents""" - try: - logfire.info(f"Planning task: {task} and context: {ctx}") - print(f"Planning task: {task} and context: {ctx}") - # Create a new StreamResponse for Planner Agent - planner_stream_output = StreamResponse( - agent_name="Planner Agent", - instructions=task, - steps=[], - output="", - status_code=0 - ) - - # Add to orchestrator's response collection if available - if ctx.deps.agent_responses is not None: - ctx.deps.agent_responses.append(planner_stream_output) - - await _safe_websocket_send(ctx.deps.websocket, planner_stream_output) - - # Update planner stream - planner_stream_output.steps.append("Planning task...") - await _safe_websocket_send(ctx.deps.websocket, planner_stream_output) - - # Run planner agent - planner_response = await planner_agent.run(user_prompt=task) - - # Update planner stream with results - plan_text = planner_response.data.plan - planner_stream_output.steps.append("Task planned successfully") - planner_stream_output.output = plan_text - planner_stream_output.status_code = 200 - await _safe_websocket_send(ctx.deps.websocket, planner_stream_output) - - # Also update orchestrator stream - ctx.deps.stream_output.steps.append("Task planned successfully") - await _safe_websocket_send(ctx.deps.websocket, ctx.deps.stream_output) - - return f"Task planned successfully\nTask: {plan_text}" - except Exception as e: - error_msg = f"Error planning task: {str(e)}" - logfire.error(error_msg, exc_info=True) - - # Update planner stream with error - if planner_stream_output: - planner_stream_output.steps.append(f"Planning failed: {str(e)}") - planner_stream_output.status_code = 500 - await _safe_websocket_send(ctx.deps.websocket, planner_stream_output) - - # Also update orchestrator stream - if ctx.deps.stream_output: - ctx.deps.stream_output.steps.append(f"Planning failed: {str(e)}") - await _safe_websocket_send(ctx.deps.websocket, ctx.deps.stream_output) - - return f"Failed to plan task: {error_msg}" - - -@server.tool() -async def code_task(task: str, ctx: RunContext[orchestrator_deps]) -> str: - """Assigns coding tasks to the coder agent""" - try: - logfire.info(f"Assigning coding task: {task}") - print(f"Assigning coding task: {task} and context: {ctx}") - # Create a new StreamResponse for Coder Agent - coder_stream_output = StreamResponse( - agent_name="Coder Agent", - instructions=task, - steps=[], - output="", - status_code=0 - ) - - # Add to orchestrator's response collection if available - if ctx.deps.agent_responses is not None: - ctx.deps.agent_responses.append(coder_stream_output) - - # Send initial update for Coder Agent - await _safe_websocket_send(ctx.deps.websocket, coder_stream_output) - - # Create deps with the new stream_output - deps_for_coder_agent = CoderAgentDeps( - websocket=ctx.deps.websocket, - stream_output=coder_stream_output - ) - - # Run coder agent - coder_response = await coder_agent.run( - user_prompt=task, - deps=deps_for_coder_agent - ) - - # Extract response data - response_data = coder_response.data.content - - # Update coder_stream_output with coding results - coder_stream_output.output = response_data - coder_stream_output.status_code = 200 - coder_stream_output.steps.append("Coding task completed successfully") - await _safe_websocket_send(ctx.deps.websocket, coder_stream_output) - - # Add a reminder in the result message to update the plan using planner_agent_update - response_with_reminder = f"{response_data}\n\nReminder: You must now call planner_agent_update with the completed task description: \"{task} (coder_agent)\"" - - return response_with_reminder - except Exception as e: - error_msg = f"Error assigning coding task: {str(e)}" - logfire.error(error_msg, exc_info=True) - - # Update coder_stream_output with error - coder_stream_output.steps.append(f"Coding task failed: {str(e)}") - coder_stream_output.status_code = 500 - await _safe_websocket_send(ctx.deps.websocket, coder_stream_output) - - return f"Failed to assign coding task: {error_msg}" - - -@server.tool() -async def web_surf_task(task: str,ctx: RunContext[orchestrator_deps]) -> str: - """Assigns web surfing tasks to the web surfer agent""" - try: - logfire.info(f"Assigning web surfing task: {task}") - - # Create a new StreamResponse for WebSurfer - web_surfer_stream_output = StreamResponse( - agent_name="Web Surfer", - instructions=task, - steps=[], - output="", - status_code=0, - live_url=None - ) - - # Add to orchestrator's response collection if available - if ctx.deps.agent_responses is not None: - ctx.deps.agent_responses.append(web_surfer_stream_output) - - await _safe_websocket_send(ctx.deps.websocket, web_surfer_stream_output) - - # Initialize WebSurfer agent - web_surfer_agent = WebSurfer(api_url="http://localhost:8000/api/v1/web/stream") - - # Run WebSurfer with its own stream_output - success, message, messages = await web_surfer_agent.generate_reply( - instruction=task, - websocket=ctx.deps.websocket, - stream_output=web_surfer_stream_output - ) - - # Update WebSurfer's stream_output with final result - if success: - web_surfer_stream_output.steps.append("Web search completed successfully") - web_surfer_stream_output.output = message - web_surfer_stream_output.status_code = 200 - - # Add a reminder to update the plan - message_with_reminder = f"{message}\n\nReminder: You must now call planner_agent_update with the completed task description: \"{task} (web_surfer_agent)\"" - else: - web_surfer_stream_output.steps.append(f"Web search completed with issues: {message[:100]}") - web_surfer_stream_output.status_code = 500 - message_with_reminder = message - - await _safe_websocket_send(ctx.deps.websocket, web_surfer_stream_output) - - web_surfer_stream_output.steps.append(f"WebSurfer completed: {'Success' if success else 'Failed'}") - await _safe_websocket_send(ctx.deps.websocket, web_surfer_stream_output) - - return message_with_reminder - except Exception as e: - error_msg = f"Error assigning web surfing task: {str(e)}" - logfire.error(error_msg, exc_info=True) - - # Update WebSurfer's stream_output with error - web_surfer_stream_output.steps.append(f"Web search failed: {str(e)}") - web_surfer_stream_output.status_code = 500 - await _safe_websocket_send(ctx.deps.websocket, web_surfer_stream_output) - return f"Failed to assign web surfing task: {error_msg}" - -@server.tool() -async def ask_human(question: str, ctx: RunContext[orchestrator_deps]) -> str: - """Sends a question to the frontend and waits for human input""" - try: - logfire.info(f"Asking human: {question}") - print(f"Asking human: {question} and context: {ctx}") - # Create a new StreamResponse for Human Input - human_stream_output = StreamResponse( - agent_name="Human Input", - instructions=question, - steps=[], - output="", - status_code=0 - ) - - # Add to orchestrator's response collection if available - if ctx.deps.agent_responses is not None: - ctx.deps.agent_responses.append(human_stream_output) - - # Send the question to frontend - await _safe_websocket_send(ctx.deps.websocket, human_stream_output) - - # Update stream with waiting message - human_stream_output.steps.append("Waiting for human input...") - await _safe_websocket_send(ctx.deps.websocket, human_stream_output) - - # Wait for response from frontend - response = await ctx.deps.websocket.receive_text() - - # Update stream with response - human_stream_output.steps.append("Received human input") - human_stream_output.output = response - human_stream_output.status_code = 200 - await _safe_websocket_send(ctx.deps.websocket, human_stream_output) - - return response - except Exception as e: - error_msg = f"Error getting human input: {str(e)}" - logfire.error(error_msg, exc_info=True) - - # Update stream with error - human_stream_output.steps.append(f"Failed to get human input: {str(e)}") - human_stream_output.status_code = 500 - await _safe_websocket_send(ctx.deps.websocket, human_stream_output) - - return f"Failed to get human input: {error_msg}" - -@server.tool() -async def planner_agent_update(completed_task: str,ctx: RunContext[orchestrator_deps]) -> str: - """ - Updates the todo.md file to mark a task as completed and returns the full updated plan. - - Args: - completed_task: Description of the completed task including which agent performed it - - Returns: - The complete updated todo.md content with tasks marked as completed - """ - try: - logfire.info(f"Updating plan with completed task: {completed_task}") - print(f"Updating plan with completed task: {completed_task} and context: {ctx}") - # Create a new StreamResponse for Planner Agent update - planner_stream_output = StreamResponse( - agent_name="Planner Agent", - instructions=f"Update todo.md to mark as completed: {completed_task}", - steps=[], - output="", - status_code=0 - ) - - # Send initial update - await _safe_websocket_send(ctx.deps.websocket, planner_stream_output) - - # Directly read and update the todo.md file - base_dir = os.path.abspath(os.path.dirname(os.path.dirname(__file__))) - planner_dir = os.path.join(base_dir, "agents", "planner") - todo_path = os.path.join(planner_dir, "todo.md") - - planner_stream_output.steps.append("Reading current todo.md...") - await _safe_websocket_send(ctx.deps.websocket, planner_stream_output) - - # Make sure the directory exists - os.makedirs(planner_dir, exist_ok=True) - - try: - # Check if todo.md exists - if not os.path.exists(todo_path): - planner_stream_output.steps.append("No todo.md file found. Will create new one after task completion.") - await _safe_websocket_send(ctx.deps.websocket, planner_stream_output) - - # We'll directly call planner_agent.run() to create a new plan first - plan_prompt = f"Create a simple task plan based on this completed task: {completed_task}" - plan_response = await planner_agent.run(user_prompt=plan_prompt) - current_content = plan_response.data.plan - else: - # Read existing todo.md - with open(todo_path, "r") as file: - current_content = file.read() - planner_stream_output.steps.append(f"Found existing todo.md ({len(current_content)} bytes)") - await _safe_websocket_send(ctx.deps.websocket, planner_stream_output) - - # Now call planner_agent.run() with specific instructions to update the plan - update_prompt = f""" - Here is the current todo.md content: - - {current_content} - - Please update this plan to mark the following task as completed: {completed_task} - Return ONLY the fully updated plan with appropriate tasks marked as [x] instead of [ ]. - """ - - planner_stream_output.steps.append("Asking planner to update the plan...") - await _safe_websocket_send(ctx.deps.websocket, planner_stream_output) - - updated_plan_response = await planner_agent.run(user_prompt=update_prompt) - updated_plan = updated_plan_response.data.plan - - # Write the updated plan back to todo.md - with open(todo_path, "w") as file: - file.write(updated_plan) - - planner_stream_output.steps.append("Plan updated successfully") - planner_stream_output.output = updated_plan - planner_stream_output.status_code = 200 - await _safe_websocket_send(ctx.deps.websocket, planner_stream_output) - - # Update orchestrator stream - if ctx.deps.stream_output: - ctx.deps.stream_output.steps.append(f"Plan updated to mark task as completed: {completed_task}") - await _safe_websocket_send(ctx.deps.websocket, ctx.deps.stream_output) - - return updated_plan - - except Exception as e: - error_msg = f"Error during plan update operations: {str(e)}" - logfire.error(error_msg, exc_info=True) - - planner_stream_output.steps.append(f"Plan update failed: {str(e)}") - planner_stream_output.status_code = 500 - await _safe_websocket_send(ctx.deps.websocket, planner_stream_output) - - return f"Failed to update the plan: {error_msg}" - - except Exception as e: - error_msg = f"Error updating plan: {str(e)}" - logfire.error(error_msg, exc_info=True) - - # Update stream output with error - if ctx.deps.stream_output: - ctx.deps.stream_output.steps.append(f"Failed to update plan: {str(e)}") - await _safe_websocket_send(ctx.deps.websocket, ctx.deps.stream_output) - - return f"Failed to update plan: {error_msg}" - -async def _safe_websocket_send(websocket: Optional[WebSocket], message: Any) -> bool: - """Safely send message through websocket with error handling""" - try: - if websocket and websocket.client_state.CONNECTED: - await websocket.send_text(json.dumps(asdict(message))) - logfire.debug("WebSocket message sent (_safe_websocket_send): {message}", message=message) - return True - return False - except Exception as e: - logfire.error(f"WebSocket send failed: {str(e)}") - return False - def run_server(): """Run the MCP server""" server.run(transport="sse") diff --git a/cortex_on/instructor.py b/cortex_on/instructor.py index 6731efd..92f2958 100644 --- a/cortex_on/instructor.py +++ b/cortex_on/instructor.py @@ -7,6 +7,7 @@ from datetime import datetime from typing import Any, Dict, List, Optional, Tuple, Union import uuid +import threading # Third-party imports from dotenv import load_dotenv @@ -18,16 +19,34 @@ from pydantic_ai.models.anthropic import AnthropicModel # Local application imports -from agents.code_agent import coder_agent +from agents.code_agent import CoderAgentDeps, coder_agent from agents.orchestrator_agent import orchestrator_agent, orchestrator_deps from agents.planner_agent import planner_agent from agents.web_surfer import WebSurfer from utils.ant_client import get_client from utils.stream_response_format import StreamResponse - +from agents.mcp_server import server load_dotenv() +# Flag to track if MCP server is running +_mcp_server_running = False +def start_mcp_server_in_thread(): + """Start the MCP server in a separate thread""" + global _mcp_server_running + if _mcp_server_running: + return + + _mcp_server_running = True + + def run_server(): + logfire.info("Starting MCP server...") + server.run(transport="sse") + + # Start in a separate thread + thread = threading.Thread(target=run_server, daemon=True) + thread.start() + logfire.info("MCP server thread started") class DateTimeEncoder(json.JSONEncoder): @@ -46,6 +65,335 @@ def default(self, obj): return super().default(obj) +def register_tools(websocket: WebSocket) -> None: + """ + Dynamically register MCP server tools with the provided WebSocket. + This ensures all tools have access to the active WebSocket connection. + """ + # First, unregister existing tools if they exist + tool_names = ["plan_task", "code_task", "web_surf_task", "ask_human", "planner_agent_update"] + for tool_name in tool_names: + if tool_name in server._tool_manager._tools: + del server._tool_manager._tools[tool_name] + + logfire.info("Registering MCP tools with WebSocket connection") + + # Function to create each tool with the websocket in closure + async def plan_task(task: str) -> str: + """Plans the task and assigns it to the appropriate agents""" + try: + logfire.info(f"Planning task: {task}") + print(f"Planning task: {task}") + # Create a new StreamResponse for Planner Agent + planner_stream_output = StreamResponse( + agent_name="Planner Agent", + instructions=task, + steps=[], + output="", + status_code=0 + ) + + await _safe_websocket_send(websocket, planner_stream_output) + + # Update planner stream + planner_stream_output.steps.append("Planning task...") + await _safe_websocket_send(websocket, planner_stream_output) + + # Run planner agent + planner_response = await planner_agent.run(user_prompt=task) + + # Update planner stream with results + plan_text = planner_response.data.plan + planner_stream_output.steps.append("Task planned successfully") + planner_stream_output.output = plan_text + planner_stream_output.status_code = 200 + await _safe_websocket_send(websocket, planner_stream_output) + + return f"Task planned successfully\nTask: {plan_text}" + except Exception as e: + error_msg = f"Error planning task: {str(e)}" + logfire.error(error_msg, exc_info=True) + + # Update planner stream with error + if 'planner_stream_output' in locals(): + planner_stream_output.steps.append(f"Planning failed: {str(e)}") + planner_stream_output.status_code = 500 + await _safe_websocket_send(websocket, planner_stream_output) + + return f"Failed to plan task: {error_msg}" + + async def code_task(task: str) -> str: + """Assigns coding tasks to the coder agent""" + try: + logfire.info(f"Assigning coding task: {task}") + print(f"Assigning coding task: {task}") + # Create a new StreamResponse for Coder Agent + coder_stream_output = StreamResponse( + agent_name="Coder Agent", + instructions=task, + steps=[], + output="", + status_code=0 + ) + + await _safe_websocket_send(websocket, coder_stream_output) + + # Create deps with the new stream_output + deps_for_coder_agent = CoderAgentDeps( + websocket=websocket, + stream_output=coder_stream_output + ) + + # Run coder agent + coder_response = await coder_agent.run( + user_prompt=task, + deps=deps_for_coder_agent + ) + + # Extract response data + response_data = coder_response.data.content + + # Update coder_stream_output with coding results + coder_stream_output.output = response_data + coder_stream_output.status_code = 200 + coder_stream_output.steps.append("Coding task completed successfully") + await _safe_websocket_send(websocket, coder_stream_output) + + # Add a reminder in the result message to update the plan using planner_agent_update + response_with_reminder = f"{response_data}\n\nReminder: You must now call planner_agent_update with the completed task description: \"{task} (coder_agent)\"" + + return response_with_reminder + except Exception as e: + error_msg = f"Error assigning coding task: {str(e)}" + logfire.error(error_msg, exc_info=True) + + # Update coder_stream_output with error + if 'coder_stream_output' in locals(): + coder_stream_output.steps.append(f"Coding task failed: {str(e)}") + coder_stream_output.status_code = 500 + await _safe_websocket_send(websocket, coder_stream_output) + + return f"Failed to assign coding task: {error_msg}" + + async def web_surf_task(task: str) -> str: + """Assigns web surfing tasks to the web surfer agent""" + try: + logfire.info(f"Assigning web surfing task: {task}") + + # Create a new StreamResponse for WebSurfer + web_surfer_stream_output = StreamResponse( + agent_name="Web Surfer", + instructions=task, + steps=[], + output="", + status_code=0, + live_url=None + ) + + await _safe_websocket_send(websocket, web_surfer_stream_output) + + # Initialize WebSurfer agent + web_surfer_agent = WebSurfer(api_url="http://localhost:8000/api/v1/web/stream") + + # Run WebSurfer with its own stream_output + success, message, messages = await web_surfer_agent.generate_reply( + instruction=task, + websocket=websocket, + stream_output=web_surfer_stream_output + ) + + # Update WebSurfer's stream_output with final result + if success: + web_surfer_stream_output.steps.append("Web search completed successfully") + web_surfer_stream_output.output = message + web_surfer_stream_output.status_code = 200 + + # Add a reminder to update the plan + message_with_reminder = f"{message}\n\nReminder: You must now call planner_agent_update with the completed task description: \"{task} (web_surfer_agent)\"" + else: + web_surfer_stream_output.steps.append(f"Web search completed with issues: {message[:100]}") + web_surfer_stream_output.status_code = 500 + message_with_reminder = message + + await _safe_websocket_send(websocket, web_surfer_stream_output) + + web_surfer_stream_output.steps.append(f"WebSurfer completed: {'Success' if success else 'Failed'}") + await _safe_websocket_send(websocket, web_surfer_stream_output) + + return message_with_reminder + except Exception as e: + error_msg = f"Error assigning web surfing task: {str(e)}" + logfire.error(error_msg, exc_info=True) + + # Update WebSurfer's stream_output with error + if 'web_surfer_stream_output' in locals(): + web_surfer_stream_output.steps.append(f"Web search failed: {str(e)}") + web_surfer_stream_output.status_code = 500 + await _safe_websocket_send(websocket, web_surfer_stream_output) + return f"Failed to assign web surfing task: {error_msg}" + + async def ask_human(question: str) -> str: + """Sends a question to the frontend and waits for human input""" + try: + logfire.info(f"Asking human: {question}") + print(f"Asking human: {question}") + # Create a new StreamResponse for Human Input + human_stream_output = StreamResponse( + agent_name="Human Input", + instructions=question, + steps=[], + output="", + status_code=0 + ) + + # Send the question to frontend + await _safe_websocket_send(websocket, human_stream_output) + + # Update stream with waiting message + human_stream_output.steps.append("Waiting for human input...") + await _safe_websocket_send(websocket, human_stream_output) + + # Wait for response from frontend + response = await websocket.receive_text() + + # Update stream with response + human_stream_output.steps.append("Received human input") + human_stream_output.output = response + human_stream_output.status_code = 200 + await _safe_websocket_send(websocket, human_stream_output) + + return response + except Exception as e: + error_msg = f"Error getting human input: {str(e)}" + logfire.error(error_msg, exc_info=True) + + # Update stream with error + if 'human_stream_output' in locals(): + human_stream_output.steps.append(f"Failed to get human input: {str(e)}") + human_stream_output.status_code = 500 + await _safe_websocket_send(websocket, human_stream_output) + + return f"Failed to get human input: {error_msg}" + + async def planner_agent_update(completed_task: str) -> str: + """ + Updates the todo.md file to mark a task as completed and returns the full updated plan. + """ + try: + logfire.info(f"Updating plan with completed task: {completed_task}") + print(f"Updating plan with completed task: {completed_task}") + # Create a new StreamResponse for Planner Agent update + planner_stream_output = StreamResponse( + agent_name="Planner Agent", + instructions=f"Update todo.md to mark as completed: {completed_task}", + steps=[], + output="", + status_code=0 + ) + + # Send initial update + await _safe_websocket_send(websocket, planner_stream_output) + + # Directly read and update the todo.md file + base_dir = os.path.abspath(os.path.dirname(os.path.dirname(__file__))) + planner_dir = os.path.join(base_dir, "agents", "planner") + todo_path = os.path.join(planner_dir, "todo.md") + + planner_stream_output.steps.append("Reading current todo.md...") + await _safe_websocket_send(websocket, planner_stream_output) + + # Make sure the directory exists + os.makedirs(planner_dir, exist_ok=True) + + try: + # Check if todo.md exists + if not os.path.exists(todo_path): + planner_stream_output.steps.append("No todo.md file found. Will create new one after task completion.") + await _safe_websocket_send(websocket, planner_stream_output) + + # We'll directly call planner_agent.run() to create a new plan first + plan_prompt = f"Create a simple task plan based on this completed task: {completed_task}" + plan_response = await planner_agent.run(user_prompt=plan_prompt) + current_content = plan_response.data.plan + else: + # Read existing todo.md + with open(todo_path, "r") as file: + current_content = file.read() + planner_stream_output.steps.append(f"Found existing todo.md ({len(current_content)} bytes)") + await _safe_websocket_send(websocket, planner_stream_output) + + # Now call planner_agent.run() with specific instructions to update the plan + update_prompt = f""" + Here is the current todo.md content: + + {current_content} + + Please update this plan to mark the following task as completed: {completed_task} + Return ONLY the fully updated plan with appropriate tasks marked as [x] instead of [ ]. + """ + + planner_stream_output.steps.append("Asking planner to update the plan...") + await _safe_websocket_send(websocket, planner_stream_output) + + updated_plan_response = await planner_agent.run(user_prompt=update_prompt) + updated_plan = updated_plan_response.data.plan + + # Write the updated plan back to todo.md + with open(todo_path, "w") as file: + file.write(updated_plan) + + planner_stream_output.steps.append("Plan updated successfully") + planner_stream_output.output = updated_plan + planner_stream_output.status_code = 200 + await _safe_websocket_send(websocket, planner_stream_output) + + return updated_plan + + except Exception as e: + error_msg = f"Error during plan update operations: {str(e)}" + logfire.error(error_msg, exc_info=True) + + planner_stream_output.steps.append(f"Plan update failed: {str(e)}") + planner_stream_output.status_code = 500 + await _safe_websocket_send(websocket, planner_stream_output) + + return f"Failed to update the plan: {error_msg}" + + except Exception as e: + error_msg = f"Error updating plan: {str(e)}" + logfire.error(error_msg, exc_info=True) + + return f"Failed to update plan: {error_msg}" + + # Helper function for websocket communication + async def _safe_websocket_send(socket: WebSocket, message: Any) -> bool: + """Safely send message through websocket with error handling""" + try: + if socket and socket.client_state.CONNECTED: + await socket.send_text(json.dumps(asdict(message))) + logfire.debug("WebSocket message sent (_safe_websocket_send): {message}", message=message) + return True + return False + except Exception as e: + logfire.error(f"WebSocket send failed: {str(e)}") + return False + + # Now register all the generated tools with the MCP server + tool_definitions = { + "plan_task": (plan_task, "Plans the task and assigns it to the appropriate agents"), + "code_task": (code_task, "Assigns coding tasks to the coder agent"), + "web_surf_task": (web_surf_task, "Assigns web surfing tasks to the web surfer agent"), + "ask_human": (ask_human, "Sends a question to the frontend and waits for human input"), + "planner_agent_update": (planner_agent_update, "Updates the todo.md file to mark a task as completed") + } + + # Register each tool + for name, (fn, desc) in tool_definitions.items(): + server._tool_manager.add_tool(fn, name=name, description=desc) + + logfire.info(f"Successfully registered {len(tool_definitions)} tools with the MCP server") + + # Main Orchestrator Class class SystemInstructor: def __init__(self): @@ -94,6 +442,15 @@ async def run(self, task: str, websocket: WebSocket) -> List[Dict[str, Any]]: ) try: + # Register tools first - before MCP server starts or is accessed by orchestrator + register_tools(websocket=self.websocket) + + # Start the MCP server if it's not already running + start_mcp_server_in_thread() + + # Give MCP server a moment to initialize + await asyncio.sleep(1) + # Initialize system await self._safe_websocket_send(stream_output) stream_output.steps.append("Agents initialized successfully") From f80608fcdcf72ebf9985103139850c50e40d8cf1 Mon Sep 17 00:00:00 2001 From: Sakalya100 Date: Thu, 24 Apr 2025 19:53:52 +0530 Subject: [PATCH 15/35] refactor(MCP Server Tool Integration) - Moved the ask_human function from instructor.py to orchestrator_agent.py, attached to client directly to avoid future event loops --- cortex_on/agents/orchestrator_agent.py | 59 +++++++++++++++++++++++++- cortex_on/instructor.py | 44 ------------------- 2 files changed, 58 insertions(+), 45 deletions(-) diff --git a/cortex_on/agents/orchestrator_agent.py b/cortex_on/agents/orchestrator_agent.py index 0dc3db0..7c570c3 100644 --- a/cortex_on/agents/orchestrator_agent.py +++ b/cortex_on/agents/orchestrator_agent.py @@ -181,7 +181,64 @@ class orchestrator_deps: mcp_servers=[server], ) - +@orchestrator_agent.tool +async def ask_human(ctx: RunContext[orchestrator_deps], question: str) -> str: + """Sends a question to the frontend and waits for human input""" + try: + logfire.info(f"Asking human: {question}") + + # Create a new StreamResponse for Human Input + human_stream_output = StreamResponse( + agent_name="Human Input", + instructions=question, + steps=[], + output="", + status_code=0 + ) + + # Add to orchestrator's response collection if available + if ctx.deps.agent_responses is not None: + ctx.deps.agent_responses.append(human_stream_output) + + # Send the question to frontend + await _safe_websocket_send(ctx.deps.websocket, human_stream_output) + + # Update stream with waiting message + human_stream_output.steps.append("Waiting for human input...") + await _safe_websocket_send(ctx.deps.websocket, human_stream_output) + + # Wait for response from frontend + response = await ctx.deps.websocket.receive_text() + + # Update stream with response + human_stream_output.steps.append("Received human input") + human_stream_output.output = response + human_stream_output.status_code = 200 + await _safe_websocket_send(ctx.deps.websocket, human_stream_output) + + return response + except Exception as e: + error_msg = f"Error getting human input: {str(e)}" + logfire.error(error_msg, exc_info=True) + + # Update stream with error + human_stream_output.steps.append(f"Failed to get human input: {str(e)}") + human_stream_output.status_code = 500 + await _safe_websocket_send(ctx.deps.websocket, human_stream_output) + + return f"Failed to get human input: {error_msg}" + +async def _safe_websocket_send(socket: WebSocket, message: Any) -> bool: + """Safely send message through websocket with error handling""" + try: + if socket and socket.client_state.CONNECTED: + await socket.send_text(json.dumps(asdict(message))) + logfire.debug("WebSocket message sent (_safe_websocket_send): {message}", message=message) + return True + return False + except Exception as e: + logfire.error(f"WebSocket send failed: {str(e)}") + return False # @orchestrator_agent.tool # async def plan_task(ctx: RunContext[orchestrator_deps], task: str) -> str: # """Plans the task and assigns it to the appropriate agents""" diff --git a/cortex_on/instructor.py b/cortex_on/instructor.py index 92f2958..86468e6 100644 --- a/cortex_on/instructor.py +++ b/cortex_on/instructor.py @@ -232,49 +232,6 @@ async def web_surf_task(task: str) -> str: await _safe_websocket_send(websocket, web_surfer_stream_output) return f"Failed to assign web surfing task: {error_msg}" - async def ask_human(question: str) -> str: - """Sends a question to the frontend and waits for human input""" - try: - logfire.info(f"Asking human: {question}") - print(f"Asking human: {question}") - # Create a new StreamResponse for Human Input - human_stream_output = StreamResponse( - agent_name="Human Input", - instructions=question, - steps=[], - output="", - status_code=0 - ) - - # Send the question to frontend - await _safe_websocket_send(websocket, human_stream_output) - - # Update stream with waiting message - human_stream_output.steps.append("Waiting for human input...") - await _safe_websocket_send(websocket, human_stream_output) - - # Wait for response from frontend - response = await websocket.receive_text() - - # Update stream with response - human_stream_output.steps.append("Received human input") - human_stream_output.output = response - human_stream_output.status_code = 200 - await _safe_websocket_send(websocket, human_stream_output) - - return response - except Exception as e: - error_msg = f"Error getting human input: {str(e)}" - logfire.error(error_msg, exc_info=True) - - # Update stream with error - if 'human_stream_output' in locals(): - human_stream_output.steps.append(f"Failed to get human input: {str(e)}") - human_stream_output.status_code = 500 - await _safe_websocket_send(websocket, human_stream_output) - - return f"Failed to get human input: {error_msg}" - async def planner_agent_update(completed_task: str) -> str: """ Updates the todo.md file to mark a task as completed and returns the full updated plan. @@ -383,7 +340,6 @@ async def _safe_websocket_send(socket: WebSocket, message: Any) -> bool: "plan_task": (plan_task, "Plans the task and assigns it to the appropriate agents"), "code_task": (code_task, "Assigns coding tasks to the coder agent"), "web_surf_task": (web_surf_task, "Assigns web surfing tasks to the web surfer agent"), - "ask_human": (ask_human, "Sends a question to the frontend and waits for human input"), "planner_agent_update": (planner_agent_update, "Updates the todo.md file to mark a task as completed") } From 6801d4df566d8085886a0279a9aa30999cef3495 Mon Sep 17 00:00:00 2001 From: aryan Date: Sat, 26 Apr 2025 20:51:58 +0530 Subject: [PATCH 16/35] Enhance agent section generation to avoid duplication - Added a unique message_id to responses in instructor.py and orchestrator_agent.py to improve message tracking. - Updated StreamResponse format to include message_id for better identification of messages. - Modified ChatList component to handle message_id for updating and rendering messages, ensuring accurate message updates and preventing duplicates. --- cortex_on/agents/orchestrator_agent.py | 4 +- cortex_on/instructor.py | 15 ++- cortex_on/utils/stream_response_format.py | 2 + frontend/src/components/home/ChatList.tsx | 140 ++++++++++++---------- frontend/src/types/chatTypes.ts | 2 + 5 files changed, 96 insertions(+), 67 deletions(-) diff --git a/cortex_on/agents/orchestrator_agent.py b/cortex_on/agents/orchestrator_agent.py index 7c570c3..cfb5a7b 100644 --- a/cortex_on/agents/orchestrator_agent.py +++ b/cortex_on/agents/orchestrator_agent.py @@ -3,6 +3,7 @@ import traceback from typing import List, Optional, Dict, Any, Union, Tuple from datetime import datetime +import uuid from pydantic import BaseModel from dataclasses import asdict, dataclass import logfire @@ -193,7 +194,8 @@ async def ask_human(ctx: RunContext[orchestrator_deps], question: str) -> str: instructions=question, steps=[], output="", - status_code=0 + status_code=0, + message_id=str(uuid.uuid4()) ) # Add to orchestrator's response collection if available diff --git a/cortex_on/instructor.py b/cortex_on/instructor.py index 86468e6..4786f0b 100644 --- a/cortex_on/instructor.py +++ b/cortex_on/instructor.py @@ -90,7 +90,8 @@ async def plan_task(task: str) -> str: instructions=task, steps=[], output="", - status_code=0 + status_code=0, + message_id=str(uuid.uuid4()) ) await _safe_websocket_send(websocket, planner_stream_output) @@ -133,7 +134,8 @@ async def code_task(task: str) -> str: instructions=task, steps=[], output="", - status_code=0 + status_code=0, + message_id=str(uuid.uuid4()) ) await _safe_websocket_send(websocket, coder_stream_output) @@ -187,7 +189,8 @@ async def web_surf_task(task: str) -> str: steps=[], output="", status_code=0, - live_url=None + live_url=None, + message_id=str(uuid.uuid4()) ) await _safe_websocket_send(websocket, web_surfer_stream_output) @@ -245,7 +248,8 @@ async def planner_agent_update(completed_task: str) -> str: instructions=f"Update todo.md to mark as completed: {completed_task}", steps=[], output="", - status_code=0 + status_code=0, + message_id=str(uuid.uuid4()) ) # Send initial update @@ -386,7 +390,8 @@ async def run(self, task: str, websocket: WebSocket) -> List[Dict[str, Any]]: instructions=task, steps=[], output="", - status_code=0 + status_code=0, + message_id=str(uuid.uuid4()) ) self.orchestrator_response.append(stream_output) diff --git a/cortex_on/utils/stream_response_format.py b/cortex_on/utils/stream_response_format.py index d99ac9a..286761e 100644 --- a/cortex_on/utils/stream_response_format.py +++ b/cortex_on/utils/stream_response_format.py @@ -1,5 +1,6 @@ from dataclasses import dataclass from typing import List, Optional +import uuid @dataclass class StreamResponse: @@ -9,3 +10,4 @@ class StreamResponse: status_code: int output: str live_url: Optional[str] = None + message_id: str = "" # Unique identifier for each message diff --git a/frontend/src/components/home/ChatList.tsx b/frontend/src/components/home/ChatList.tsx index 9d8cb21..0ab5b18 100644 --- a/frontend/src/components/home/ChatList.tsx +++ b/frontend/src/components/home/ChatList.tsx @@ -130,7 +130,7 @@ const ChatList = ({isLoading, setIsLoading}: ChatListPageProps) => { setIsLoading(true); const lastMessageData = lastMessage.data || []; - const {agent_name, instructions, steps, output, status_code, live_url} = + const {agent_name, instructions, steps, output, status_code, live_url, message_id} = lastJsonMessage as SystemMessage; console.log(lastJsonMessage); @@ -146,12 +146,14 @@ const ChatList = ({isLoading, setIsLoading}: ChatListPageProps) => { setLiveUrl(""); } - const agentIndex = lastMessageData.findIndex( - (agent: SystemMessage) => agent.agent_name === agent_name + // Check if we already have a message with this message_id + const existingMessageIndex = lastMessageData.findIndex( + (msg: SystemMessage) => msg.message_id === message_id ); let updatedLastMessageData; - if (agentIndex !== -1) { + if (existingMessageIndex !== -1 && message_id) { + // If we have a message_id and it already exists, update that specific message let filteredSteps = steps; if (agent_name === "Web Surfer") { const plannerStep = steps.find((step) => step.startsWith("Plan")); @@ -163,15 +165,17 @@ const ChatList = ({isLoading, setIsLoading}: ChatListPageProps) => { : steps.filter((step) => step.startsWith("Current")); } updatedLastMessageData = [...lastMessageData]; - updatedLastMessageData[agentIndex] = { + updatedLastMessageData[existingMessageIndex] = { agent_name, instructions, steps: filteredSteps, output, status_code, live_url, + message_id }; } else { + // If message_id doesn't exist or we don't have a message_id, add a new entry updatedLastMessageData = [ ...lastMessageData, { @@ -181,6 +185,7 @@ const ChatList = ({isLoading, setIsLoading}: ChatListPageProps) => { output, status_code, live_url, + message_id: message_id || `${agent_name}-${Date.now()}` // Fallback unique ID if message_id isn't provided }, ]; } @@ -197,19 +202,31 @@ const ChatList = ({isLoading, setIsLoading}: ChatListPageProps) => { if (status_code === 200) { setOutputsList((prevList) => { - const existingIndex = prevList.findIndex( - (item) => item.agent === agent_name - ); - + // If we have a message_id, look for an exact match + // If not found, create a new entry rather than updating by agent name + const indexByMessageId = message_id + ? prevList.findIndex(item => item.id === message_id) + : -1; + let newList; let newOutputIndex; - if (existingIndex >= 0) { + if (indexByMessageId >= 0) { + // Update the existing entry with matching message_id newList = [...prevList]; - newList[existingIndex] = {agent: agent_name, output}; - newOutputIndex = existingIndex; + newList[indexByMessageId] = { + agent: agent_name, + output, + id: message_id + }; + newOutputIndex = indexByMessageId; } else { - newList = [...prevList, {agent: agent_name, output}]; + // Create a new entry with this output + newList = [...prevList, { + agent: agent_name, + output, + id: message_id || `${agent_name}-${Date.now()}` + }]; newOutputIndex = newList.length - 1; } @@ -580,7 +597,7 @@ const ChatList = ({isLoading, setIsLoading}: ChatListPageProps) => { systemMessage.agent_name === "Orchestrator" ? (
{systemMessage.steps && @@ -609,7 +626,7 @@ const ChatList = ({isLoading, setIsLoading}: ChatListPageProps) => { ) : systemMessage.agent_name === "Human Input" ? (
@@ -761,7 +778,7 @@ const ChatList = ({isLoading, setIsLoading}: ChatListPageProps) => {
) : ( @@ -814,15 +831,16 @@ const ChatList = ({isLoading, setIsLoading}: ChatListPageProps) => { (systemMessage.agent_name !== "Web Surfer" && systemMessage.agent_name !== "Human Input" ? (
- handleOutputSelection( - outputsList.findIndex( - (item) => - item.agent === - systemMessage.agent_name - ) - ) - } + onClick={() => { + // First try to find by message_id, then fall back to agent name + const outputIndex = systemMessage.message_id + ? outputsList.findIndex(item => item.id === systemMessage.message_id) + : outputsList.findIndex(item => item.agent === systemMessage.agent_name); + + if (outputIndex >= 0) { + handleOutputSelection(outputIndex); + } + }} className="rounded-md w- py-2 px-4 bg-secondary text-secondary-foreground flex items-center justify-between cursor-pointer transition-all hover:shadow-md hover:scale-102 duration-300 animate-pulse-once" > {getAgentOutputCard( @@ -852,43 +870,43 @@ const ChatList = ({isLoading, setIsLoading}: ChatListPageProps) => { systemMessage?.output ) && (
- {message.data.find( - (systemMessage) => - systemMessage.agent_name === "Orchestrator" - )?.status_code === 200 ? ( -
- handleOutputSelection( - outputsList.findIndex( - (item) => item.agent === "Orchestrator" - ) - ) - } - className="rounded-md py-2 bg-[#F7E8FA] text-[#BD24CA] cursor-pointer transition-all hover:shadow-md hover:scale-102 duration-300 animate-pulse-once" - > -
- -

- Task has been completed. Click here to - view results. -

- + {(() => { + const orchestratorMessage = message.data.find( + (systemMessage) => + systemMessage.agent_name === "Orchestrator" + ); + return orchestratorMessage?.status_code === 200 ? ( +
{ + // First try to find by message_id, then fall back to agent name + const outputIndex = orchestratorMessage?.message_id + ? outputsList.findIndex(item => item.id === orchestratorMessage.message_id) + : outputsList.findIndex(item => item.agent === "Orchestrator"); + + if (outputIndex >= 0) { + handleOutputSelection(outputIndex); + } + }} + className="rounded-md py-2 bg-[#F7E8FA] text-[#BD24CA] cursor-pointer transition-all hover:shadow-md hover:scale-102 duration-300 animate-pulse-once" + > +
+ +

+ Task has been completed. Click here to + view results. +

+ +
-
- ) : ( - - systemMessage.agent_name === - "Orchestrator" - )?.output - } - /> - )} + ) : ( + + ); + })()}
)}
diff --git a/frontend/src/types/chatTypes.ts b/frontend/src/types/chatTypes.ts index a314798..56acfe7 100644 --- a/frontend/src/types/chatTypes.ts +++ b/frontend/src/types/chatTypes.ts @@ -15,6 +15,7 @@ export interface SystemMessage { output: string; status_code: number; live_url: string; + message_id?: string; } export interface Message { @@ -27,4 +28,5 @@ export interface Message { export interface AgentOutput { agent: string; output: string; + id?: string; } From d88f9f676e73470e76ba5bdba31a0b66a1aafaa7 Mon Sep 17 00:00:00 2001 From: aryan Date: Sat, 26 Apr 2025 23:32:14 +0530 Subject: [PATCH 17/35] feat(ServerManager for MCP): - Introduced a ServerManager class to handle multiple MCP server instances, allowing for dynamic server startup and management. - Updated the start_mcp_server function for better server initialization and registration of tools. - Enhanced the register_tools function to target specific server instances, improving modularity. - Modified the SystemInstructor class to support multiple server configurations, ensuring flexibility in orchestration. - Added backward compatibility for legacy server startup methods. --- cortex_on/agents/orchestrator_agent.py | 6 +- cortex_on/instructor.py | 136 ++++++++++++++++++------- ta-browser/core/orchestrator.py | 30 +++--- 3 files changed, 118 insertions(+), 54 deletions(-) diff --git a/cortex_on/agents/orchestrator_agent.py b/cortex_on/agents/orchestrator_agent.py index cfb5a7b..f61f38c 100644 --- a/cortex_on/agents/orchestrator_agent.py +++ b/cortex_on/agents/orchestrator_agent.py @@ -164,7 +164,11 @@ class orchestrator_deps: # Initialize MCP Server # server = MCPServerStdio('python', ["-m", "agents.mcp_server"]) -server = MCPServerHTTP(url='http://localhost:3001/sse') +#we can add multiple servers here +#example: +# server1 = MCPServerHTTP(url='http://localhost:8004/sse') +# server2 = MCPServerHTTP(url='http://localhost:8003/sse') +server = MCPServerHTTP(url='http://localhost:8002/sse') # Initialize Anthropic provider with API key provider = AnthropicProvider(api_key=os.environ.get("ANTHROPIC_API_KEY")) diff --git a/cortex_on/instructor.py b/cortex_on/instructor.py index 4786f0b..489a2b1 100644 --- a/cortex_on/instructor.py +++ b/cortex_on/instructor.py @@ -17,6 +17,7 @@ from pydantic_ai import Agent from pydantic_ai.messages import ModelMessage from pydantic_ai.models.anthropic import AnthropicModel +from mcp.server.fastmcp import FastMCP # Local application imports from agents.code_agent import CoderAgentDeps, coder_agent @@ -28,26 +29,68 @@ from agents.mcp_server import server load_dotenv() -# Flag to track if MCP server is running -_mcp_server_running = False - -def start_mcp_server_in_thread(): - """Start the MCP server in a separate thread""" - global _mcp_server_running - if _mcp_server_running: - return - - _mcp_server_running = True +# Server manager to handle multiple MCP servers +class ServerManager: + def __init__(self): + self.servers = {} # Dictionary to track running servers by port + self.default_port = 8002 # default port for main MCP server with agents as a tool - def run_server(): - logfire.info("Starting MCP server...") - server.run(transport="sse") + def start_server(self, port=None, name=None): + """Start an MCP server on the specified port""" + if port is None: + port = self.default_port + + if name is None: + name = f"mcp_server_{port}" + + # Check if server is already running on this port + if port in self.servers and self.servers[port]['running']: + logfire.info(f"MCP server already running on port {port}") + return + + # Configure server for this port + server_instance = FastMCP(name=name, host="0.0.0.0", port=port) + + # Track server in our registry + self.servers[port] = { + 'running': True, + 'name': name, + 'instance': server_instance, + 'thread': None + } + + def run_server(): + logfire.info(f"Starting MCP server '{name}' on port {port}...") + # Configure the server to use the specified port + server_instance.run(transport="sse") + + # Start in a separate thread + thread = threading.Thread(target=run_server, daemon=True) + thread.start() + self.servers[port]['thread'] = thread + logfire.info(f"MCP server thread started for '{name}' on port {port}") - # Start in a separate thread - thread = threading.Thread(target=run_server, daemon=True) - thread.start() - logfire.info("MCP server thread started") + def get_server(self, port=None): + """Get the server instance for the specified port""" + if port is None: + port = self.default_port + + if port in self.servers: + return self.servers[port]['instance'] + return None + +# Initialize the server manager +server_manager = ServerManager() +def start_mcp_server(port=None, name=None): + """Start an MCP server on the specified port""" + server_manager.start_server(port=port, name=name) + #we can add multiple servers here + +# For backwards compatibility +def start_mcp_server_in_thread(): + """Start the MCP server in a separate thread (legacy function)""" + start_mcp_server() class DateTimeEncoder(json.JSONEncoder): """Custom JSON encoder that can handle datetime objects and Pydantic models""" @@ -65,26 +108,34 @@ def default(self, obj): return super().default(obj) -def register_tools(websocket: WebSocket) -> None: +def register_tools_for_main_mcp_server(websocket: WebSocket, port=None) -> None: """ Dynamically register MCP server tools with the provided WebSocket. This ensures all tools have access to the active WebSocket connection. + + Args: + websocket: The active WebSocket connection + port: Optional port number to target a specific MCP server """ + # Get the appropriate server instance + server_instance = server_manager.get_server(port) + if server_instance is None: + logfire.error(f"No MCP server found on port {port or server_manager.default_port}") + return + # First, unregister existing tools if they exist tool_names = ["plan_task", "code_task", "web_surf_task", "ask_human", "planner_agent_update"] for tool_name in tool_names: - if tool_name in server._tool_manager._tools: - del server._tool_manager._tools[tool_name] + if tool_name in server_instance._tool_manager._tools: + del server_instance._tool_manager._tools[tool_name] logfire.info("Registering MCP tools with WebSocket connection") - # Function to create each tool with the websocket in closure async def plan_task(task: str) -> str: """Plans the task and assigns it to the appropriate agents""" try: logfire.info(f"Planning task: {task}") print(f"Planning task: {task}") - # Create a new StreamResponse for Planner Agent planner_stream_output = StreamResponse( agent_name="Planner Agent", instructions=task, @@ -347,11 +398,11 @@ async def _safe_websocket_send(socket: WebSocket, message: Any) -> bool: "planner_agent_update": (planner_agent_update, "Updates the todo.md file to mark a task as completed") } - # Register each tool + # Register each tool with the specified server instance for name, (fn, desc) in tool_definitions.items(): - server._tool_manager.add_tool(fn, name=name, description=desc) + server_instance._tool_manager.add_tool(fn, name=name, description=desc) - logfire.info(f"Successfully registered {len(tool_definitions)} tools with the MCP server") + logfire.info(f"Successfully registered {len(tool_definitions)} tools with the MCP server on port {port or server_manager.default_port}") # Main Orchestrator Class @@ -382,8 +433,15 @@ async def _safe_websocket_send(self, message: Any) -> bool: logfire.error(f"WebSocket send failed: {str(e)}") return False - async def run(self, task: str, websocket: WebSocket) -> List[Dict[str, Any]]: - """Main orchestration loop with comprehensive error handling""" + async def run(self, task: str, websocket: WebSocket, server_config: Optional[Dict[str, int]] = None) -> List[Dict[str, Any]]: + """ + Main orchestration loop with comprehensive error handling + + Args: + task: The task instructions + websocket: The active WebSocket connection + server_config: Optional configuration for MCP servers {name: port} + """ self.websocket = websocket stream_output = StreamResponse( agent_name="Orchestrator", @@ -403,20 +461,22 @@ async def run(self, task: str, websocket: WebSocket) -> List[Dict[str, Any]]: ) try: - # Register tools first - before MCP server starts or is accessed by orchestrator - register_tools(websocket=self.websocket) - - # Start the MCP server if it's not already running - start_mcp_server_in_thread() - - # Give MCP server a moment to initialize - await asyncio.sleep(1) - # Initialize system await self._safe_websocket_send(stream_output) - stream_output.steps.append("Agents initialized successfully") - await self._safe_websocket_send(stream_output) + + # Apply default server configuration if none provided + if server_config is None: + server_config = { + "main": server_manager.default_port + } + + # Start each configured MCP server + for server_name, port in server_config.items(): + start_mcp_server(port=port, name=server_name) + # Register tools for this server + register_tools_for_main_mcp_server(websocket=self.websocket, port=port) + # Configure orchestrator_agent to use the main MCP server port async with orchestrator_agent.run_mcp_servers(): orchestrator_response = await orchestrator_agent.run( user_prompt=task, diff --git a/ta-browser/core/orchestrator.py b/ta-browser/core/orchestrator.py index f112130..fc6a3b2 100644 --- a/ta-browser/core/orchestrator.py +++ b/ta-browser/core/orchestrator.py @@ -674,11 +674,11 @@ async def run(self, command): return await self.handle_context_limit_error() await self.handle_agent_error('planner', e) - self.log_token_usage( - agent_type='planner', - usage=planner_response.usage, - step=self.iteration_counter - ) + # self.log_token_usage( + # agent_type='planner', + # usage=planner_response.usage, + # step=self.iteration_counter + # ) browser_error = None tool_interactions_str = None @@ -717,11 +717,11 @@ async def run(self, command): logger.info(f"Browser Agent Response: {browser_response.data}") await self._update_current_url() - self.log_token_usage( - agent_type='browser', - usage=browser_response.usage, - step=self.iteration_counter - ) + # self.log_token_usage( + # agent_type='browser', + # usage=browser_response.usage, + # step=self.iteration_counter + # ) except BrowserNavigationError as e: # Immediately terminate the task with error details @@ -778,11 +778,11 @@ async def run(self, command): logger.info(f"Critique Response: {critique_data.final_response}") logger.info(f"Critique Terminate: {critique_data.terminate}") - self.log_token_usage( - agent_type='critique', - usage=critique_response.usage, - step=self.iteration_counter - ) + # self.log_token_usage( + # agent_type='critique', + # usage=critique_response.usage, + # step=self.iteration_counter + # ) if critique_data.terminate: # Generate final_response if missing From 2d970f30e2c7e9f9b8b8fa8aeca2f9d12ae11292 Mon Sep 17 00:00:00 2001 From: Sakalya100 Date: Mon, 28 Apr 2025 17:10:51 +0530 Subject: [PATCH 18/35] refactor(MCP Server Management and Tool Registration): - Refactored code and added ServerManager class and tool registration to mcp_server.py for server related functionalities - Removed print statements - Removed unused dependencies imported in files - Restructured instructor.py with SystemInstructor class only --- cortex_on/agents/mcp_server.py | 379 +++++++++++++++++++++++-- cortex_on/agents/orchestrator_agent.py | 349 +---------------------- cortex_on/instructor.py | 369 +----------------------- 3 files changed, 371 insertions(+), 726 deletions(-) diff --git a/cortex_on/agents/mcp_server.py b/cortex_on/agents/mcp_server.py index 2469b7a..cec1cc9 100644 --- a/cortex_on/agents/mcp_server.py +++ b/cortex_on/agents/mcp_server.py @@ -1,30 +1,375 @@ -from mcp.server.fastmcp import FastMCP -from pydantic_ai import Agent -from pydantic_ai.models.anthropic import AnthropicModel -from pydantic_ai import RunContext -from fastapi import WebSocket +#Standard library imports +import uuid +import threading import os from typing import List, Optional, Dict, Any, Union, Tuple import json from dataclasses import asdict -from utils.ant_client import get_client + +#Third party imports +from mcp.server.fastmcp import FastMCP +from fastapi import WebSocket +import logfire + +#Local imports from utils.stream_response_format import StreamResponse from agents.planner_agent import planner_agent from agents.code_agent import coder_agent from agents.code_agent import coder_agent, CoderAgentDeps -from agents.orchestrator_agent import orchestrator_deps from agents.web_surfer import WebSurfer -import logfire -# Initialize the single MCP server -server = FastMCP("CortexON MCP Server", host="0.0.0.0", port=3001) +# Server manager to handle multiple MCP servers +class ServerManager: + def __init__(self): + self.servers = {} # Dictionary to track running servers by port + self.default_port = 8002 # default port for main MCP server with agents as a tool + + def start_server(self, port=None, name=None): + """Start an MCP server on the specified port""" + if port is None: + port = self.default_port + + if name is None: + name = f"mcp_server_{port}" + + # Check if server is already running on this port + if port in self.servers and self.servers[port]['running']: + logfire.info(f"MCP server already running on port {port}") + return + + # Configure server for this port + server_instance = FastMCP(name=name, host="0.0.0.0", port=port) + + # Track server in our registry + self.servers[port] = { + 'running': True, + 'name': name, + 'instance': server_instance, + 'thread': None + } + + def run_server(): + logfire.info(f"Starting MCP server '{name}' on port {port}...") + # Configure the server to use the specified port + server_instance.run(transport="sse") + + # Start in a separate thread + thread = threading.Thread(target=run_server, daemon=True) + thread.start() + self.servers[port]['thread'] = thread + logfire.info(f"MCP server thread started for '{name}' on port {port}") + + def get_server(self, port=None): + """Get the server instance for the specified port""" + if port is None: + port = self.default_port + + if port in self.servers: + return self.servers[port]['instance'] + return None + +# Initialize the server manager +server_manager = ServerManager() + +def start_mcp_server(port=None, name=None): + """Start an MCP server on the specified port""" + server_manager.start_server(port=port, name=name) + #we can add multiple servers here + +# For backwards compatibility +def start_mcp_server_in_thread(): + """Start the MCP server in a separate thread (legacy function)""" + start_mcp_server() + +def register_tools_for_main_mcp_server(websocket: WebSocket, port=None) -> None: + """ + Dynamically register MCP server tools with the provided WebSocket. + This ensures all tools have access to the active WebSocket connection. + + Args: + websocket: The active WebSocket connection + port: Optional port number to target a specific MCP server + """ + # Get the appropriate server instance + server_instance = server_manager.get_server(port) + if server_instance is None: + logfire.error(f"No MCP server found on port {port or server_manager.default_port}") + return + + # First, unregister existing tools if they exist + tool_names = ["plan_task", "code_task", "web_surf_task", "ask_human", "planner_agent_update"] + for tool_name in tool_names: + if tool_name in server_instance._tool_manager._tools: + del server_instance._tool_manager._tools[tool_name] + + logfire.info("Registering MCP tools with WebSocket connection") + + async def plan_task(task: str) -> str: + """Plans the task and assigns it to the appropriate agents""" + try: + logfire.info(f"Planning task: {task}") + planner_stream_output = StreamResponse( + agent_name="Planner Agent", + instructions=task, + steps=[], + output="", + status_code=0, + message_id=str(uuid.uuid4()) + ) + + await _safe_websocket_send(websocket, planner_stream_output) + + # Update planner stream + planner_stream_output.steps.append("Planning task...") + await _safe_websocket_send(websocket, planner_stream_output) + + # Run planner agent + planner_response = await planner_agent.run(user_prompt=task) + + # Update planner stream with results + plan_text = planner_response.data.plan + planner_stream_output.steps.append("Task planned successfully") + planner_stream_output.output = plan_text + planner_stream_output.status_code = 200 + await _safe_websocket_send(websocket, planner_stream_output) + + return f"Task planned successfully\nTask: {plan_text}" + except Exception as e: + error_msg = f"Error planning task: {str(e)}" + logfire.error(error_msg, exc_info=True) + + # Update planner stream with error + if 'planner_stream_output' in locals(): + planner_stream_output.steps.append(f"Planning failed: {str(e)}") + planner_stream_output.status_code = 500 + await _safe_websocket_send(websocket, planner_stream_output) + + return f"Failed to plan task: {error_msg}" + + async def code_task(task: str) -> str: + """Assigns coding tasks to the coder agent""" + try: + logfire.info(f"Assigning coding task: {task}") + # Create a new StreamResponse for Coder Agent + coder_stream_output = StreamResponse( + agent_name="Coder Agent", + instructions=task, + steps=[], + output="", + status_code=0, + message_id=str(uuid.uuid4()) + ) + + await _safe_websocket_send(websocket, coder_stream_output) + + # Create deps with the new stream_output + deps_for_coder_agent = CoderAgentDeps( + websocket=websocket, + stream_output=coder_stream_output + ) + + # Run coder agent + coder_response = await coder_agent.run( + user_prompt=task, + deps=deps_for_coder_agent + ) + + # Extract response data + response_data = coder_response.data.content + + # Update coder_stream_output with coding results + coder_stream_output.output = response_data + coder_stream_output.status_code = 200 + coder_stream_output.steps.append("Coding task completed successfully") + await _safe_websocket_send(websocket, coder_stream_output) + + # Add a reminder in the result message to update the plan using planner_agent_update + response_with_reminder = f"{response_data}\n\nReminder: You must now call planner_agent_update with the completed task description: \"{task} (coder_agent)\"" + + return response_with_reminder + except Exception as e: + error_msg = f"Error assigning coding task: {str(e)}" + logfire.error(error_msg, exc_info=True) + + # Update coder_stream_output with error + if 'coder_stream_output' in locals(): + coder_stream_output.steps.append(f"Coding task failed: {str(e)}") + coder_stream_output.status_code = 500 + await _safe_websocket_send(websocket, coder_stream_output) + + return f"Failed to assign coding task: {error_msg}" + + async def web_surf_task(task: str) -> str: + """Assigns web surfing tasks to the web surfer agent""" + try: + logfire.info(f"Assigning web surfing task: {task}") + + # Create a new StreamResponse for WebSurfer + web_surfer_stream_output = StreamResponse( + agent_name="Web Surfer", + instructions=task, + steps=[], + output="", + status_code=0, + live_url=None, + message_id=str(uuid.uuid4()) + ) -# Note: All tools are now dynamically registered in instructor.py -# This avoids the problem of websocket not being available when tools are defined + await _safe_websocket_send(websocket, web_surfer_stream_output) + + # Initialize WebSurfer agent + web_surfer_agent = WebSurfer(api_url="http://localhost:8000/api/v1/web/stream") + + # Run WebSurfer with its own stream_output + success, message, messages = await web_surfer_agent.generate_reply( + instruction=task, + websocket=websocket, + stream_output=web_surfer_stream_output + ) + + # Update WebSurfer's stream_output with final result + if success: + web_surfer_stream_output.steps.append("Web search completed successfully") + web_surfer_stream_output.output = message + web_surfer_stream_output.status_code = 200 -def run_server(): - """Run the MCP server""" - server.run(transport="sse") + # Add a reminder to update the plan + message_with_reminder = f"{message}\n\nReminder: You must now call planner_agent_update with the completed task description: \"{task} (web_surfer_agent)\"" + else: + web_surfer_stream_output.steps.append(f"Web search completed with issues: {message[:100]}") + web_surfer_stream_output.status_code = 500 + message_with_reminder = message -if __name__ == "__main__": - run_server() \ No newline at end of file + await _safe_websocket_send(websocket, web_surfer_stream_output) + + web_surfer_stream_output.steps.append(f"WebSurfer completed: {'Success' if success else 'Failed'}") + await _safe_websocket_send(websocket, web_surfer_stream_output) + + return message_with_reminder + except Exception as e: + error_msg = f"Error assigning web surfing task: {str(e)}" + logfire.error(error_msg, exc_info=True) + + # Update WebSurfer's stream_output with error + if 'web_surfer_stream_output' in locals(): + web_surfer_stream_output.steps.append(f"Web search failed: {str(e)}") + web_surfer_stream_output.status_code = 500 + await _safe_websocket_send(websocket, web_surfer_stream_output) + return f"Failed to assign web surfing task: {error_msg}" + + async def planner_agent_update(completed_task: str) -> str: + """ + Updates the todo.md file to mark a task as completed and returns the full updated plan. + """ + try: + logfire.info(f"Updating plan with completed task: {completed_task}") + # Create a new StreamResponse for Planner Agent update + planner_stream_output = StreamResponse( + agent_name="Planner Agent", + instructions=f"Update todo.md to mark as completed: {completed_task}", + steps=[], + output="", + status_code=0, + message_id=str(uuid.uuid4()) + ) + + # Send initial update + await _safe_websocket_send(websocket, planner_stream_output) + + # Directly read and update the todo.md file + base_dir = os.path.abspath(os.path.dirname(os.path.dirname(__file__))) + planner_dir = os.path.join(base_dir, "agents", "planner") + todo_path = os.path.join(planner_dir, "todo.md") + + planner_stream_output.steps.append("Reading current todo.md...") + await _safe_websocket_send(websocket, planner_stream_output) + + # Make sure the directory exists + os.makedirs(planner_dir, exist_ok=True) + + try: + # Check if todo.md exists + if not os.path.exists(todo_path): + planner_stream_output.steps.append("No todo.md file found. Will create new one after task completion.") + await _safe_websocket_send(websocket, planner_stream_output) + + # We'll directly call planner_agent.run() to create a new plan first + plan_prompt = f"Create a simple task plan based on this completed task: {completed_task}" + plan_response = await planner_agent.run(user_prompt=plan_prompt) + current_content = plan_response.data.plan + else: + # Read existing todo.md + with open(todo_path, "r") as file: + current_content = file.read() + planner_stream_output.steps.append(f"Found existing todo.md ({len(current_content)} bytes)") + await _safe_websocket_send(websocket, planner_stream_output) + + # Now call planner_agent.run() with specific instructions to update the plan + update_prompt = f""" + Here is the current todo.md content: + + {current_content} + + Please update this plan to mark the following task as completed: {completed_task} + Return ONLY the fully updated plan with appropriate tasks marked as [x] instead of [ ]. + """ + + planner_stream_output.steps.append("Asking planner to update the plan...") + await _safe_websocket_send(websocket, planner_stream_output) + + updated_plan_response = await planner_agent.run(user_prompt=update_prompt) + updated_plan = updated_plan_response.data.plan + + # Write the updated plan back to todo.md + with open(todo_path, "w") as file: + file.write(updated_plan) + + planner_stream_output.steps.append("Plan updated successfully") + planner_stream_output.output = updated_plan + planner_stream_output.status_code = 200 + await _safe_websocket_send(websocket, planner_stream_output) + + return updated_plan + + except Exception as e: + error_msg = f"Error during plan update operations: {str(e)}" + logfire.error(error_msg, exc_info=True) + + planner_stream_output.steps.append(f"Plan update failed: {str(e)}") + planner_stream_output.status_code = 500 + await _safe_websocket_send(websocket, planner_stream_output) + + return f"Failed to update the plan: {error_msg}" + + except Exception as e: + error_msg = f"Error updating plan: {str(e)}" + logfire.error(error_msg, exc_info=True) + + return f"Failed to update plan: {error_msg}" + + # Helper function for websocket communication + async def _safe_websocket_send(socket: WebSocket, message: Any) -> bool: + """Safely send message through websocket with error handling""" + try: + if socket and socket.client_state.CONNECTED: + await socket.send_text(json.dumps(asdict(message))) + logfire.debug("WebSocket message sent (_safe_websocket_send): {message}", message=message) + return True + return False + except Exception as e: + logfire.error(f"WebSocket send failed: {str(e)}") + return False + + # Now register all the generated tools with the MCP server + tool_definitions = { + "plan_task": (plan_task, "Plans the task and assigns it to the appropriate agents"), + "code_task": (code_task, "Assigns coding tasks to the coder agent"), + "web_surf_task": (web_surf_task, "Assigns web surfing tasks to the web surfer agent"), + "planner_agent_update": (planner_agent_update, "Updates the todo.md file to mark a task as completed") + } + + # Register each tool with the specified server instance + for name, (fn, desc) in tool_definitions.items(): + server_instance._tool_manager.add_tool(fn, name=name, description=desc) + + logfire.info(f"Successfully registered {len(tool_definitions)} tools with the MCP server on port {port or server_manager.default_port}") \ No newline at end of file diff --git a/cortex_on/agents/orchestrator_agent.py b/cortex_on/agents/orchestrator_agent.py index f61f38c..e10978b 100644 --- a/cortex_on/agents/orchestrator_agent.py +++ b/cortex_on/agents/orchestrator_agent.py @@ -1,22 +1,21 @@ +#Standard library imports import os import json -import traceback from typing import List, Optional, Dict, Any, Union, Tuple -from datetime import datetime import uuid -from pydantic import BaseModel from dataclasses import asdict, dataclass + +#Third party imports import logfire from fastapi import WebSocket from dotenv import load_dotenv from pydantic_ai.models.anthropic import AnthropicModel from pydantic_ai.providers.anthropic import AnthropicProvider from pydantic_ai import Agent, RunContext -from pydantic_ai.mcp import MCPServerHTTP, MCPServerStdio +from pydantic_ai.mcp import MCPServerHTTP + +#Local imports from utils.stream_response_format import StreamResponse -from agents.planner_agent import planner_agent, update_todo_status -from agents.code_agent import coder_agent, CoderAgentDeps -from utils.ant_client import get_client load_dotenv() @dataclass @@ -163,7 +162,6 @@ class orchestrator_deps: """ # Initialize MCP Server -# server = MCPServerStdio('python', ["-m", "agents.mcp_server"]) #we can add multiple servers here #example: # server1 = MCPServerHTTP(url='http://localhost:8004/sse') @@ -186,6 +184,7 @@ class orchestrator_deps: mcp_servers=[server], ) +# Human Input Tool attached to the orchestrator agent as a tool @orchestrator_agent.tool async def ask_human(ctx: RunContext[orchestrator_deps], question: str) -> str: """Sends a question to the frontend and waits for human input""" @@ -244,336 +243,4 @@ async def _safe_websocket_send(socket: WebSocket, message: Any) -> bool: return False except Exception as e: logfire.error(f"WebSocket send failed: {str(e)}") - return False -# @orchestrator_agent.tool -# async def plan_task(ctx: RunContext[orchestrator_deps], task: str) -> str: -# """Plans the task and assigns it to the appropriate agents""" -# try: -# logfire.info(f"Planning task: {task}") - -# # Create a new StreamResponse for Planner Agent -# planner_stream_output = StreamResponse( -# agent_name="Planner Agent", -# instructions=task, -# steps=[], -# output="", -# status_code=0 -# ) - -# # Add to orchestrator's response collection if available -# if ctx.deps.agent_responses is not None: -# ctx.deps.agent_responses.append(planner_stream_output) - -# await _safe_websocket_send(ctx.deps.websocket, planner_stream_output) - -# # Update planner stream -# planner_stream_output.steps.append("Planning task...") -# await _safe_websocket_send(ctx.deps.websocket, planner_stream_output) - -# # Run planner agent -# planner_response = await planner_agent.run(user_prompt=task) - -# # Update planner stream with results -# plan_text = planner_response.data.plan -# planner_stream_output.steps.append("Task planned successfully") -# planner_stream_output.output = plan_text -# planner_stream_output.status_code = 200 -# await _safe_websocket_send(ctx.deps.websocket, planner_stream_output) - -# # Also update orchestrator stream -# ctx.deps.stream_output.steps.append("Task planned successfully") -# await _safe_websocket_send(ctx.deps.websocket, ctx.deps.stream_output) - -# return f"Task planned successfully\nTask: {plan_text}" -# except Exception as e: -# error_msg = f"Error planning task: {str(e)}" -# logfire.error(error_msg, exc_info=True) - -# # Update planner stream with error -# if planner_stream_output: -# planner_stream_output.steps.append(f"Planning failed: {str(e)}") -# planner_stream_output.status_code = 500 -# await _safe_websocket_send(ctx.deps.websocket, planner_stream_output) - -# # Also update orchestrator stream -# if ctx.deps.stream_output: -# ctx.deps.stream_output.steps.append(f"Planning failed: {str(e)}") -# await _safe_websocket_send(ctx.deps.websocket, ctx.deps.stream_output) - -# return f"Failed to plan task: {error_msg}" - -# @orchestrator_agent.tool -# async def coder_task(ctx: RunContext[orchestrator_deps], task: str) -> str: -# """Assigns coding tasks to the coder agent""" -# try: -# logfire.info(f"Assigning coding task: {task}") - -# # Create a new StreamResponse for Coder Agent -# coder_stream_output = StreamResponse( -# agent_name="Coder Agent", -# instructions=task, -# steps=[], -# output="", -# status_code=0 -# ) - -# # Add to orchestrator's response collection if available -# if ctx.deps.agent_responses is not None: -# ctx.deps.agent_responses.append(coder_stream_output) - -# # Send initial update for Coder Agent -# await _safe_websocket_send(ctx.deps.websocket, coder_stream_output) - -# # Create deps with the new stream_output -# deps_for_coder_agent = CoderAgentDeps( -# websocket=ctx.deps.websocket, -# stream_output=coder_stream_output -# ) - -# # Run coder agent -# coder_response = await coder_agent.run( -# user_prompt=task, -# deps=deps_for_coder_agent -# ) - -# # Extract response data -# response_data = coder_response.data.content - -# # Update coder_stream_output with coding results -# coder_stream_output.output = response_data -# coder_stream_output.status_code = 200 -# coder_stream_output.steps.append("Coding task completed successfully") -# await _safe_websocket_send(ctx.deps.websocket, coder_stream_output) - -# # Add a reminder in the result message to update the plan using planner_agent_update -# response_with_reminder = f"{response_data}\n\nReminder: You must now call planner_agent_update with the completed task description: \"{task} (coder_agent)\"" - -# return response_with_reminder -# except Exception as e: -# error_msg = f"Error assigning coding task: {str(e)}" -# logfire.error(error_msg, exc_info=True) - -# # Update coder_stream_output with error -# coder_stream_output.steps.append(f"Coding task failed: {str(e)}") -# coder_stream_output.status_code = 500 -# await _safe_websocket_send(ctx.deps.websocket, coder_stream_output) - -# return f"Failed to assign coding task: {error_msg}" - -# @orchestrator_agent.tool -# async def web_surfer_task(ctx: RunContext[orchestrator_deps], task: str) -> str: -# """Assigns web surfing tasks to the web surfer agent""" -# try: -# logfire.info(f"Assigning web surfing task: {task}") - -# # Create a new StreamResponse for WebSurfer -# web_surfer_stream_output = StreamResponse( -# agent_name="Web Surfer", -# instructions=task, -# steps=[], -# output="", -# status_code=0, -# live_url=None -# ) - -# # Add to orchestrator's response collection if available -# if ctx.deps.agent_responses is not None: -# ctx.deps.agent_responses.append(web_surfer_stream_output) - -# await _safe_websocket_send(ctx.deps.websocket, web_surfer_stream_output) - -# # Initialize WebSurfer agent -# web_surfer_agent = WebSurfer(api_url="http://localhost:8000/api/v1/web/stream") - -# # Run WebSurfer with its own stream_output -# success, message, messages = await web_surfer_agent.generate_reply( -# instruction=task, -# websocket=ctx.deps.websocket, -# stream_output=web_surfer_stream_output -# ) - -# # Update WebSurfer's stream_output with final result -# if success: -# web_surfer_stream_output.steps.append("Web search completed successfully") -# web_surfer_stream_output.output = message -# web_surfer_stream_output.status_code = 200 - -# # Add a reminder to update the plan -# message_with_reminder = f"{message}\n\nReminder: You must now call planner_agent_update with the completed task description: \"{task} (web_surfer_agent)\"" -# else: -# web_surfer_stream_output.steps.append(f"Web search completed with issues: {message[:100]}") -# web_surfer_stream_output.status_code = 500 -# message_with_reminder = message - -# await _safe_websocket_send(ctx.deps.websocket, web_surfer_stream_output) - -# web_surfer_stream_output.steps.append(f"WebSurfer completed: {'Success' if success else 'Failed'}") -# await _safe_websocket_send(ctx.deps.websocket, web_surfer_stream_output) - -# return message_with_reminder -# except Exception as e: -# error_msg = f"Error assigning web surfing task: {str(e)}" -# logfire.error(error_msg, exc_info=True) - -# # Update WebSurfer's stream_output with error -# web_surfer_stream_output.steps.append(f"Web search failed: {str(e)}") -# web_surfer_stream_output.status_code = 500 -# await _safe_websocket_send(ctx.deps.websocket, web_surfer_stream_output) -# return f"Failed to assign web surfing task: {error_msg}" - -# @orchestrator_agent.tool -# async def ask_human(ctx: RunContext[orchestrator_deps], question: str) -> str: -# """Sends a question to the frontend and waits for human input""" -# try: -# logfire.info(f"Asking human: {question}") - -# # Create a new StreamResponse for Human Input -# human_stream_output = StreamResponse( -# agent_name="Human Input", -# instructions=question, -# steps=[], -# output="", -# status_code=0 -# ) - -# # Add to orchestrator's response collection if available -# if ctx.deps.agent_responses is not None: -# ctx.deps.agent_responses.append(human_stream_output) - -# # Send the question to frontend -# await _safe_websocket_send(ctx.deps.websocket, human_stream_output) - -# # Update stream with waiting message -# human_stream_output.steps.append("Waiting for human input...") -# await _safe_websocket_send(ctx.deps.websocket, human_stream_output) - -# # Wait for response from frontend -# response = await ctx.deps.websocket.receive_text() - -# # Update stream with response -# human_stream_output.steps.append("Received human input") -# human_stream_output.output = response -# human_stream_output.status_code = 200 -# await _safe_websocket_send(ctx.deps.websocket, human_stream_output) - -# return response -# except Exception as e: -# error_msg = f"Error getting human input: {str(e)}" -# logfire.error(error_msg, exc_info=True) - -# # Update stream with error -# human_stream_output.steps.append(f"Failed to get human input: {str(e)}") -# human_stream_output.status_code = 500 -# await _safe_websocket_send(ctx.deps.websocket, human_stream_output) - -# return f"Failed to get human input: {error_msg}" - -# @orchestrator_agent.tool -# async def planner_agent_update(ctx: RunContext[orchestrator_deps], completed_task: str) -> str: -# """ -# Updates the todo.md file to mark a task as completed and returns the full updated plan. - -# Args: -# completed_task: Description of the completed task including which agent performed it - -# Returns: -# The complete updated todo.md content with tasks marked as completed -# """ -# try: -# logfire.info(f"Updating plan with completed task: {completed_task}") - -# # Create a new StreamResponse for Planner Agent update -# planner_stream_output = StreamResponse( -# agent_name="Planner Agent", -# instructions=f"Update todo.md to mark as completed: {completed_task}", -# steps=[], -# output="", -# status_code=0 -# ) - -# # Send initial update -# await _safe_websocket_send(ctx.deps.websocket, planner_stream_output) - -# # Directly read and update the todo.md file -# base_dir = os.path.abspath(os.path.dirname(os.path.dirname(__file__))) -# planner_dir = os.path.join(base_dir, "agents", "planner") -# todo_path = os.path.join(planner_dir, "todo.md") - -# planner_stream_output.steps.append("Reading current todo.md...") -# await _safe_websocket_send(ctx.deps.websocket, planner_stream_output) - -# # Make sure the directory exists -# os.makedirs(planner_dir, exist_ok=True) - -# try: -# # Check if todo.md exists -# if not os.path.exists(todo_path): -# planner_stream_output.steps.append("No todo.md file found. Will create new one after task completion.") -# await _safe_websocket_send(ctx.deps.websocket, planner_stream_output) - -# # We'll directly call planner_agent.run() to create a new plan first -# plan_prompt = f"Create a simple task plan based on this completed task: {completed_task}" -# plan_response = await planner_agent.run(user_prompt=plan_prompt) -# current_content = plan_response.data.plan -# else: -# # Read existing todo.md -# with open(todo_path, "r") as file: -# current_content = file.read() -# planner_stream_output.steps.append(f"Found existing todo.md ({len(current_content)} bytes)") -# await _safe_websocket_send(ctx.deps.websocket, planner_stream_output) - -# # Now call planner_agent.run() with specific instructions to update the plan -# update_prompt = f""" -# Here is the current todo.md content: - -# {current_content} - -# Please update this plan to mark the following task as completed: {completed_task} -# Return ONLY the fully updated plan with appropriate tasks marked as [x] instead of [ ]. -# """ - -# planner_stream_output.steps.append("Asking planner to update the plan...") -# await _safe_websocket_send(ctx.deps.websocket, planner_stream_output) - -# updated_plan_response = await planner_agent.run(user_prompt=update_prompt) -# updated_plan = updated_plan_response.data.plan - -# # Write the updated plan back to todo.md -# with open(todo_path, "w") as file: -# file.write(updated_plan) - -# planner_stream_output.steps.append("Plan updated successfully") -# planner_stream_output.output = updated_plan -# planner_stream_output.status_code = 200 -# await _safe_websocket_send(ctx.deps.websocket, planner_stream_output) - -# # Update orchestrator stream -# if ctx.deps.stream_output: -# ctx.deps.stream_output.steps.append(f"Plan updated to mark task as completed: {completed_task}") -# await _safe_websocket_send(ctx.deps.websocket, ctx.deps.stream_output) - -# return updated_plan - -# except Exception as e: -# error_msg = f"Error during plan update operations: {str(e)}" -# logfire.error(error_msg, exc_info=True) - -# planner_stream_output.steps.append(f"Plan update failed: {str(e)}") -# planner_stream_output.status_code = 500 -# await _safe_websocket_send(ctx.deps.websocket, planner_stream_output) - -# return f"Failed to update the plan: {error_msg}" - -# except Exception as e: -# error_msg = f"Error updating plan: {str(e)}" -# logfire.error(error_msg, exc_info=True) - -# # Update stream output with error -# if ctx.deps.stream_output: -# ctx.deps.stream_output.steps.append(f"Failed to update plan: {str(e)}") -# await _safe_websocket_send(ctx.deps.websocket, ctx.deps.stream_output) - -# return f"Failed to update plan: {error_msg}" - -# # Helper function for sending WebSocket messages - + return False \ No newline at end of file diff --git a/cortex_on/instructor.py b/cortex_on/instructor.py index 489a2b1..74bdb58 100644 --- a/cortex_on/instructor.py +++ b/cortex_on/instructor.py @@ -1,13 +1,11 @@ # Standard library imports import json import os -import asyncio import traceback from dataclasses import asdict from datetime import datetime from typing import Any, Dict, List, Optional, Tuple, Union import uuid -import threading # Third-party imports from dotenv import load_dotenv @@ -20,78 +18,11 @@ from mcp.server.fastmcp import FastMCP # Local application imports -from agents.code_agent import CoderAgentDeps, coder_agent from agents.orchestrator_agent import orchestrator_agent, orchestrator_deps -from agents.planner_agent import planner_agent -from agents.web_surfer import WebSurfer -from utils.ant_client import get_client from utils.stream_response_format import StreamResponse -from agents.mcp_server import server +from agents.mcp_server import start_mcp_server, register_tools_for_main_mcp_server, server_manager load_dotenv() -# Server manager to handle multiple MCP servers -class ServerManager: - def __init__(self): - self.servers = {} # Dictionary to track running servers by port - self.default_port = 8002 # default port for main MCP server with agents as a tool - - def start_server(self, port=None, name=None): - """Start an MCP server on the specified port""" - if port is None: - port = self.default_port - - if name is None: - name = f"mcp_server_{port}" - - # Check if server is already running on this port - if port in self.servers and self.servers[port]['running']: - logfire.info(f"MCP server already running on port {port}") - return - - # Configure server for this port - server_instance = FastMCP(name=name, host="0.0.0.0", port=port) - - # Track server in our registry - self.servers[port] = { - 'running': True, - 'name': name, - 'instance': server_instance, - 'thread': None - } - - def run_server(): - logfire.info(f"Starting MCP server '{name}' on port {port}...") - # Configure the server to use the specified port - server_instance.run(transport="sse") - - # Start in a separate thread - thread = threading.Thread(target=run_server, daemon=True) - thread.start() - self.servers[port]['thread'] = thread - logfire.info(f"MCP server thread started for '{name}' on port {port}") - - def get_server(self, port=None): - """Get the server instance for the specified port""" - if port is None: - port = self.default_port - - if port in self.servers: - return self.servers[port]['instance'] - return None - -# Initialize the server manager -server_manager = ServerManager() - -def start_mcp_server(port=None, name=None): - """Start an MCP server on the specified port""" - server_manager.start_server(port=port, name=name) - #we can add multiple servers here - -# For backwards compatibility -def start_mcp_server_in_thread(): - """Start the MCP server in a separate thread (legacy function)""" - start_mcp_server() - class DateTimeEncoder(json.JSONEncoder): """Custom JSON encoder that can handle datetime objects and Pydantic models""" def default(self, obj): @@ -107,304 +38,6 @@ def default(self, obj): return {k: v for k, v in obj.__dict__.items() if not k.startswith('_')} return super().default(obj) - -def register_tools_for_main_mcp_server(websocket: WebSocket, port=None) -> None: - """ - Dynamically register MCP server tools with the provided WebSocket. - This ensures all tools have access to the active WebSocket connection. - - Args: - websocket: The active WebSocket connection - port: Optional port number to target a specific MCP server - """ - # Get the appropriate server instance - server_instance = server_manager.get_server(port) - if server_instance is None: - logfire.error(f"No MCP server found on port {port or server_manager.default_port}") - return - - # First, unregister existing tools if they exist - tool_names = ["plan_task", "code_task", "web_surf_task", "ask_human", "planner_agent_update"] - for tool_name in tool_names: - if tool_name in server_instance._tool_manager._tools: - del server_instance._tool_manager._tools[tool_name] - - logfire.info("Registering MCP tools with WebSocket connection") - - async def plan_task(task: str) -> str: - """Plans the task and assigns it to the appropriate agents""" - try: - logfire.info(f"Planning task: {task}") - print(f"Planning task: {task}") - planner_stream_output = StreamResponse( - agent_name="Planner Agent", - instructions=task, - steps=[], - output="", - status_code=0, - message_id=str(uuid.uuid4()) - ) - - await _safe_websocket_send(websocket, planner_stream_output) - - # Update planner stream - planner_stream_output.steps.append("Planning task...") - await _safe_websocket_send(websocket, planner_stream_output) - - # Run planner agent - planner_response = await planner_agent.run(user_prompt=task) - - # Update planner stream with results - plan_text = planner_response.data.plan - planner_stream_output.steps.append("Task planned successfully") - planner_stream_output.output = plan_text - planner_stream_output.status_code = 200 - await _safe_websocket_send(websocket, planner_stream_output) - - return f"Task planned successfully\nTask: {plan_text}" - except Exception as e: - error_msg = f"Error planning task: {str(e)}" - logfire.error(error_msg, exc_info=True) - - # Update planner stream with error - if 'planner_stream_output' in locals(): - planner_stream_output.steps.append(f"Planning failed: {str(e)}") - planner_stream_output.status_code = 500 - await _safe_websocket_send(websocket, planner_stream_output) - - return f"Failed to plan task: {error_msg}" - - async def code_task(task: str) -> str: - """Assigns coding tasks to the coder agent""" - try: - logfire.info(f"Assigning coding task: {task}") - print(f"Assigning coding task: {task}") - # Create a new StreamResponse for Coder Agent - coder_stream_output = StreamResponse( - agent_name="Coder Agent", - instructions=task, - steps=[], - output="", - status_code=0, - message_id=str(uuid.uuid4()) - ) - - await _safe_websocket_send(websocket, coder_stream_output) - - # Create deps with the new stream_output - deps_for_coder_agent = CoderAgentDeps( - websocket=websocket, - stream_output=coder_stream_output - ) - - # Run coder agent - coder_response = await coder_agent.run( - user_prompt=task, - deps=deps_for_coder_agent - ) - - # Extract response data - response_data = coder_response.data.content - - # Update coder_stream_output with coding results - coder_stream_output.output = response_data - coder_stream_output.status_code = 200 - coder_stream_output.steps.append("Coding task completed successfully") - await _safe_websocket_send(websocket, coder_stream_output) - - # Add a reminder in the result message to update the plan using planner_agent_update - response_with_reminder = f"{response_data}\n\nReminder: You must now call planner_agent_update with the completed task description: \"{task} (coder_agent)\"" - - return response_with_reminder - except Exception as e: - error_msg = f"Error assigning coding task: {str(e)}" - logfire.error(error_msg, exc_info=True) - - # Update coder_stream_output with error - if 'coder_stream_output' in locals(): - coder_stream_output.steps.append(f"Coding task failed: {str(e)}") - coder_stream_output.status_code = 500 - await _safe_websocket_send(websocket, coder_stream_output) - - return f"Failed to assign coding task: {error_msg}" - - async def web_surf_task(task: str) -> str: - """Assigns web surfing tasks to the web surfer agent""" - try: - logfire.info(f"Assigning web surfing task: {task}") - - # Create a new StreamResponse for WebSurfer - web_surfer_stream_output = StreamResponse( - agent_name="Web Surfer", - instructions=task, - steps=[], - output="", - status_code=0, - live_url=None, - message_id=str(uuid.uuid4()) - ) - - await _safe_websocket_send(websocket, web_surfer_stream_output) - - # Initialize WebSurfer agent - web_surfer_agent = WebSurfer(api_url="http://localhost:8000/api/v1/web/stream") - - # Run WebSurfer with its own stream_output - success, message, messages = await web_surfer_agent.generate_reply( - instruction=task, - websocket=websocket, - stream_output=web_surfer_stream_output - ) - - # Update WebSurfer's stream_output with final result - if success: - web_surfer_stream_output.steps.append("Web search completed successfully") - web_surfer_stream_output.output = message - web_surfer_stream_output.status_code = 200 - - # Add a reminder to update the plan - message_with_reminder = f"{message}\n\nReminder: You must now call planner_agent_update with the completed task description: \"{task} (web_surfer_agent)\"" - else: - web_surfer_stream_output.steps.append(f"Web search completed with issues: {message[:100]}") - web_surfer_stream_output.status_code = 500 - message_with_reminder = message - - await _safe_websocket_send(websocket, web_surfer_stream_output) - - web_surfer_stream_output.steps.append(f"WebSurfer completed: {'Success' if success else 'Failed'}") - await _safe_websocket_send(websocket, web_surfer_stream_output) - - return message_with_reminder - except Exception as e: - error_msg = f"Error assigning web surfing task: {str(e)}" - logfire.error(error_msg, exc_info=True) - - # Update WebSurfer's stream_output with error - if 'web_surfer_stream_output' in locals(): - web_surfer_stream_output.steps.append(f"Web search failed: {str(e)}") - web_surfer_stream_output.status_code = 500 - await _safe_websocket_send(websocket, web_surfer_stream_output) - return f"Failed to assign web surfing task: {error_msg}" - - async def planner_agent_update(completed_task: str) -> str: - """ - Updates the todo.md file to mark a task as completed and returns the full updated plan. - """ - try: - logfire.info(f"Updating plan with completed task: {completed_task}") - print(f"Updating plan with completed task: {completed_task}") - # Create a new StreamResponse for Planner Agent update - planner_stream_output = StreamResponse( - agent_name="Planner Agent", - instructions=f"Update todo.md to mark as completed: {completed_task}", - steps=[], - output="", - status_code=0, - message_id=str(uuid.uuid4()) - ) - - # Send initial update - await _safe_websocket_send(websocket, planner_stream_output) - - # Directly read and update the todo.md file - base_dir = os.path.abspath(os.path.dirname(os.path.dirname(__file__))) - planner_dir = os.path.join(base_dir, "agents", "planner") - todo_path = os.path.join(planner_dir, "todo.md") - - planner_stream_output.steps.append("Reading current todo.md...") - await _safe_websocket_send(websocket, planner_stream_output) - - # Make sure the directory exists - os.makedirs(planner_dir, exist_ok=True) - - try: - # Check if todo.md exists - if not os.path.exists(todo_path): - planner_stream_output.steps.append("No todo.md file found. Will create new one after task completion.") - await _safe_websocket_send(websocket, planner_stream_output) - - # We'll directly call planner_agent.run() to create a new plan first - plan_prompt = f"Create a simple task plan based on this completed task: {completed_task}" - plan_response = await planner_agent.run(user_prompt=plan_prompt) - current_content = plan_response.data.plan - else: - # Read existing todo.md - with open(todo_path, "r") as file: - current_content = file.read() - planner_stream_output.steps.append(f"Found existing todo.md ({len(current_content)} bytes)") - await _safe_websocket_send(websocket, planner_stream_output) - - # Now call planner_agent.run() with specific instructions to update the plan - update_prompt = f""" - Here is the current todo.md content: - - {current_content} - - Please update this plan to mark the following task as completed: {completed_task} - Return ONLY the fully updated plan with appropriate tasks marked as [x] instead of [ ]. - """ - - planner_stream_output.steps.append("Asking planner to update the plan...") - await _safe_websocket_send(websocket, planner_stream_output) - - updated_plan_response = await planner_agent.run(user_prompt=update_prompt) - updated_plan = updated_plan_response.data.plan - - # Write the updated plan back to todo.md - with open(todo_path, "w") as file: - file.write(updated_plan) - - planner_stream_output.steps.append("Plan updated successfully") - planner_stream_output.output = updated_plan - planner_stream_output.status_code = 200 - await _safe_websocket_send(websocket, planner_stream_output) - - return updated_plan - - except Exception as e: - error_msg = f"Error during plan update operations: {str(e)}" - logfire.error(error_msg, exc_info=True) - - planner_stream_output.steps.append(f"Plan update failed: {str(e)}") - planner_stream_output.status_code = 500 - await _safe_websocket_send(websocket, planner_stream_output) - - return f"Failed to update the plan: {error_msg}" - - except Exception as e: - error_msg = f"Error updating plan: {str(e)}" - logfire.error(error_msg, exc_info=True) - - return f"Failed to update plan: {error_msg}" - - # Helper function for websocket communication - async def _safe_websocket_send(socket: WebSocket, message: Any) -> bool: - """Safely send message through websocket with error handling""" - try: - if socket and socket.client_state.CONNECTED: - await socket.send_text(json.dumps(asdict(message))) - logfire.debug("WebSocket message sent (_safe_websocket_send): {message}", message=message) - return True - return False - except Exception as e: - logfire.error(f"WebSocket send failed: {str(e)}") - return False - - # Now register all the generated tools with the MCP server - tool_definitions = { - "plan_task": (plan_task, "Plans the task and assigns it to the appropriate agents"), - "code_task": (code_task, "Assigns coding tasks to the coder agent"), - "web_surf_task": (web_surf_task, "Assigns web surfing tasks to the web surfer agent"), - "planner_agent_update": (planner_agent_update, "Updates the todo.md file to mark a task as completed") - } - - # Register each tool with the specified server instance - for name, (fn, desc) in tool_definitions.items(): - server_instance._tool_manager.add_tool(fn, name=name, description=desc) - - logfire.info(f"Successfully registered {len(tool_definitions)} tools with the MCP server on port {port or server_manager.default_port}") - - # Main Orchestrator Class class SystemInstructor: def __init__(self): From 6ac5bcc380bcb210631c856139ac4ca8c4b58fa5 Mon Sep 17 00:00:00 2001 From: Sakalya100 Date: Tue, 29 Apr 2025 20:25:42 +0530 Subject: [PATCH 19/35] feat: added external mcp server support - Added support for GitHub and Server Time mcp servers - Added dynamic prompts and server initialisation in runtime --- cortex_on/Dockerfile | 6 ++ cortex_on/agents/orchestrator_agent.py | 2 +- cortex_on/config/external_mcp_servers.json | 22 ++++ cortex_on/connect_to_external_server.py | 112 +++++++++++++++++++++ cortex_on/instructor.py | 35 +++++-- cortex_on/requirements.txt | 3 +- 6 files changed, 167 insertions(+), 13 deletions(-) create mode 100644 cortex_on/config/external_mcp_servers.json create mode 100644 cortex_on/connect_to_external_server.py diff --git a/cortex_on/Dockerfile b/cortex_on/Dockerfile index 955808b..9cbcc4a 100644 --- a/cortex_on/Dockerfile +++ b/cortex_on/Dockerfile @@ -8,6 +8,12 @@ RUN apt-get update && apt-get install -y \ build-essential \ cmake \ g++ \ + curl \ + && rm -rf /var/lib/apt/lists/* + +# Install Node.js and npm +RUN curl -fsSL https://deb.nodesource.com/setup_18.x | bash - \ + && apt-get install -y nodejs \ && rm -rf /var/lib/apt/lists/* RUN export PYTHONPATH=/app diff --git a/cortex_on/agents/orchestrator_agent.py b/cortex_on/agents/orchestrator_agent.py index e10978b..21cb3e5 100644 --- a/cortex_on/agents/orchestrator_agent.py +++ b/cortex_on/agents/orchestrator_agent.py @@ -181,7 +181,7 @@ class orchestrator_deps: name="Orchestrator Agent", system_prompt=orchestrator_system_prompt, deps_type=orchestrator_deps, - mcp_servers=[server], + # mcp_servers=[server], ) # Human Input Tool attached to the orchestrator agent as a tool diff --git a/cortex_on/config/external_mcp_servers.json b/cortex_on/config/external_mcp_servers.json new file mode 100644 index 0000000..a20c0c1 --- /dev/null +++ b/cortex_on/config/external_mcp_servers.json @@ -0,0 +1,22 @@ +{ + "github": { + "command": "npx", + "args": [ + "-y", + "@modelcontextprotocol/server-github" + ], + "env": { + "GITHUB_PERSONAL_ACCESS_TOKEN": "" + }, + "description": "GitHub MCP server for repository operations" + }, + "timezone": { + "command": "python", + "args": [ + "-m", + "mcp_server_time" + ], + "env": {}, + "description": "MCP server for timezone can convert between timezones" + } +} \ No newline at end of file diff --git a/cortex_on/connect_to_external_server.py b/cortex_on/connect_to_external_server.py new file mode 100644 index 0000000..b3ba345 --- /dev/null +++ b/cortex_on/connect_to_external_server.py @@ -0,0 +1,112 @@ +import asyncio +import json +import os +from contextlib import AsyncExitStack +from typing import Any, Dict, List, Optional, Tuple, Union + +import nest_asyncio +from colorama import Fore, Style, init +from mcp import ClientSession, StdioServerParameters +from pydantic_ai.models.anthropic import AnthropicModel +from pydantic_ai.providers.anthropic import AnthropicProvider + +init(autoreset=True) # Initialize colorama with autoreset=True + +from dotenv import load_dotenv +from pydantic_ai.mcp import MCPServerStdio + +load_dotenv() + +class StdioServerProvider: + """Class for creating and managing MCPServerStdio instances from a JSON configuration file""" + + def __init__(self, config_path: str = 'config/external_mcp_servers.json'): + self.config_path = config_path + self.servers: Dict[str, MCPServerStdio] = {} + self.server_configs: Dict[str, Dict[str, Any]] = {} + self.server_tools: Dict[str, List[Dict[str, Any]]] = {} + + async def load_servers(self) -> Tuple[List[MCPServerStdio], str]: + """Load server configurations from JSON and create MCPServerStdio instances""" + # Check if config file exists in the specified path or try to find it + if not os.path.exists(self.config_path): + # Try to find it relative to the current file + alt_path = os.path.join(os.path.dirname(__file__), self.config_path) + if os.path.exists(alt_path): + self.config_path = alt_path + else: + # Try to find it in the parent directory + parent_dir = os.path.dirname(os.path.dirname(__file__)) + alt_path = os.path.join(parent_dir, self.config_path) + if os.path.exists(alt_path): + self.config_path = alt_path + else: + raise FileNotFoundError(f"Could not find config file: {self.config_path}") + + print(f"Loading stdio server configuration from: {self.config_path}") + + # Load the configuration file + with open(self.config_path, 'r') as f: + self.server_configs = json.load(f) + + # Create MCPServerStdio instances for each server + stdio_servers = [] + server_names = [] + + for server_name, config in self.server_configs.items(): + # Skip servers without a command + if 'command' not in config or not config['command']: + print(f"Skipping {server_name} - no command specified") + continue + + command = config['command'] + args = config.get('args', []) + env = config.get('env', {}) + + # Create the MCPServerStdio instance + try: + server = MCPServerStdio( + command, + args=args, + env=env + ) + + self.servers[server_name] = server + stdio_servers.append(server) + server_names.append(server_name) + + print(f"Created MCPServerStdio for {server_name} with command: {command} {' '.join(args)}") + except Exception as e: + print(f"Error creating MCPServerStdio for {server_name}: {str(e)}") + + # Generate a combined system prompt with server information + system_prompt = self._generate_system_prompt(server_names) + + return stdio_servers, system_prompt + + def _generate_system_prompt(self, server_names: List[str]) -> str: + """Generate a system prompt for the agent with information about available servers""" + if not server_names: + return "You are an AI assistant that can help with various tasks." + + servers_list = ", ".join([f"`{name}`" for name in server_names]) + + prompt = f"""You also have access to the following MCP servers: {servers_list}. + Each server provides specialized tools that you can use to complete tasks: + + """ + + # Add details about each server and its tools if available + for server_name in server_names: + config = self.server_configs.get(server_name, {}) + description = config.get('description', f"MCP server for {server_name}") + + prompt += f"- {server_name}: {description}\n" + + prompt += """ + When using these servers, reference them by their tools as available. + """ + + return prompt + +server_provider = StdioServerProvider() \ No newline at end of file diff --git a/cortex_on/instructor.py b/cortex_on/instructor.py index 74bdb58..8283b3d 100644 --- a/cortex_on/instructor.py +++ b/cortex_on/instructor.py @@ -2,6 +2,9 @@ import json import os import traceback +import yaml +import subprocess +import asyncio from dataclasses import asdict from datetime import datetime from typing import Any, Dict, List, Optional, Tuple, Union @@ -16,11 +19,14 @@ from pydantic_ai.messages import ModelMessage from pydantic_ai.models.anthropic import AnthropicModel from mcp.server.fastmcp import FastMCP +from pydantic_ai.mcp import MCPServerHTTP # Local application imports -from agents.orchestrator_agent import orchestrator_agent, orchestrator_deps +from agents.orchestrator_agent import orchestrator_agent, orchestrator_deps, orchestrator_system_prompt from utils.stream_response_format import StreamResponse from agents.mcp_server import start_mcp_server, register_tools_for_main_mcp_server, server_manager +from connect_to_external_server import server_provider +from prompts import time_server_prompt load_dotenv() class DateTimeEncoder(json.JSONEncoder): @@ -44,6 +50,7 @@ def __init__(self): self.websocket: Optional[WebSocket] = None self.stream_output: Optional[StreamResponse] = None self.orchestrator_response: List[StreamResponse] = [] + self.external_servers: Dict[str, Dict[str, Any]] = {} self._setup_logging() def _setup_logging(self) -> None: @@ -90,26 +97,33 @@ async def run(self, task: str, websocket: WebSocket, server_config: Optional[Dic deps_for_orchestrator = orchestrator_deps( websocket=self.websocket, stream_output=stream_output, - agent_responses=self.orchestrator_response # Pass reference to collection + agent_responses=self.orchestrator_response ) try: # Initialize system await self._safe_websocket_send(stream_output) - # Apply default server configuration if none provided + # Use the default port for main MCP server + main_port = server_manager.default_port # This is 8002 + + # Merge default and external server configurations if server_config is None: server_config = { - "main": server_manager.default_port + "main": main_port } - # Start each configured MCP server - for server_name, port in server_config.items(): - start_mcp_server(port=port, name=server_name) - # Register tools for this server - register_tools_for_main_mcp_server(websocket=self.websocket, port=port) + # Start the main MCP server - already handled by the framework + start_mcp_server(port=main_port, name="main") + register_tools_for_main_mcp_server(websocket=self.websocket, port=main_port) + + # Start each configured external MCP server + servers, system_prompt = await server_provider.load_servers() + orchestrator_agent._mcp_servers = servers + orchestrator_agent.system_prompt = orchestrator_system_prompt + "\n\n" + system_prompt + logfire.info(f"Updated orchestrator agent with {len(servers)} MCP servers. Current MCP servers: {orchestrator_agent._mcp_servers}") - # Configure orchestrator_agent to use the main MCP server port + # Configure orchestrator_agent to use all configured MCP servers async with orchestrator_agent.run_mcp_servers(): orchestrator_response = await orchestrator_agent.run( user_prompt=task, @@ -143,7 +157,6 @@ async def run(self, task: str, websocket: WebSocket, server_config: Optional[Dic finally: logfire.info("Orchestration process complete") - # Clear any sensitive data async def shutdown(self): """Clean shutdown of orchestrator""" diff --git a/cortex_on/requirements.txt b/cortex_on/requirements.txt index e656aee..5bc13a4 100644 --- a/cortex_on/requirements.txt +++ b/cortex_on/requirements.txt @@ -95,4 +95,5 @@ XlsxWriter==3.2.0 yarl==1.18.3 zipp==3.21.0 fast-graphrag==0.0.4 -llama_parse==0.5.19 \ No newline at end of file +llama_parse==0.5.19 +mcp-server-time \ No newline at end of file From df9f0fa0c013de9285b2691ff106067f2b327194 Mon Sep 17 00:00:00 2001 From: Sakalya100 Date: Fri, 2 May 2025 13:57:25 +0530 Subject: [PATCH 20/35] feat: added maps server --- cortex_on/config/external_mcp_servers.json | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/cortex_on/config/external_mcp_servers.json b/cortex_on/config/external_mcp_servers.json index a20c0c1..b21573d 100644 --- a/cortex_on/config/external_mcp_servers.json +++ b/cortex_on/config/external_mcp_servers.json @@ -10,13 +10,15 @@ }, "description": "GitHub MCP server for repository operations" }, - "timezone": { + "google_maps": { "command": "python", "args": [ "-m", - "mcp_server_time" + "mcp_server_maps" ], - "env": {}, - "description": "MCP server for timezone can convert between timezones" + "env": { + "GOOGLE_MAPS_API_KEY": "" + }, + "description": "MCP server for Google Maps operations like geocoding, directions, and place search" } } \ No newline at end of file From 8dfb698a8e22f2ac8ce14e20903f18ee246ee20c Mon Sep 17 00:00:00 2001 From: Sakalya100 Date: Fri, 2 May 2025 13:59:10 +0530 Subject: [PATCH 21/35] feat: updated mcp server config to include Google Maps server --- cortex_on/config/external_mcp_servers.json | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/cortex_on/config/external_mcp_servers.json b/cortex_on/config/external_mcp_servers.json index b21573d..c867f9a 100644 --- a/cortex_on/config/external_mcp_servers.json +++ b/cortex_on/config/external_mcp_servers.json @@ -10,15 +10,12 @@ }, "description": "GitHub MCP server for repository operations" }, - "google_maps": { - "command": "python", - "args": [ - "-m", - "mcp_server_maps" - ], - "env": { - "GOOGLE_MAPS_API_KEY": "" - }, - "description": "MCP server for Google Maps operations like geocoding, directions, and place search" - } + "google-maps": { + "command": "npx", + "args": ["-y", "@modelcontextprotocol/server-google-maps"], + "env": { + "GOOGLE_MAPS_API_KEY": "" + }, + "description": "Google Maps MCP server for geocoding, directions, and place search" + } } \ No newline at end of file From 1a3f2e7c17ac45cbc15765cc1b586a00f18e176c Mon Sep 17 00:00:00 2001 From: Sakalya100 Date: Sat, 3 May 2025 18:22:22 +0530 Subject: [PATCH 22/35] feat: Added dynamic prompt update + External Server streaming - Dynamic Prompts Update with respect to servers and tools associated in the config - Streaming of status update for external server to UI through websocket - Handling proper server reset and shutdown on New Chat to overcome duplicate tool name issue when re-attaching servers and tools in runtime --- cortex_on/agents/mcp_server.py | 50 +++- cortex_on/agents/orchestrator_agent.py | 89 +++++- cortex_on/connect_to_external_server.py | 342 +++++++++++++++++++++- cortex_on/instructor.py | 173 ++++++++++- cortex_on/utils/stream_response_format.py | 3 +- 5 files changed, 633 insertions(+), 24 deletions(-) diff --git a/cortex_on/agents/mcp_server.py b/cortex_on/agents/mcp_server.py index cec1cc9..37f8d82 100644 --- a/cortex_on/agents/mcp_server.py +++ b/cortex_on/agents/mcp_server.py @@ -372,4 +372,52 @@ async def _safe_websocket_send(socket: WebSocket, message: Any) -> bool: for name, (fn, desc) in tool_definitions.items(): server_instance._tool_manager.add_tool(fn, name=name, description=desc) - logfire.info(f"Successfully registered {len(tool_definitions)} tools with the MCP server on port {port or server_manager.default_port}") \ No newline at end of file + logfire.info(f"Successfully registered {len(tool_definitions)} tools with the MCP server on port {port or server_manager.default_port}") + + +def get_unique_tool_name(tool_name: str, registered_names: set) -> str: + """Ensure a tool name is unique by adding a suffix if necessary""" + if tool_name not in registered_names: + return tool_name + + # Add numeric suffix to make the name unique + base_name = tool_name + suffix = 1 + while f"{base_name}_{suffix}" in registered_names: + suffix += 1 + return f"{base_name}_{suffix}" + +def check_mcp_server_tools(server, registered_tools: set) -> None: + """Check and fix duplicate tool names in an MCP server""" + try: + # This relies on implementation details of MCP Server + if hasattr(server, '_mcp_api') and server._mcp_api: + api = server._mcp_api + + # Check if API has a tool manager + if hasattr(api, '_tool_manager'): + tool_manager = api._tool_manager + + # Check if the tool manager has tools + if hasattr(tool_manager, '_tools') and tool_manager._tools: + # Get a copy of original tool names + original_tools = list(tool_manager._tools.keys()) + for tool_name in original_tools: + # If this tool name conflicts with existing ones + if tool_name in registered_tools: + # Create a unique name + unique_name = get_unique_tool_name(tool_name, registered_tools) + # Get the tool + tool = tool_manager._tools[tool_name] + # Add it with the new name + tool_manager._tools[unique_name] = tool + # Remove the old one + del tool_manager._tools[tool_name] + # Add the new name to the registry + registered_tools.add(unique_name) + logfire.info(f"Renamed tool {tool_name} to {unique_name} to avoid duplicate") + else: + # Add the name to the registry + registered_tools.add(tool_name) + except Exception as e: + logfire.error(f"Error checking MCP server tools: {str(e)}") \ No newline at end of file diff --git a/cortex_on/agents/orchestrator_agent.py b/cortex_on/agents/orchestrator_agent.py index 21cb3e5..bd16d1b 100644 --- a/cortex_on/agents/orchestrator_agent.py +++ b/cortex_on/agents/orchestrator_agent.py @@ -37,6 +37,19 @@ class orchestrator_deps: - Implements technical solutions - Executes code operations +3. External MCP servers: + - Specialized servers for specific tasks like GitHub operations, Google Maps, etc. + - Each server provides its own set of tools that can be accessed with the server name prefix + - For example: github.search_repositories, google-maps.geocode + +[SERVER SELECTION GUIDELINES] +When deciding which service or agent to use: +1. For general code-related tasks: Use coder_agent +2. For general web browsing tasks: Use web_surfer_agent +3. For GitHub operations: Use github.* tools (search repos, manage issues, etc.) +4. For location and maps tasks: Use google-maps.* tools (geocoding, directions, places) +5. You can use multiple services in sequence for complex tasks + [AVAILABLE TOOLS] 1. plan_task(task: str) -> str: - Plans the given task and assigns it to appropriate agents @@ -78,6 +91,27 @@ class orchestrator_deps: - Returns the updated plan with completed tasks marked - Must be called after each agent completes a task +6. server_status_update(server_name: str, status_message: str, progress: float = 0, details: Dict[str, Any] = None) -> str: + - Sends live updates about external server access to the UI + - Use when accessing external APIs or MCP servers (like Google Maps, GitHub, etc.) + - Parameters: + * server_name: Name of the server (e.g., 'google_maps', 'github') + * status_message: Short, descriptive status message + * progress: Progress percentage (0-100) + * details: Optional detailed information + - Send frequent updates during lengthy operations + - Updates the UI in real-time with server interaction progress + - Call this when: + * Starting to access a server + * Making requests to external APIs + * Receiving responses from external systems + * Completing server interactions + - Examples: + * "Connecting to Google Maps API..." + * "Fetching location data for New York..." + * "Processing route information..." + * "Retrieved map data successfully" + [MANDATORY WORKFLOW] 1. On receiving task: IF task involves login/credentials/authentication: @@ -176,12 +210,14 @@ class orchestrator_deps: provider=provider ) +# Initialize the agent with just the main MCP server for now +# External servers will be added dynamically at runtime orchestrator_agent = Agent( model=model, name="Orchestrator Agent", system_prompt=orchestrator_system_prompt, deps_type=orchestrator_deps, - # mcp_servers=[server], + mcp_servers=[server], # Start with just the main server ) # Human Input Tool attached to the orchestrator agent as a tool @@ -233,6 +269,57 @@ async def ask_human(ctx: RunContext[orchestrator_deps], question: str) -> str: return f"Failed to get human input: {error_msg}" +@orchestrator_agent.tool +async def server_status_update(ctx: RunContext[orchestrator_deps], server_name: str, status_message: str, progress: float = 0, details: Dict[str, Any] = None) -> str: + """Send status update about an external server to the UI + + Args: + server_name: Name of the server being accessed (e.g., 'google_maps', 'github') + status_message: Short status message to display + progress: Progress percentage (0-100) + details: Optional detailed information about the server status + """ + try: + if server_name == 'npx': + logfire.info(f"Server Initialisation with npx. No requirement of sending update to UI") + return f"Server Initialisation with npx. No requirement of sending update to UI" + + logfire.info(f"Server status update for {server_name}: {status_message}") + if ctx.deps.stream_output is None: + return f"Could not send status update: No stream output available" + + # Initialize server_status if needed + if ctx.deps.stream_output.server_status is None: + ctx.deps.stream_output.server_status = {} + + # Create status update + status_update = { + "status": status_message, + "progress": progress, + "timestamp": str(uuid.uuid4()) # Generate unique ID for this update + } + + # Add optional details + if details: + status_update["details"] = details + + # Update stream_output + ctx.deps.stream_output.server_status[server_name] = status_update + ctx.deps.stream_output.steps.append(f"Server update from {server_name}: {status_message}") + + # Send update to WebSocket + success = await _safe_websocket_send(ctx.deps.websocket, ctx.deps.stream_output) + + if success: + return f"Successfully sent status update for {server_name}" + else: + return f"Failed to send status update for {server_name}: WebSocket error" + + except Exception as e: + error_msg = f"Error sending server status update: {str(e)}" + logfire.error(error_msg, exc_info=True) + return f"Failed to send server status update: {error_msg}" + async def _safe_websocket_send(socket: WebSocket, message: Any) -> bool: """Safely send message through websocket with error handling""" try: diff --git a/cortex_on/connect_to_external_server.py b/cortex_on/connect_to_external_server.py index b3ba345..35c6aee 100644 --- a/cortex_on/connect_to_external_server.py +++ b/cortex_on/connect_to_external_server.py @@ -3,9 +3,11 @@ import os from contextlib import AsyncExitStack from typing import Any, Dict, List, Optional, Tuple, Union +from datetime import datetime import nest_asyncio from colorama import Fore, Style, init +import logfire from mcp import ClientSession, StdioServerParameters from pydantic_ai.models.anthropic import AnthropicModel from pydantic_ai.providers.anthropic import AnthropicProvider @@ -25,9 +27,36 @@ def __init__(self, config_path: str = 'config/external_mcp_servers.json'): self.servers: Dict[str, MCPServerStdio] = {} self.server_configs: Dict[str, Dict[str, Any]] = {} self.server_tools: Dict[str, List[Dict[str, Any]]] = {} + self.server_status: Dict[str, Dict[str, Any]] = {} + self.registered_tool_names: List[str] = [] # Track all registered tool names to ensure uniqueness + self.active_servers: List[MCPServerStdio] = [] # Track currently active servers + async def shutdown_servers(self): + """Properly shut down all active servers""" + for server in self.active_servers: + try: + if hasattr(server, 'close') and callable(server.close): + await server.close() + elif hasattr(server, '__aexit__') and callable(server.__aexit__): + await server.__aexit__(None, None, None) + logfire.info(f"Shut down MCP server: {server}") + except Exception as e: + print(f"Error shutting down server: {str(e)}") + + # Clear the active servers list + self.active_servers = [] + self.servers = {} + self.server_tools = {} + print("All servers have been shut down") + async def load_servers(self) -> Tuple[List[MCPServerStdio], str]: """Load server configurations from JSON and create MCPServerStdio instances""" + # First shut down any existing servers + await self.shutdown_servers() + + # Clear registered tool names before loading new servers + self.registered_tool_names = [] + # Check if config file exists in the specified path or try to find it if not os.path.exists(self.config_path): # Try to find it relative to the current file @@ -65,6 +94,7 @@ async def load_servers(self) -> Tuple[List[MCPServerStdio], str]: # Create the MCPServerStdio instance try: + # Use namespaced tool names by setting the namespace parameter server = MCPServerStdio( command, args=args, @@ -74,26 +104,39 @@ async def load_servers(self) -> Tuple[List[MCPServerStdio], str]: self.servers[server_name] = server stdio_servers.append(server) server_names.append(server_name) + self.active_servers.append(server) # Track in active servers list + + # Initialize server status + self.server_status[server_name] = { + "status": "initializing", + "last_update": datetime.now().isoformat() + } print(f"Created MCPServerStdio for {server_name} with command: {command} {' '.join(args)}") except Exception as e: print(f"Error creating MCPServerStdio for {server_name}: {str(e)}") + # Wait for servers to initialize before attempting to discover tools + await asyncio.sleep(2) + + # Try to discover tools from all servers + for server_name in server_names: + print(f"Attempting to discover tools for {server_name}...") + await self.get_server_tools(server_name) + # Generate a combined system prompt with server information system_prompt = self._generate_system_prompt(server_names) return stdio_servers, system_prompt def _generate_system_prompt(self, server_names: List[str]) -> str: - """Generate a system prompt for the agent with information about available servers""" + """Generate a system prompt for the agent with information about available servers and their tools""" if not server_names: return "You are an AI assistant that can help with various tasks." servers_list = ", ".join([f"`{name}`" for name in server_names]) - prompt = f"""You also have access to the following MCP servers: {servers_list}. - Each server provides specialized tools that you can use to complete tasks: - + prompt = f"""[EXTERNAL SERVER CAPABILITIES] """ # Add details about each server and its tools if available @@ -101,12 +144,295 @@ def _generate_system_prompt(self, server_names: List[str]) -> str: config = self.server_configs.get(server_name, {}) description = config.get('description', f"MCP server for {server_name}") - prompt += f"- {server_name}: {description}\n" - + prompt += f"""- {server_name}: + Description: {description} + Usage scenarios: + """ + + # Add general usage scenarios based on server description + keywords = server_name.lower().split('-') + if "github" in keywords: + prompt += """ - Repository operations + - Code browsing and analysis + - Issue and PR management + """ + elif "google" in keywords and "maps" in keywords: + prompt += """ - Geocoding and location services + - Directions and routing + - Place search and information + """ + else: + # Generic description based on server name + prompt += f""" - {server_name.replace('-', ' ').title()} operations + """ + + # Add tool information + prompt += " Available tools:\n" + + # Use any tools we've discovered + tools = self.server_tools.get(server_name, []) + if tools: + for tool in tools: + tool_name = tool.get("name", f"{server_name}.unknown_tool") + tool_description = tool.get("description", "No description available") + prompt += f" - {tool_name}: {tool_description}\n" + else: + # If no tools are discovered, provide generic information + prompt += f" - Various tools prefixed with '{server_name}.'\n" + + prompt += """ + [HOW TO USE EXTERNAL SERVERS] + 1. When a user's task requires capabilities from an external server: + - Identify which server is most appropriate based on the task description + - Use the server's tools directly with the server name prefix (e.g., {server}.tool_name) + - Include all required parameters for the tool + + 2. Server selection guidelines: + """ + + # Dynamically generate server selection guidelines based on available servers + for server_name in server_names: + server_title = server_name.replace('-', ' ').title() + prompt += f" - For {server_title} operations: Choose the {server_name} server\n" + prompt += """ - When using these servers, reference them by their tools as available. - """ + 3. Important notes: + - Always include the server name prefix with the tool name + - Multiple servers can be used in the same task when needed + - Provide detailed parameters based on the specific tool requirements + """ return prompt + async def get_server_tools(self, server_name: str) -> List[Dict[str, Any]]: + """Fetch tool information from a running server by introspecting its capabilities""" + if server_name not in self.servers: + return [] + + try: + server = self.servers[server_name] + tools = [] + + # Access the private _mcp_api property to get tool information + # Note: This is implementation-specific and might need adjustment + # based on the actual MCPServerStdio implementation + if hasattr(server, '_mcp_api') and server._mcp_api: + api = server._mcp_api + + # Check if the API has a tool manager + if hasattr(api, '_tool_manager'): + tool_manager = api._tool_manager + + # Get tools from the tool manager + if hasattr(tool_manager, '_tools') and tool_manager._tools: + for tool_name, tool_info in tool_manager._tools.items(): + # Ensure tool name has server namespace prefix + if not tool_name.startswith(f"{server_name}."): + prefixed_name = f"{server_name}.{tool_name}" + else: + prefixed_name = tool_name + + # Check if this tool name is already registered + if prefixed_name in self.registered_tool_names: + # Make the name unique by adding a suffix + base_name = prefixed_name + suffix = 1 + while f"{base_name}_{suffix}" in self.registered_tool_names: + suffix += 1 + prefixed_name = f"{base_name}_{suffix}" + + # Add to the registered names list + self.registered_tool_names.append(prefixed_name) + + # Extract description if available + description = "No description available" + if hasattr(tool_info, 'description') and tool_info.description: + description = tool_info.description + elif hasattr(tool_info, '__doc__') and tool_info.__doc__: + description = tool_info.__doc__.strip() + + tools.append({ + "name": prefixed_name, + "description": description + }) + + # If we couldn't get tools through introspection, try to get schema + if not tools and hasattr(server, 'get_schema'): + try: + # Some MCP servers might have a get_schema method + schema = await server.get_schema() + if schema and 'tools' in schema: + for tool in schema['tools']: + name = tool.get('name', '') + # Ensure tool name has server namespace prefix + if not name.startswith(f"{server_name}."): + prefixed_name = f"{server_name}.{name}" + else: + prefixed_name = name + + # Check if this tool name is already registered + if prefixed_name in self.registered_tool_names: + # Make the name unique by adding a suffix + base_name = prefixed_name + suffix = 1 + while f"{base_name}_{suffix}" in self.registered_tool_names: + suffix += 1 + prefixed_name = f"{base_name}_{suffix}" + + # Add to the registered names list + self.registered_tool_names.append(prefixed_name) + + tools.append({ + "name": prefixed_name, + "description": tool.get('description', f"Tool from {server_name}") + }) + except Exception as schema_err: + print(f"Error getting schema from {server_name}: {str(schema_err)}") + + # Fallback for when we can't directly access the tool information: + # We'll use some typical tools based on server name to provide useful information + if not tools: + fallback_tools = [] + + if server_name == "github": + fallback_tools = [ + {"base_name": "search_repositories", "description": "Search for GitHub repositories"}, + {"base_name": "get_repository", "description": "Get details about a specific repository"}, + {"base_name": "list_issues", "description": "List issues for a repository"}, + {"base_name": "create_issue", "description": "Create a new issue in a repository"}, + {"base_name": "search_code", "description": "Search for code within repositories"} + ] + elif server_name == "google-maps": + fallback_tools = [ + {"base_name": "geocode", "description": "Convert addresses to geographic coordinates"}, + {"base_name": "directions", "description": "Get directions between locations"}, + {"base_name": "places", "description": "Search for places near a location"}, + {"base_name": "distance_matrix", "description": "Calculate distance and travel time"} + ] + else: + fallback_tools = [ + {"base_name": "use", "description": f"Use the {server_name} service"} + ] + + # Process the fallback tools with unique naming + for tool_info in fallback_tools: + base_name = tool_info["base_name"] + prefixed_name = f"{server_name}.{base_name}" + + # Check if this tool name is already registered + if prefixed_name in self.registered_tool_names: + # Make the name unique by adding a suffix + suffix = 1 + while f"{prefixed_name}_{suffix}" in self.registered_tool_names: + suffix += 1 + prefixed_name = f"{prefixed_name}_{suffix}" + + # Add to the registered names list + self.registered_tool_names.append(prefixed_name) + + tools.append({ + "name": prefixed_name, + "description": tool_info["description"] + }) + + print(f"Using fallback tool definitions for {server_name} - actual tools couldn't be discovered") + + # Store and return the tools + self.server_tools[server_name] = tools + print(f"Discovered {len(tools)} tools for {server_name}") + return tools + except Exception as e: + print(f"Error discovering tools from {server_name}: {str(e)}") + return [] + + async def monitor_server_status(self, server_name: str, callback: callable) -> None: + """ + Set up monitoring for a server's status and call the callback with updates + + Args: + server_name: The name of the server to monitor + callback: An async function to call with status updates (takes server_name and status dict) + """ + if server_name not in self.servers: + return + + try: + # Set initial status + status = { + "status": "monitoring", + "progress": 75, + "last_update": datetime.now().isoformat() + } + + # Call the callback with initial status + try: + await callback(server_name, status) + except Exception as cb_err: + logfire.error(f"Error calling status callback: {str(cb_err)}") + + # Check server health periodically + check_count = 0 + while server_name in self.servers: + check_count += 1 + + # Get the server + server = self.servers[server_name] + is_healthy = False + + # Try to determine if the server is healthy + try: + if hasattr(server, '_mcp_api') and server._mcp_api: + # We'll consider it healthy if it has an API + is_healthy = True + + # Get more detailed health info if available + if hasattr(server._mcp_api, 'health') and callable(server._mcp_api.health): + health_info = await server._mcp_api.health() + if isinstance(health_info, dict): + status.update(health_info) + except Exception: + is_healthy = False + + # Update the status based on health check + if is_healthy: + status = { + "status": "running", + "progress": 100, + "health": "ok", + "last_update": datetime.now().isoformat(), + "check_count": check_count + } + else: + status = { + "status": "degraded", + "progress": 80, + "health": "degraded", + "last_update": datetime.now().isoformat(), + "check_count": check_count + } + + # Call the callback with the status + try: + await callback(server_name, status) + except Exception as cb_err: + logfire.error(f"Error calling status callback: {str(cb_err)}") + + # Wait before the next check + await asyncio.sleep(5) # Check every 5 seconds + + except Exception as e: + logfire.error(f"Error monitoring server {server_name}: {str(e)}") + + # Try to send a final error status + try: + status = { + "status": "error", + "progress": 0, + "error": str(e), + "last_update": datetime.now().isoformat() + } + await callback(server_name, status) + except Exception: + pass + server_provider = StdioServerProvider() \ No newline at end of file diff --git a/cortex_on/instructor.py b/cortex_on/instructor.py index 8283b3d..0fb8520 100644 --- a/cortex_on/instructor.py +++ b/cortex_on/instructor.py @@ -15,18 +15,12 @@ from fastapi import WebSocket import logfire from pydantic import BaseModel -from pydantic_ai import Agent -from pydantic_ai.messages import ModelMessage -from pydantic_ai.models.anthropic import AnthropicModel -from mcp.server.fastmcp import FastMCP -from pydantic_ai.mcp import MCPServerHTTP # Local application imports from agents.orchestrator_agent import orchestrator_agent, orchestrator_deps, orchestrator_system_prompt from utils.stream_response_format import StreamResponse -from agents.mcp_server import start_mcp_server, register_tools_for_main_mcp_server, server_manager +from agents.mcp_server import start_mcp_server, register_tools_for_main_mcp_server, server_manager, check_mcp_server_tools from connect_to_external_server import server_provider -from prompts import time_server_prompt load_dotenv() class DateTimeEncoder(json.JSONEncoder): @@ -72,6 +66,81 @@ async def _safe_websocket_send(self, message: Any) -> bool: except Exception as e: logfire.error(f"WebSocket send failed: {str(e)}") return False + async def send_server_status_update(self, stream_output: StreamResponse, server_name: str, status: Dict[str, Any]) -> bool: + """Send server status update via WebSocket + + Args: + stream_output: The StreamResponse object to update + server_name: Name of the server being accessed + status: Status information to stream + """ + try: + # Ensure we have a server_status dictionary + if not hasattr(stream_output, 'server_status') or stream_output.server_status is None: + stream_output.server_status = {} + + # Add a timestamp to the status update + status_with_timestamp = {**status, "timestamp": datetime.now().isoformat()} + + # Update the status in the stream_output + stream_output.server_status[server_name] = status_with_timestamp + + # Add a step message for non-npx servers or if it's an important status + important_statuses = ["ready", "error", "failed", "connected"] + if server_name != 'npx' or status.get('status', '') in important_statuses: + stream_output.steps.append(f"Server update from {server_name}: {status.get('status', 'processing')}") + + # Make sure the WebSocket is still connected + if self.websocket and self.websocket.client_state.CONNECTED: + # Send the update and retry if needed + max_retries = 3 + for attempt in range(max_retries): + try: + # Try to send the message + await self.websocket.send_text(json.dumps(asdict(stream_output))) + logfire.debug(f"Server status update sent for {server_name}: {status.get('status')}") + return True + except Exception as send_err: + if attempt < max_retries - 1: + # Brief wait before retry + await asyncio.sleep(0.1 * (attempt + 1)) + logfire.warning(f"Retrying server status update ({attempt+1}/{max_retries})") + else: + # Last attempt failed + logfire.error(f"Failed to send server status update after {max_retries} attempts: {str(send_err)}") + return False + else: + logfire.warning(f"WebSocket disconnected, couldn't send status update for {server_name}") + return False + + except Exception as e: + logfire.error(f"Failed to send server status update: {str(e)}") + return False + + def _reset_orchestrator_agent(self): + """Reset the orchestrator agent for a new chat session""" + try: + # Keep only the main server (first one) and remove all external servers + if len(orchestrator_agent._mcp_servers) > 1: + main_server = orchestrator_agent._mcp_servers[0] + orchestrator_agent._mcp_servers = [main_server] + logfire.info("Reset orchestrator_agent MCP servers to just the main server") + + # Reset the system prompt to its original state + orchestrator_agent.system_prompt = orchestrator_system_prompt + logfire.info("Reset orchestrator_agent system prompt to default") + + # If there's a tools manager, clear any cache it might have + for server in orchestrator_agent._mcp_servers: + if hasattr(server, '_mcp_api') and server._mcp_api: + api = server._mcp_api + if hasattr(api, '_tool_manager'): + tool_manager = api._tool_manager + if hasattr(tool_manager, '_cached_tool_schemas'): + tool_manager._cached_tool_schemas = None + logfire.info(f"Cleared tool schema cache for server {server}") + except Exception as e: + logfire.error(f"Error resetting orchestrator agent: {str(e)}") async def run(self, task: str, websocket: WebSocket, server_config: Optional[Dict[str, int]] = None) -> List[Dict[str, Any]]: """ @@ -82,6 +151,9 @@ async def run(self, task: str, websocket: WebSocket, server_config: Optional[Dic websocket: The active WebSocket connection server_config: Optional configuration for MCP servers {name: port} """ + # Reset the orchestrator agent to ensure we start fresh for each new chat + # self._reset_orchestrator_agent() + self.websocket = websocket stream_output = StreamResponse( agent_name="Orchestrator", @@ -91,8 +163,8 @@ async def run(self, task: str, websocket: WebSocket, server_config: Optional[Dic status_code=0, message_id=str(uuid.uuid4()) ) - self.orchestrator_response.append(stream_output) - + self.orchestrator_response = [stream_output] # Reset the response list for new chat + # Create dependencies with list to track agent responses deps_for_orchestrator = orchestrator_deps( websocket=self.websocket, @@ -119,12 +191,74 @@ async def run(self, task: str, websocket: WebSocket, server_config: Optional[Dic # Start each configured external MCP server servers, system_prompt = await server_provider.load_servers() - orchestrator_agent._mcp_servers = servers - orchestrator_agent.system_prompt = orchestrator_system_prompt + "\n\n" + system_prompt + + # Send status update for each server being loaded + for i, server in enumerate(servers): + server_name = server.command.split('/')[-1] if hasattr(server, 'command') else f"server_{i}" + await self.send_server_status_update( + stream_output, + server_name, + {"status": "initializing", "progress": i/len(servers)*100} + ) + + # We need to make sure each MCP server has unique tool names + # First, check the main MCP server's tools + registered_tools = set() + main_server = orchestrator_agent._mcp_servers[0] + check_mcp_server_tools(main_server, registered_tools) + + # Now add each external server and check its tools + for server in servers: + # Check and deduplicate tools before adding + check_mcp_server_tools(server, registered_tools) + # Adding one at a time after checking + orchestrator_agent._mcp_servers.append(server) + logfire.info(f"Added MCP server: {server.__class__.__name__}") + + # Properly integrate external server capabilities into the system prompt + updated_system_prompt = orchestrator_system_prompt + if system_prompt and system_prompt.strip(): + if "[AVAILABLE TOOLS]" in updated_system_prompt: + sections = updated_system_prompt.split("[AVAILABLE TOOLS]") + updated_system_prompt = sections[0] + system_prompt + "\n\n[AVAILABLE TOOLS]" + sections[1] + else: + # If we can't find the section, just append to the end (fallback) + updated_system_prompt = updated_system_prompt + "\n\n" + system_prompt + + orchestrator_agent.system_prompt = updated_system_prompt logfire.info(f"Updated orchestrator agent with {len(servers)} MCP servers. Current MCP servers: {orchestrator_agent._mcp_servers}") - # Configure orchestrator_agent to use all configured MCP servers + logfire.info("Starting to register MCP server tools with Claude") + + # Send another status update before starting MCP servers + for i, server in enumerate(servers): + server_name = server.command.split('/')[-1] if hasattr(server, 'command') else f"server_{i}" + await self.send_server_status_update( + stream_output, + server_name, + {"status": "connecting", "progress": 50 + i/len(servers)*25} + ) + await asyncio.sleep(0.1) # Brief pause to allow updates to be sent + async with orchestrator_agent.run_mcp_servers(): + # Send status update that servers are ready + for i, server in enumerate(servers): + server_name = server.command.split('/')[-1] if hasattr(server, 'command') else f"server_{i}" + await self.send_server_status_update( + stream_output, + server_name, + {"status": "ready", "progress": 100} + ) + await asyncio.sleep(0.1) # Brief pause to allow updates to be sent + + # Start monitoring this server's status in the background + asyncio.create_task( + server_provider.monitor_server_status( + server_name, + lambda s, status: self.send_server_status_update(stream_output, s, status) + ) + ) + orchestrator_response = await orchestrator_agent.run( user_prompt=task, deps=deps_for_orchestrator @@ -138,6 +272,14 @@ async def run(self, task: str, websocket: WebSocket, server_config: Optional[Dic return [json.loads(json.dumps(asdict(i), cls=DateTimeEncoder)) for i in self.orchestrator_response] except Exception as e: + if "WebSocketDisconnect" in str(e): + logfire.info("WebSocket disconnected. Client likely closed the connection.") + try: + await self.shutdown() + except Exception as shutdown_err: + logfire.error(f"Error during cleanup after disconnect: {shutdown_err}") + return [json.loads(json.dumps(asdict(i), cls=DateTimeEncoder)) for i in self.orchestrator_response] + error_msg = f"Critical orchestration error: {str(e)}\n{traceback.format_exc()}" logfire.error(error_msg) @@ -147,7 +289,6 @@ async def run(self, task: str, websocket: WebSocket, server_config: Optional[Dic self.orchestrator_response.append(stream_output) await self._safe_websocket_send(stream_output) - # Even in case of critical error, return what we have try: return [json.loads(json.dumps(asdict(i), cls=DateTimeEncoder)) for i in self.orchestrator_response] except Exception as serialize_error: @@ -161,6 +302,12 @@ async def run(self, task: str, websocket: WebSocket, server_config: Optional[Dic async def shutdown(self): """Clean shutdown of orchestrator""" try: + # Reset the orchestrator agent + self._reset_orchestrator_agent() + + # Shut down all external MCP servers + await server_provider.shutdown_servers() + # Close websocket if open if self.websocket: await self.websocket.close() diff --git a/cortex_on/utils/stream_response_format.py b/cortex_on/utils/stream_response_format.py index 286761e..6d02040 100644 --- a/cortex_on/utils/stream_response_format.py +++ b/cortex_on/utils/stream_response_format.py @@ -1,5 +1,5 @@ from dataclasses import dataclass -from typing import List, Optional +from typing import List, Optional, Dict, Any import uuid @dataclass @@ -11,3 +11,4 @@ class StreamResponse: output: str live_url: Optional[str] = None message_id: str = "" # Unique identifier for each message + server_status: Optional[Dict[str, Any]] = None # Status updates from external servers From 8f046f5bb3d046953831e7397554a58337e8144d Mon Sep 17 00:00:00 2001 From: Sakalya100 Date: Mon, 5 May 2025 18:00:46 +0530 Subject: [PATCH 23/35] Added MCP Server detailed setup steps in README.md with sample queries --- README.md | 62 +++++++++++++++++++++++++++++++++++++++++++++ cortex_on/README.md | 3 +++ 2 files changed, 65 insertions(+) diff --git a/README.md b/README.md index 3920e4f..c9d8e17 100644 --- a/README.md +++ b/README.md @@ -112,6 +112,68 @@ This project uses HashiCorp Cloud Platform (HCP) Vault for secure secrets manage #### WebSocket - `VITE_WEBSOCKET_URL=ws://localhost:8081/ws` +#### Configuring External MCP Servers (OPTIONAL) + +CortexON supports integration with external MCP (Model Context Protocol) servers for extended capabilities. Configure these in the `cortex_on/config/external_mcp_servers.json` file. + +#### 1. GitHub Personal Access Token + +1. **Create a GitHub Account** if you don't already have one at [github.com](https://github.com) + +2. **Generate a Personal Access Token (PAT)**: + - Follow the steps as listed here: [Personal Access Token Setup](https://github.com/modelcontextprotocol/servers/tree/main/src/github#setup) + +3. **Add the Token to Your Configuration**: + - Open `cortex_on/config/external_mcp_servers.json` + - Find the GitHub section and replace the empty token: + ```json + "env": { + "GITHUB_PERSONAL_ACCESS_TOKEN": "ghp_YourTokenHere" + } + ``` +4. **Sample Queries** + - Update the README.md file in the repository by on branch main. Insert the line "Changed by CortexOn" in the end. Provide the updated file content as the content parameter and set branch as main. + - List the latest commit and in which repo the commit was made by + +#### 2. Google Maps API Key + +1. **Create a Google Cloud Account**: + - Go to [Google Cloud Console](https://console.cloud.google.com/) + - Create an account or sign in with your Google account + +2. **Create a New Project**: + - In the cloud console, click on the project dropdown at the top + - Click "New Project" + - Name it (e.g., "CortexON Maps") + - Click "Create" + +3. **Enable the Required APIs**: + - In your project, go to "APIs & Services" → "Library" + - Search for and enable these APIs: + * Maps JavaScript API + * Geocoding API + * Directions API + * Places API + * Distance Matrix API + - You can enable more APIs as per your requirements + +4. **Create an API Key**: + - Go to "APIs & Services" → "Credentials" + - Click "Create Credentials" → "API Key" + - Your new API key will be displayed + +5. **Add the API Key to Your Configuration**: + - Open `cortex_on/config/external_mcp_servers.json` + - Find the Google Maps section and replace the empty key: + ```json + "env": { + "GOOGLE_MAPS_API_KEY": "" + } + ``` +6. **Sample Queries** + - Find the closest pizza shops to \[address] within a 5-mile radius + - Find the shortest driving route that includes the following stops: \[address 1], \[address 2], and \[address 3] + ### Docker Setup 1. Clone the CortexON repository: diff --git a/cortex_on/README.md b/cortex_on/README.md index 99ae575..b730373 100644 --- a/cortex_on/README.md +++ b/cortex_on/README.md @@ -3,3 +3,6 @@ - configure `.env` (using example `.env.copy`) - either run `python -m src.main` in root folder - or run `uvicorn --reload --access-log --host 0.0.0.0 --port 8001 src.main:app` to use with frontend + + + From f3de91484de6997f713c2aae356260a4965ff007 Mon Sep 17 00:00:00 2001 From: Sakalya100 Date: Mon, 5 May 2025 18:02:44 +0530 Subject: [PATCH 24/35] Updated README.md with proper formatting --- README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index c9d8e17..a9c0994 100644 --- a/README.md +++ b/README.md @@ -112,7 +112,8 @@ This project uses HashiCorp Cloud Platform (HCP) Vault for secure secrets manage #### WebSocket - `VITE_WEBSOCKET_URL=ws://localhost:8081/ws` -#### Configuring External MCP Servers (OPTIONAL) +--- +### Configuring External MCP Servers (OPTIONAL) CortexON supports integration with external MCP (Model Context Protocol) servers for extended capabilities. Configure these in the `cortex_on/config/external_mcp_servers.json` file. @@ -174,6 +175,8 @@ CortexON supports integration with external MCP (Model Context Protocol) servers - Find the closest pizza shops to \[address] within a 5-mile radius - Find the shortest driving route that includes the following stops: \[address 1], \[address 2], and \[address 3] +--- + ### Docker Setup 1. Clone the CortexON repository: From 835de2a085fa7fd8ccdba3e0d066152582683802 Mon Sep 17 00:00:00 2001 From: Soumyajit Mondal Date: Tue, 27 May 2025 22:28:43 +0530 Subject: [PATCH 25/35] feat: mcp page ui added --- frontend/src/App.tsx | 2 ++ frontend/src/components/home/Header.tsx | 13 ++++++++- frontend/src/components/mcp/DefaultView.tsx | 12 +++++++++ frontend/src/components/mcp/Sidebar.tsx | 30 +++++++++++++++++++++ frontend/src/pages/MCP.tsx | 24 +++++++++++++++++ 5 files changed, 80 insertions(+), 1 deletion(-) create mode 100644 frontend/src/components/mcp/DefaultView.tsx create mode 100644 frontend/src/components/mcp/Sidebar.tsx create mode 100644 frontend/src/pages/MCP.tsx diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index ed4c83e..8589064 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -2,6 +2,7 @@ import {BrowserRouter, Route, Routes} from "react-router-dom"; import Home from "./pages/Home"; import {Layout} from "./pages/Layout"; import {Vault} from "./pages/Vault"; +import {MCP} from "./pages/MCP"; function App() { return ( @@ -10,6 +11,7 @@ function App() { }> } /> } /> + } /> diff --git a/frontend/src/components/home/Header.tsx b/frontend/src/components/home/Header.tsx index d6217fd..db548ab 100644 --- a/frontend/src/components/home/Header.tsx +++ b/frontend/src/components/home/Header.tsx @@ -21,7 +21,7 @@ const Header = () => { > Logo
-
+
nav("/vault")} className={`w-[10%] h-full flex justify-center items-center cursor-pointer border-b-2 hover:border-[#BD24CA] ${ @@ -32,6 +32,17 @@ const Header = () => { >

Vault

+ +
nav("/mcp")} + className={`w-[10%] h-full flex justify-center items-center cursor-pointer border-b-2 hover:border-[#BD24CA] ${ + location.includes("/mcp") + ? "border-[#BD24CA]" + : "border-background" + }`} + > +

MCP

+
+ ); + })} +
+ ); +}; + +export default Sidebar; \ No newline at end of file diff --git a/frontend/src/pages/MCP.tsx b/frontend/src/pages/MCP.tsx new file mode 100644 index 0000000..597ba01 --- /dev/null +++ b/frontend/src/pages/MCP.tsx @@ -0,0 +1,24 @@ +import {ScrollArea} from "@/components/ui/scroll-area"; +import Sidebar from "@/components/mcp/Sidebar"; +import DefaultView from "@/components/mcp/DefaultView"; + +export const MCP = () => { + return ( +
+ +
+ {/*
+

MCP

+

+ Monitor and control your processes. +

+
*/} + + + +
+
+ ); +}; + +export default MCP; \ No newline at end of file From 2e62b2280ceceac1ced3b8fba7ca516b3e66898a Mon Sep 17 00:00:00 2001 From: Soumyajit Mondal Date: Tue, 27 May 2025 22:41:54 +0530 Subject: [PATCH 26/35] feat: mcp server pages ui for each mcp server button added --- frontend/src/components/mcp/Sidebar.tsx | 7 +++- .../components/mcp/services/ClaudeView.tsx | 26 +++++++++++++ .../src/components/mcp/services/FigmaView.tsx | 26 +++++++++++++ .../components/mcp/services/GithubView.tsx | 26 +++++++++++++ .../mcp/services/GoogleMapsView.tsx | 26 +++++++++++++ frontend/src/pages/MCP.tsx | 38 +++++++++++++------ 6 files changed, 137 insertions(+), 12 deletions(-) create mode 100644 frontend/src/components/mcp/services/ClaudeView.tsx create mode 100644 frontend/src/components/mcp/services/FigmaView.tsx create mode 100644 frontend/src/components/mcp/services/GithubView.tsx create mode 100644 frontend/src/components/mcp/services/GoogleMapsView.tsx diff --git a/frontend/src/components/mcp/Sidebar.tsx b/frontend/src/components/mcp/Sidebar.tsx index 20607c5..da64b2b 100644 --- a/frontend/src/components/mcp/Sidebar.tsx +++ b/frontend/src/components/mcp/Sidebar.tsx @@ -1,6 +1,10 @@ import React from 'react'; import { Github, Map, Figma, Bot } from 'lucide-react'; +interface SidebarProps { + onServiceSelect: (service: string) => void; +} + const services = [ { name: 'GitHub', icon: Github }, { name: 'Google Maps', icon: Map }, @@ -8,7 +12,7 @@ const services = [ { name: 'Claude', icon: Bot }, ]; -const Sidebar = () => { +const Sidebar = ({ onServiceSelect }: SidebarProps) => { return (
{services.map((service) => { @@ -16,6 +20,7 @@ const Sidebar = () => { return ( +
+
+ ); +}; + +export default ClaudeView; \ No newline at end of file diff --git a/frontend/src/components/mcp/services/FigmaView.tsx b/frontend/src/components/mcp/services/FigmaView.tsx new file mode 100644 index 0000000..47d0691 --- /dev/null +++ b/frontend/src/components/mcp/services/FigmaView.tsx @@ -0,0 +1,26 @@ +import { Input } from "@/components/ui/input"; +import { Button } from "@/components/ui/button"; + +const FigmaView = () => { + return ( +
+
+

Figma MCP

+

+ Some details, how to configure token etc +

+
+ +
+ + +
+
+ ); +}; + +export default FigmaView; \ No newline at end of file diff --git a/frontend/src/components/mcp/services/GithubView.tsx b/frontend/src/components/mcp/services/GithubView.tsx new file mode 100644 index 0000000..e849fa2 --- /dev/null +++ b/frontend/src/components/mcp/services/GithubView.tsx @@ -0,0 +1,26 @@ +import { Input } from "@/components/ui/input"; +import { Button } from "@/components/ui/button"; + +const GithubView = () => { + return ( +
+
+

GitHub MCP

+

+ Some details, how to configure token etc +

+
+ +
+ + +
+
+ ); +}; + +export default GithubView; \ No newline at end of file diff --git a/frontend/src/components/mcp/services/GoogleMapsView.tsx b/frontend/src/components/mcp/services/GoogleMapsView.tsx new file mode 100644 index 0000000..4c55608 --- /dev/null +++ b/frontend/src/components/mcp/services/GoogleMapsView.tsx @@ -0,0 +1,26 @@ +import { Input } from "@/components/ui/input"; +import { Button } from "@/components/ui/button"; + +const GoogleMapsView = () => { + return ( +
+
+

Google Maps MCP

+

+ Some details, how to configure token etc +

+
+ +
+ + +
+
+ ); +}; + +export default GoogleMapsView; \ No newline at end of file diff --git a/frontend/src/pages/MCP.tsx b/frontend/src/pages/MCP.tsx index 597ba01..742a016 100644 --- a/frontend/src/pages/MCP.tsx +++ b/frontend/src/pages/MCP.tsx @@ -1,20 +1,36 @@ -import {ScrollArea} from "@/components/ui/scroll-area"; +import { ScrollArea } from "@/components/ui/scroll-area"; import Sidebar from "@/components/mcp/Sidebar"; import DefaultView from "@/components/mcp/DefaultView"; +import GithubView from "@/components/mcp/services/GithubView"; +import GoogleMapsView from "@/components/mcp/services/GoogleMapsView"; +import FigmaView from "@/components/mcp/services/FigmaView"; +import ClaudeView from "@/components/mcp/services/ClaudeView"; +import { useState } from "react"; export const MCP = () => { + const [selectedService, setSelectedService] = useState(null); + + const renderContent = () => { + switch (selectedService) { + case "GitHub": + return ; + case "Google Maps": + return ; + case "Figma": + return ; + case "Claude": + return ; + default: + return ; + } + }; + return (
- -
- {/*
-

MCP

-

- Monitor and control your processes. -

-
*/} - - + +
+ + {renderContent()}
From ef5fd6dc9bc7366029473fc6dd66a03b0931a398 Mon Sep 17 00:00:00 2001 From: Sakalya100 Date: Wed, 28 May 2025 13:41:44 +0530 Subject: [PATCH 27/35] feat(mcp apis): - Added GET and POST APIs for MCP UI - Added request models for POST API for standaridzed request --- cortex_on/config/external_mcp_servers.json | 41 ++++++----- cortex_on/main.py | 81 +++++++++++++++++++++- cortex_on/utils/models.py | 10 +++ 3 files changed, 112 insertions(+), 20 deletions(-) diff --git a/cortex_on/config/external_mcp_servers.json b/cortex_on/config/external_mcp_servers.json index c867f9a..f28d952 100644 --- a/cortex_on/config/external_mcp_servers.json +++ b/cortex_on/config/external_mcp_servers.json @@ -1,21 +1,24 @@ { - "github": { - "command": "npx", - "args": [ - "-y", - "@modelcontextprotocol/server-github" - ], - "env": { - "GITHUB_PERSONAL_ACCESS_TOKEN": "" - }, - "description": "GitHub MCP server for repository operations" - }, - "google-maps": { - "command": "npx", - "args": ["-y", "@modelcontextprotocol/server-google-maps"], - "env": { - "GOOGLE_MAPS_API_KEY": "" - }, - "description": "Google Maps MCP server for geocoding, directions, and place search" + "github": { + "command": "npx", + "args": [ + "-y", + "@modelcontextprotocol/server-github" + ], + "env": {}, + "description": "GitHub MCP server for repository operations", + "status": "disabled", + "secret_key": "GITHUB_PERSONAL_ACCESS_TOKEN" + }, + "google-maps": { + "command": "npx", + "args": [ + "-y", + "@modelcontextprotocol/server-google-maps" + ], + "env": {}, + "description": "Google Maps MCP server for geocoding, directions, and place search", + "status": "disabled", + "secret_key": "GOOGLE_MAPS_API_KEY" } -} \ No newline at end of file +} \ No newline at end of file diff --git a/cortex_on/main.py b/cortex_on/main.py index a8dd4de..ef2fe9b 100644 --- a/cortex_on/main.py +++ b/cortex_on/main.py @@ -1,11 +1,13 @@ # Standard library imports from typing import List, Optional +import json # Third-party imports -from fastapi import FastAPI, WebSocket +from fastapi import FastAPI, WebSocket, HTTPException # Local application imports from instructor import SystemInstructor +from utils.models import MCPRequest app: FastAPI = FastAPI() @@ -25,3 +27,80 @@ async def websocket_endpoint(websocket: WebSocket): while True: data = await websocket.receive_text() await generate_response(data, websocket) + +@app.get("/agent/mcp/servers") +async def get_mcp_servers(): + with open("config/external_mcp_servers.json", "r") as f: + servers = json.load(f) + + servers_list = [] + for server in servers: + servers_list.append({ + "name": server, + "description": servers[server]["description"], + "status": servers[server]["status"] + }) + return servers_list + +@app.get("/agent/mcp/servers/{server_name}") +async def get_mcp_server(server_name: str): + with open("config/external_mcp_servers.json", "r") as f: + servers = json.load(f) + + if server_name not in servers: + raise HTTPException(status_code=404, detail="Server not found") + + config = { + 'command': servers[server_name]['command'], + 'args': servers[server_name]['args'] + # 'env': servers[server_name]['env'] + } if servers[server_name]['status'] == 'enabled' else {} + + return { + 'name': server_name, + 'status': servers[server_name]['status'], + 'description': servers[server_name]['description'], + 'config': config + } + +@app.post("/agent/mcp/servers") +async def configure_mcp_server(mcp_request: MCPRequest): + with open("config/external_mcp_servers.json", "r") as f: + servers = json.load(f) + + if mcp_request.server_name not in servers: + raise HTTPException(status_code=404, detail="Server not found") + + if not mcp_request.server_secret: + raise HTTPException(status_code=400, detail=f"Server secret is required to enable {mcp_request.server_name}") + + if mcp_request.action == 'enable': + if servers[mcp_request.server_name]['status'] == 'enabled': + raise HTTPException(status_code=400, detail=f"{mcp_request.server_name} is already enabled") + servers[mcp_request.server_name]['status'] = 'enabled' + server_secret_key = servers[mcp_request.server_name]['secret_key'] + servers[mcp_request.server_name]['env'][server_secret_key] = mcp_request.server_secret + + elif mcp_request.action == 'disable': + if servers[mcp_request.server_name]['status'] == 'disabled': + raise HTTPException(status_code=400, detail=f"{mcp_request.server_name} is already disabled") + servers[mcp_request.server_name]['status'] = 'disabled' + servers[mcp_request.server_name]['env'] = {} + + + with open("config/external_mcp_servers.json", "w") as f: + json.dump(servers, f, indent=4) + + config = { + 'command': servers[mcp_request.server_name]['command'], + 'args': servers[mcp_request.server_name]['args'] + # 'env': servers[server_name]['env'] + } if servers[mcp_request.server_name]['status'] == 'enabled' else {} + + return { + 'name': mcp_request.server_name, + 'status': servers[mcp_request.server_name]['status'], + 'description': servers[mcp_request.server_name]['description'], + 'config': config + } + diff --git a/cortex_on/utils/models.py b/cortex_on/utils/models.py index 2666c64..e8e55c3 100644 --- a/cortex_on/utils/models.py +++ b/cortex_on/utils/models.py @@ -1,5 +1,6 @@ from pydantic import BaseModel from typing import Dict, Optional +from enum import Enum class FactModel(BaseModel): facts: str @@ -21,3 +22,12 @@ class LedgerModel(BaseModel): is_progress_being_made: LedgerAnswer next_speaker: LedgerAnswer instruction_or_question: LedgerAnswer + +class Action(str, Enum): + enable = "enable" + disable = "disable" + +class MCPRequest(BaseModel): + server_name: str + action: Action + server_secret: str From d904f04e03602e08c2c4c100af92d0de2b5a3ac3 Mon Sep 17 00:00:00 2001 From: Soumyajit Mondal Date: Wed, 28 May 2025 16:10:41 +0530 Subject: [PATCH 28/35] feat: MCP landing page ans server page UI --- cortex_on/config/external_mcp_servers.json | 29 ++++---- cortex_on/main.py | 81 +++++++++++++++++++++- cortex_on/utils/models.py | 10 +++ 3 files changed, 106 insertions(+), 14 deletions(-) diff --git a/cortex_on/config/external_mcp_servers.json b/cortex_on/config/external_mcp_servers.json index c867f9a..ba07964 100644 --- a/cortex_on/config/external_mcp_servers.json +++ b/cortex_on/config/external_mcp_servers.json @@ -2,20 +2,23 @@ "github": { "command": "npx", "args": [ - "-y", - "@modelcontextprotocol/server-github" + "-y", + "@modelcontextprotocol/server-github" ], - "env": { - "GITHUB_PERSONAL_ACCESS_TOKEN": "" - }, - "description": "GitHub MCP server for repository operations" + "env": {}, + "description": "GitHub MCP server for repository operations", + "status": "disabled", + "secret_key": "GITHUB_PERSONAL_ACCESS_TOKEN" }, "google-maps": { "command": "npx", - "args": ["-y", "@modelcontextprotocol/server-google-maps"], - "env": { - "GOOGLE_MAPS_API_KEY": "" - }, - "description": "Google Maps MCP server for geocoding, directions, and place search" - } -} \ No newline at end of file + "args": [ + "-y", + "@modelcontextprotocol/server-google-maps" + ], + "env": {}, + "description": "Google Maps MCP server for geocoding, directions, and place search", + "status": "disabled", + "secret_key": "GOOGLE_MAPS_API_KEY" + } +} \ No newline at end of file diff --git a/cortex_on/main.py b/cortex_on/main.py index a8dd4de..ef2fe9b 100644 --- a/cortex_on/main.py +++ b/cortex_on/main.py @@ -1,11 +1,13 @@ # Standard library imports from typing import List, Optional +import json # Third-party imports -from fastapi import FastAPI, WebSocket +from fastapi import FastAPI, WebSocket, HTTPException # Local application imports from instructor import SystemInstructor +from utils.models import MCPRequest app: FastAPI = FastAPI() @@ -25,3 +27,80 @@ async def websocket_endpoint(websocket: WebSocket): while True: data = await websocket.receive_text() await generate_response(data, websocket) + +@app.get("/agent/mcp/servers") +async def get_mcp_servers(): + with open("config/external_mcp_servers.json", "r") as f: + servers = json.load(f) + + servers_list = [] + for server in servers: + servers_list.append({ + "name": server, + "description": servers[server]["description"], + "status": servers[server]["status"] + }) + return servers_list + +@app.get("/agent/mcp/servers/{server_name}") +async def get_mcp_server(server_name: str): + with open("config/external_mcp_servers.json", "r") as f: + servers = json.load(f) + + if server_name not in servers: + raise HTTPException(status_code=404, detail="Server not found") + + config = { + 'command': servers[server_name]['command'], + 'args': servers[server_name]['args'] + # 'env': servers[server_name]['env'] + } if servers[server_name]['status'] == 'enabled' else {} + + return { + 'name': server_name, + 'status': servers[server_name]['status'], + 'description': servers[server_name]['description'], + 'config': config + } + +@app.post("/agent/mcp/servers") +async def configure_mcp_server(mcp_request: MCPRequest): + with open("config/external_mcp_servers.json", "r") as f: + servers = json.load(f) + + if mcp_request.server_name not in servers: + raise HTTPException(status_code=404, detail="Server not found") + + if not mcp_request.server_secret: + raise HTTPException(status_code=400, detail=f"Server secret is required to enable {mcp_request.server_name}") + + if mcp_request.action == 'enable': + if servers[mcp_request.server_name]['status'] == 'enabled': + raise HTTPException(status_code=400, detail=f"{mcp_request.server_name} is already enabled") + servers[mcp_request.server_name]['status'] = 'enabled' + server_secret_key = servers[mcp_request.server_name]['secret_key'] + servers[mcp_request.server_name]['env'][server_secret_key] = mcp_request.server_secret + + elif mcp_request.action == 'disable': + if servers[mcp_request.server_name]['status'] == 'disabled': + raise HTTPException(status_code=400, detail=f"{mcp_request.server_name} is already disabled") + servers[mcp_request.server_name]['status'] = 'disabled' + servers[mcp_request.server_name]['env'] = {} + + + with open("config/external_mcp_servers.json", "w") as f: + json.dump(servers, f, indent=4) + + config = { + 'command': servers[mcp_request.server_name]['command'], + 'args': servers[mcp_request.server_name]['args'] + # 'env': servers[server_name]['env'] + } if servers[mcp_request.server_name]['status'] == 'enabled' else {} + + return { + 'name': mcp_request.server_name, + 'status': servers[mcp_request.server_name]['status'], + 'description': servers[mcp_request.server_name]['description'], + 'config': config + } + diff --git a/cortex_on/utils/models.py b/cortex_on/utils/models.py index 2666c64..e8e55c3 100644 --- a/cortex_on/utils/models.py +++ b/cortex_on/utils/models.py @@ -1,5 +1,6 @@ from pydantic import BaseModel from typing import Dict, Optional +from enum import Enum class FactModel(BaseModel): facts: str @@ -21,3 +22,12 @@ class LedgerModel(BaseModel): is_progress_being_made: LedgerAnswer next_speaker: LedgerAnswer instruction_or_question: LedgerAnswer + +class Action(str, Enum): + enable = "enable" + disable = "disable" + +class MCPRequest(BaseModel): + server_name: str + action: Action + server_secret: str From 7bd520076b87095ed777ec87226d0ffa31420b82 Mon Sep 17 00:00:00 2001 From: Soumyajit Mondal Date: Wed, 28 May 2025 19:49:39 +0530 Subject: [PATCH 29/35] fix: MCP page UI --- frontend/src/components/mcp/DefaultView.tsx | 4 ++-- frontend/src/components/mcp/Sidebar.tsx | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/frontend/src/components/mcp/DefaultView.tsx b/frontend/src/components/mcp/DefaultView.tsx index 49a98e9..68d4434 100644 --- a/frontend/src/components/mcp/DefaultView.tsx +++ b/frontend/src/components/mcp/DefaultView.tsx @@ -1,9 +1,9 @@ const DefaultView = () => { return (
-
+

Hi

-

This is MCP server page

+

Choose your MCP server here

); diff --git a/frontend/src/components/mcp/Sidebar.tsx b/frontend/src/components/mcp/Sidebar.tsx index da64b2b..d269b8a 100644 --- a/frontend/src/components/mcp/Sidebar.tsx +++ b/frontend/src/components/mcp/Sidebar.tsx @@ -21,9 +21,9 @@ const Sidebar = ({ onServiceSelect }: SidebarProps) => { ); From a033f703be53ccb8a4c86f029a8c15a26ec04b50 Mon Sep 17 00:00:00 2001 From: Soumyajit Mondal Date: Wed, 28 May 2025 23:27:51 +0530 Subject: [PATCH 30/35] feat:MCP UI for landing page and each server section dynamically --- cortex_on/main.py | 10 +++++ frontend/src/components/mcp/Sidebar.tsx | 53 +++++++++++++++++++------ 2 files changed, 51 insertions(+), 12 deletions(-) diff --git a/cortex_on/main.py b/cortex_on/main.py index ef2fe9b..8dc4a62 100644 --- a/cortex_on/main.py +++ b/cortex_on/main.py @@ -4,6 +4,7 @@ # Third-party imports from fastapi import FastAPI, WebSocket, HTTPException +from fastapi.middleware.cors import CORSMiddleware # Local application imports from instructor import SystemInstructor @@ -12,6 +13,15 @@ app: FastAPI = FastAPI() +# Add CORS middleware +app.add_middleware( + CORSMiddleware, + allow_origins=["http://localhost:3000"], # Frontend URL + allow_credentials=True, + allow_methods=["*"], # Allows all methods + allow_headers=["*"], # Allows all headers +) + async def generate_response(task: str, websocket: Optional[WebSocket] = None): orchestrator: SystemInstructor = SystemInstructor() return await orchestrator.run(task, websocket) diff --git a/frontend/src/components/mcp/Sidebar.tsx b/frontend/src/components/mcp/Sidebar.tsx index d269b8a..ad1850b 100644 --- a/frontend/src/components/mcp/Sidebar.tsx +++ b/frontend/src/components/mcp/Sidebar.tsx @@ -1,30 +1,59 @@ -import React from 'react'; +import React, { useEffect, useState } from 'react'; import { Github, Map, Figma, Bot } from 'lucide-react'; interface SidebarProps { onServiceSelect: (service: string) => void; } -const services = [ - { name: 'GitHub', icon: Github }, - { name: 'Google Maps', icon: Map }, - { name: 'Figma', icon: Figma }, - { name: 'Claude', icon: Bot }, -]; +interface Server { + name: string; + description: string; + status: string; +} + +const getServiceIcon = (name: string) => { + switch(name.toLowerCase()) { + case 'github': + return Github; + case 'google-maps': + return Map; + default: + return Bot; + } +}; const Sidebar = ({ onServiceSelect }: SidebarProps) => { + const [servers, setServers] = useState([]); + const [loading, setLoading] = useState(true); + + useEffect(() => { + const fetchData = async () => { + try { + const response = await fetch('http://localhost:8081/agent/mcp/servers'); + const data = await response.json(); + setServers(data); + } catch (error) { + console.error('API call failed:', error); + } finally { + setLoading(false); + } + }; + + fetchData(); + }, []); + return (
- {services.map((service) => { - const IconComponent = service.icon; + {servers.map((server) => { + const IconComponent = getServiceIcon(server.name); return ( ); })} From 8a514006f2d8bedd1f8f15b4c09a44113f9cc817 Mon Sep 17 00:00:00 2001 From: Soumyajit Mondal Date: Wed, 28 May 2025 23:59:49 +0530 Subject: [PATCH 31/35] fix: MCP page UI --- frontend/src/components/mcp/Sidebar.tsx | 2 ++ frontend/src/pages/MCP.tsx | 8 ++++---- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/frontend/src/components/mcp/Sidebar.tsx b/frontend/src/components/mcp/Sidebar.tsx index ad1850b..0749532 100644 --- a/frontend/src/components/mcp/Sidebar.tsx +++ b/frontend/src/components/mcp/Sidebar.tsx @@ -17,6 +17,8 @@ const getServiceIcon = (name: string) => { return Github; case 'google-maps': return Map; + case 'figma': + return Figma; default: return Bot; } diff --git a/frontend/src/pages/MCP.tsx b/frontend/src/pages/MCP.tsx index 742a016..fbe1a3e 100644 --- a/frontend/src/pages/MCP.tsx +++ b/frontend/src/pages/MCP.tsx @@ -12,13 +12,13 @@ export const MCP = () => { const renderContent = () => { switch (selectedService) { - case "GitHub": + case "github": return ; - case "Google Maps": + case "google-maps": return ; - case "Figma": + case "figma": return ; - case "Claude": + case "claude": return ; default: return ; From 361f5b72ab2a68986ae54d9e07e0f806287955ed Mon Sep 17 00:00:00 2001 From: AshuMishraG Date: Thu, 29 May 2025 13:15:26 +0530 Subject: [PATCH 32/35] feat: enhance MCP service components with improved error handling and UX --- .gitignore | 3 + frontend/src/components/mcp/ErrorBoundary.tsx | 58 ++++++ .../components/mcp/services/ClaudeView.tsx | 176 +++++++++++++++-- .../src/components/mcp/services/FigmaView.tsx | 176 +++++++++++++++-- .../components/mcp/services/GithubView.tsx | 179 ++++++++++++++++-- .../mcp/services/GoogleMapsView.tsx | 176 +++++++++++++++-- 6 files changed, 688 insertions(+), 80 deletions(-) create mode 100644 frontend/src/components/mcp/ErrorBoundary.tsx diff --git a/.gitignore b/.gitignore index 5717c73..3ae101e 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,6 @@ .env +node_modules/ +package-lock.json +package.json \ No newline at end of file diff --git a/frontend/src/components/mcp/ErrorBoundary.tsx b/frontend/src/components/mcp/ErrorBoundary.tsx new file mode 100644 index 0000000..66dfc21 --- /dev/null +++ b/frontend/src/components/mcp/ErrorBoundary.tsx @@ -0,0 +1,58 @@ +import React, { Component, ErrorInfo, ReactNode } from "react"; + +interface Props { + children: ReactNode; + fallback?: ReactNode; +} + +interface State { + hasError: boolean; + error: Error | null; +} + +class ErrorBoundary extends Component { + public state: State = { + hasError: false, + error: null, + }; + + public static getDerivedStateFromError(error: Error): State { + return { hasError: true, error }; + } + + public componentDidCatch(error: Error, errorInfo: ErrorInfo) { + console.error("MCP Error:", error, errorInfo); + } + + public render() { + if (this.state.hasError) { + return ( + this.props.fallback || ( +
+
+

+ Something went wrong +

+

+ {this.state.error?.message || + "An unexpected error occurred"} +

+ +
+
+ ) + ); + } + + return this.props.children; + } +} + +export default ErrorBoundary; diff --git a/frontend/src/components/mcp/services/ClaudeView.tsx b/frontend/src/components/mcp/services/ClaudeView.tsx index 8b15b38..c049182 100644 --- a/frontend/src/components/mcp/services/ClaudeView.tsx +++ b/frontend/src/components/mcp/services/ClaudeView.tsx @@ -1,26 +1,162 @@ import { Input } from "@/components/ui/input"; import { Button } from "@/components/ui/button"; +import { useState } from "react"; +import { Loader2 } from "lucide-react"; +import ErrorBoundary from "../ErrorBoundary"; -const ClaudeView = () => { - return ( -
-
-

Claude MCP

-

- Some details, how to configure token etc -

-
- -
- - +interface ServerConfig { + command?: string; + args?: string[]; +} + +interface ServerResponse { + status: "success" | "error"; + message: string; + config?: ServerConfig; +} + +const ClaudeViewContent = () => { + const [apiKey, setApiKey] = useState(""); + const [response, setResponse] = useState(null); + const [isLoading, setIsLoading] = useState(false); + + const handleToggle = async (action: "enable" | "disable") => { + setIsLoading(true); + try { + const response = await fetch( + "http://localhost:8081/agent/mcp/servers", + { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + server_name: "claude", + server_secret: apiKey, + action, + }), + } + ); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const data = await response.json(); + setResponse(data); + } catch (error) { + setResponse({ + status: "error", + message: + error instanceof Error + ? error.message + : "Failed to connect to the server. Please try again.", + }); + } finally { + setIsLoading(false); + } + }; + + const getErrorMessage = (message: string) => { + if (message.toLowerCase().includes("invalid key")) { + return "The Claude API key you entered is invalid. Please check your key and try again."; + } + if (message.toLowerCase().includes("quota exceeded")) { + return "You have exceeded your Claude API quota. Please check your usage limits."; + } + return "There was an error processing your request. Please try again."; + }; + + return ( +
+
+

Claude MCP

+

+ Configure your Claude API key to enable Claude integration. + Create a key from your Anthropic account settings. +

+
+ +
+ setApiKey(e.target.value)} + /> +
+ + +
+
+ + {response && ( +
+

+ {response.status === "success" + ? response.message + : getErrorMessage(response.message)} +

+ {response.status === "success" && response.config && ( +
+

Command: {response.config.command}

+ {response.config.args && ( +

Args: {response.config.args.join(" ")}

+ )} +
+ )} +
+ )}
-
- ); + ); +}; + +const ClaudeView = () => { + return ( + + + + ); }; -export default ClaudeView; \ No newline at end of file +export default ClaudeView; diff --git a/frontend/src/components/mcp/services/FigmaView.tsx b/frontend/src/components/mcp/services/FigmaView.tsx index 47d0691..41b1295 100644 --- a/frontend/src/components/mcp/services/FigmaView.tsx +++ b/frontend/src/components/mcp/services/FigmaView.tsx @@ -1,26 +1,162 @@ import { Input } from "@/components/ui/input"; import { Button } from "@/components/ui/button"; +import { useState } from "react"; +import { Loader2 } from "lucide-react"; +import ErrorBoundary from "../ErrorBoundary"; -const FigmaView = () => { - return ( -
-
-

Figma MCP

-

- Some details, how to configure token etc -

-
- -
- - +interface ServerConfig { + command?: string; + args?: string[]; +} + +interface ServerResponse { + status: "success" | "error"; + message: string; + config?: ServerConfig; +} + +const FigmaViewContent = () => { + const [token, setToken] = useState(""); + const [response, setResponse] = useState(null); + const [isLoading, setIsLoading] = useState(false); + + const handleToggle = async (action: "enable" | "disable") => { + setIsLoading(true); + try { + const response = await fetch( + "http://localhost:8081/agent/mcp/servers", + { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + server_name: "figma", + server_secret: token, + action, + }), + } + ); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const data = await response.json(); + setResponse(data); + } catch (error) { + setResponse({ + status: "error", + message: + error instanceof Error + ? error.message + : "Failed to connect to the server. Please try again.", + }); + } finally { + setIsLoading(false); + } + }; + + const getErrorMessage = (message: string) => { + if (message.toLowerCase().includes("invalid token")) { + return "The Figma access token you entered is invalid. Please check your token and try again."; + } + if (message.toLowerCase().includes("expired")) { + return "Your Figma access token has expired. Please generate a new token and try again."; + } + return "There was an error processing your request. Please try again."; + }; + + return ( +
+
+

Figma MCP

+

+ Configure your Figma access token to enable Figma integration. + Create a token with file:read scope. +

+
+ +
+ setToken(e.target.value)} + /> +
+ + +
+
+ + {response && ( +
+

+ {response.status === "success" + ? response.message + : getErrorMessage(response.message)} +

+ {response.status === "success" && response.config && ( +
+

Command: {response.config.command}

+ {response.config.args && ( +

Args: {response.config.args.join(" ")}

+ )} +
+ )} +
+ )}
-
- ); + ); +}; + +const FigmaView = () => { + return ( + + + + ); }; -export default FigmaView; \ No newline at end of file +export default FigmaView; diff --git a/frontend/src/components/mcp/services/GithubView.tsx b/frontend/src/components/mcp/services/GithubView.tsx index e849fa2..d81823a 100644 --- a/frontend/src/components/mcp/services/GithubView.tsx +++ b/frontend/src/components/mcp/services/GithubView.tsx @@ -1,26 +1,165 @@ import { Input } from "@/components/ui/input"; import { Button } from "@/components/ui/button"; +import { useState } from "react"; +import { Loader2 } from "lucide-react"; +import ErrorBoundary from "../ErrorBoundary"; -const GithubView = () => { - return ( -
-
-

GitHub MCP

-

- Some details, how to configure token etc -

-
- -
- - +interface ServerConfig { + command?: string; + args?: string[]; +} + +interface ServerResponse { + status: "success" | "error"; + message: string; + config?: ServerConfig; +} + +const GithubViewContent = () => { + const [token, setToken] = useState(""); + const [response, setResponse] = useState(null); + const [isLoading, setIsLoading] = useState(false); + + const handleToggle = async (action: "enable" | "disable") => { + setIsLoading(true); + try { + const response = await fetch( + "http://localhost:8081/agent/mcp/servers", + { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + server_name: "github", + server_secret: token, + action, + }), + } + ); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const data = await response.json(); + setResponse(data); + } catch (error) { + setResponse({ + status: "error", + message: + error instanceof Error + ? error.message + : "Failed to connect to the server. Please try again.", + }); + } finally { + setIsLoading(false); + } + }; + + const getErrorMessage = (message: string) => { + if (message.toLowerCase().includes("invalid token")) { + return "The GitHub token you entered is invalid. Please check your token and try again."; + } + if (message.toLowerCase().includes("unauthorized")) { + return "The GitHub token you entered is not authorized. Please check your token permissions."; + } + if (message.toLowerCase().includes("expired")) { + return "Your GitHub token has expired. Please generate a new token and try again."; + } + return "There was an error processing your request. Please try again."; + }; + + return ( +
+
+

GitHub MCP

+

+ Configure your GitHub Personal Access Token to enable GitHub + integration. Create a token with repo and workflow scopes. +

+
+ +
+ setToken(e.target.value)} + /> +
+ + +
+
+ + {response && ( +
+

+ {response.status === "success" + ? response.message + : getErrorMessage(response.message)} +

+ {response.status === "success" && response.config && ( +
+

Command: {response.config.command}

+ {response.config.args && ( +

Args: {response.config.args.join(" ")}

+ )} +
+ )} +
+ )}
-
- ); + ); +}; + +const GithubView = () => { + return ( + + + + ); }; -export default GithubView; \ No newline at end of file +export default GithubView; diff --git a/frontend/src/components/mcp/services/GoogleMapsView.tsx b/frontend/src/components/mcp/services/GoogleMapsView.tsx index 4c55608..5193764 100644 --- a/frontend/src/components/mcp/services/GoogleMapsView.tsx +++ b/frontend/src/components/mcp/services/GoogleMapsView.tsx @@ -1,26 +1,162 @@ import { Input } from "@/components/ui/input"; import { Button } from "@/components/ui/button"; +import { useState } from "react"; +import { Loader2 } from "lucide-react"; +import ErrorBoundary from "../ErrorBoundary"; -const GoogleMapsView = () => { - return ( -
-
-

Google Maps MCP

-

- Some details, how to configure token etc -

-
- -
- - +interface ServerConfig { + command?: string; + args?: string[]; +} + +interface ServerResponse { + status: "success" | "error"; + message: string; + config?: ServerConfig; +} + +const GoogleMapsViewContent = () => { + const [apiKey, setApiKey] = useState(""); + const [response, setResponse] = useState(null); + const [isLoading, setIsLoading] = useState(false); + + const handleToggle = async (action: "enable" | "disable") => { + setIsLoading(true); + try { + const response = await fetch( + "http://localhost:8081/agent/mcp/servers", + { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + server_name: "google-maps", + server_secret: apiKey, + action, + }), + } + ); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const data = await response.json(); + setResponse(data); + } catch (error) { + setResponse({ + status: "error", + message: + error instanceof Error + ? error.message + : "Failed to connect to the server. Please try again.", + }); + } finally { + setIsLoading(false); + } + }; + + const getErrorMessage = (message: string) => { + if (message.toLowerCase().includes("invalid key")) { + return "The Google Maps API key you entered is invalid. Please check your key and try again."; + } + if (message.toLowerCase().includes("quota exceeded")) { + return "You have exceeded your Google Maps API quota. Please check your usage limits."; + } + return "There was an error processing your request. Please try again."; + }; + + return ( +
+
+

Google Maps MCP

+

+ Configure your Google Maps API key to enable Google Maps + integration. Create a key with Maps JavaScript API enabled. +

+
+ +
+ setApiKey(e.target.value)} + /> +
+ + +
+
+ + {response && ( +
+

+ {response.status === "success" + ? response.message + : getErrorMessage(response.message)} +

+ {response.status === "success" && response.config && ( +
+

Command: {response.config.command}

+ {response.config.args && ( +

Args: {response.config.args.join(" ")}

+ )} +
+ )} +
+ )}
-
- ); + ); +}; + +const GoogleMapsView = () => { + return ( + + + + ); }; -export default GoogleMapsView; \ No newline at end of file +export default GoogleMapsView; From a8f7a7224cd541d693ca55f6eb5f60cd8f7fa83d Mon Sep 17 00:00:00 2001 From: AshuMishraG Date: Thu, 29 May 2025 14:14:34 +0530 Subject: [PATCH 33/35] chore: simplify frontend feedback to user-friendly messages; delegate error handling to backend - Added concise success/error messages for enable/disable actions in MCP service components - Removed all detailed frontend error parsing and validation logic - Now rely on backend for all error handling and validation, reducing frontend complexity --- frontend/src/components/mcp/ErrorBoundary.tsx | 58 ------------ .../components/mcp/services/ClaudeView.tsx | 89 +++++++----------- .../src/components/mcp/services/FigmaView.tsx | 90 +++++++----------- .../components/mcp/services/GithubView.tsx | 92 +++++++------------ .../mcp/services/GoogleMapsView.tsx | 90 +++++++----------- 5 files changed, 134 insertions(+), 285 deletions(-) delete mode 100644 frontend/src/components/mcp/ErrorBoundary.tsx diff --git a/frontend/src/components/mcp/ErrorBoundary.tsx b/frontend/src/components/mcp/ErrorBoundary.tsx deleted file mode 100644 index 66dfc21..0000000 --- a/frontend/src/components/mcp/ErrorBoundary.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import React, { Component, ErrorInfo, ReactNode } from "react"; - -interface Props { - children: ReactNode; - fallback?: ReactNode; -} - -interface State { - hasError: boolean; - error: Error | null; -} - -class ErrorBoundary extends Component { - public state: State = { - hasError: false, - error: null, - }; - - public static getDerivedStateFromError(error: Error): State { - return { hasError: true, error }; - } - - public componentDidCatch(error: Error, errorInfo: ErrorInfo) { - console.error("MCP Error:", error, errorInfo); - } - - public render() { - if (this.state.hasError) { - return ( - this.props.fallback || ( -
-
-

- Something went wrong -

-

- {this.state.error?.message || - "An unexpected error occurred"} -

- -
-
- ) - ); - } - - return this.props.children; - } -} - -export default ErrorBoundary; diff --git a/frontend/src/components/mcp/services/ClaudeView.tsx b/frontend/src/components/mcp/services/ClaudeView.tsx index c049182..105f1f6 100644 --- a/frontend/src/components/mcp/services/ClaudeView.tsx +++ b/frontend/src/components/mcp/services/ClaudeView.tsx @@ -2,26 +2,22 @@ import { Input } from "@/components/ui/input"; import { Button } from "@/components/ui/button"; import { useState } from "react"; import { Loader2 } from "lucide-react"; -import ErrorBoundary from "../ErrorBoundary"; - -interface ServerConfig { - command?: string; - args?: string[]; -} - -interface ServerResponse { - status: "success" | "error"; - message: string; - config?: ServerConfig; -} const ClaudeViewContent = () => { const [apiKey, setApiKey] = useState(""); - const [response, setResponse] = useState(null); const [isLoading, setIsLoading] = useState(false); + const [feedback, setFeedback] = useState<{ + status: "success" | "error"; + message: string; + } | null>(null); const handleToggle = async (action: "enable" | "disable") => { + if (!apiKey.trim()) { + setFeedback({ status: "error", message: "Please enter an API key." }); + return; + } setIsLoading(true); + setFeedback(null); try { const response = await fetch( "http://localhost:8081/agent/mcp/servers", @@ -37,36 +33,30 @@ const ClaudeViewContent = () => { }), } ); - - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); + if (response.ok) { + setFeedback({ + status: "success", + message: + action === "enable" + ? "Claude API key enabled successfully!" + : "Claude API key disabled successfully!", + }); + } else { + setFeedback({ + status: "error", + message: "Failed to update Claude API key. Please try again.", + }); } - - const data = await response.json(); - setResponse(data); - } catch (error) { - setResponse({ + } catch { + setFeedback({ status: "error", - message: - error instanceof Error - ? error.message - : "Failed to connect to the server. Please try again.", + message: "Network error. Please try again.", }); } finally { setIsLoading(false); } }; - const getErrorMessage = (message: string) => { - if (message.toLowerCase().includes("invalid key")) { - return "The Claude API key you entered is invalid. Please check your key and try again."; - } - if (message.toLowerCase().includes("quota exceeded")) { - return "You have exceeded your Claude API quota. Please check your usage limits."; - } - return "There was an error processing your request. Please try again."; - }; - return (
@@ -89,7 +79,7 @@ const ClaudeViewContent = () => {
- {response && ( + {/* Feedback message box */} + {feedback && feedback.message && (

- {response.status === "success" - ? response.message - : getErrorMessage(response.message)} + {feedback.message}

- {response.status === "success" && response.config && ( -
-

Command: {response.config.command}

- {response.config.args && ( -

Args: {response.config.args.join(" ")}

- )} -
- )}
)}
@@ -152,11 +133,7 @@ const ClaudeViewContent = () => { }; const ClaudeView = () => { - return ( - - - - ); + return ; }; export default ClaudeView; diff --git a/frontend/src/components/mcp/services/FigmaView.tsx b/frontend/src/components/mcp/services/FigmaView.tsx index 41b1295..63bbee6 100644 --- a/frontend/src/components/mcp/services/FigmaView.tsx +++ b/frontend/src/components/mcp/services/FigmaView.tsx @@ -2,26 +2,22 @@ import { Input } from "@/components/ui/input"; import { Button } from "@/components/ui/button"; import { useState } from "react"; import { Loader2 } from "lucide-react"; -import ErrorBoundary from "../ErrorBoundary"; - -interface ServerConfig { - command?: string; - args?: string[]; -} - -interface ServerResponse { - status: "success" | "error"; - message: string; - config?: ServerConfig; -} const FigmaViewContent = () => { const [token, setToken] = useState(""); - const [response, setResponse] = useState(null); const [isLoading, setIsLoading] = useState(false); + const [feedback, setFeedback] = useState<{ + status: "success" | "error"; + message: string; + } | null>(null); const handleToggle = async (action: "enable" | "disable") => { + if (!token.trim()) { + setFeedback({ status: "error", message: "Please enter a token." }); + return; + } setIsLoading(true); + setFeedback(null); try { const response = await fetch( "http://localhost:8081/agent/mcp/servers", @@ -37,36 +33,31 @@ const FigmaViewContent = () => { }), } ); - - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); + if (response.ok) { + setFeedback({ + status: "success", + message: + action === "enable" + ? "Figma access token enabled successfully!" + : "Figma access token disabled successfully!", + }); + } else { + setFeedback({ + status: "error", + message: + "Failed to update Figma access token. Please try again.", + }); } - - const data = await response.json(); - setResponse(data); - } catch (error) { - setResponse({ + } catch { + setFeedback({ status: "error", - message: - error instanceof Error - ? error.message - : "Failed to connect to the server. Please try again.", + message: "Network error. Please try again.", }); } finally { setIsLoading(false); } }; - const getErrorMessage = (message: string) => { - if (message.toLowerCase().includes("invalid token")) { - return "The Figma access token you entered is invalid. Please check your token and try again."; - } - if (message.toLowerCase().includes("expired")) { - return "Your Figma access token has expired. Please generate a new token and try again."; - } - return "There was an error processing your request. Please try again."; - }; - return (
@@ -89,7 +80,7 @@ const FigmaViewContent = () => {
- {response && ( + {/* Feedback message box */} + {feedback && feedback.message && (

- {response.status === "success" - ? response.message - : getErrorMessage(response.message)} + {feedback.message}

- {response.status === "success" && response.config && ( -
-

Command: {response.config.command}

- {response.config.args && ( -

Args: {response.config.args.join(" ")}

- )} -
- )}
)}
@@ -152,11 +134,7 @@ const FigmaViewContent = () => { }; const FigmaView = () => { - return ( - - - - ); + return ; }; export default FigmaView; diff --git a/frontend/src/components/mcp/services/GithubView.tsx b/frontend/src/components/mcp/services/GithubView.tsx index d81823a..942d381 100644 --- a/frontend/src/components/mcp/services/GithubView.tsx +++ b/frontend/src/components/mcp/services/GithubView.tsx @@ -2,26 +2,22 @@ import { Input } from "@/components/ui/input"; import { Button } from "@/components/ui/button"; import { useState } from "react"; import { Loader2 } from "lucide-react"; -import ErrorBoundary from "../ErrorBoundary"; - -interface ServerConfig { - command?: string; - args?: string[]; -} - -interface ServerResponse { - status: "success" | "error"; - message: string; - config?: ServerConfig; -} const GithubViewContent = () => { const [token, setToken] = useState(""); - const [response, setResponse] = useState(null); const [isLoading, setIsLoading] = useState(false); + const [feedback, setFeedback] = useState<{ + status: "success" | "error"; + message: string; + } | null>(null); const handleToggle = async (action: "enable" | "disable") => { + if (!token.trim()) { + setFeedback({ status: "error", message: "Please enter a token." }); + return; + } setIsLoading(true); + setFeedback(null); try { const response = await fetch( "http://localhost:8081/agent/mcp/servers", @@ -37,39 +33,30 @@ const GithubViewContent = () => { }), } ); - - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); + if (response.ok) { + setFeedback({ + status: "success", + message: + action === "enable" + ? "GitHub token enabled successfully!" + : "GitHub token disabled successfully!", + }); + } else { + setFeedback({ + status: "error", + message: "Failed to update GitHub token. Please try again.", + }); } - - const data = await response.json(); - setResponse(data); - } catch (error) { - setResponse({ + } catch { + setFeedback({ status: "error", - message: - error instanceof Error - ? error.message - : "Failed to connect to the server. Please try again.", + message: "Network error. Please try again.", }); } finally { setIsLoading(false); } }; - const getErrorMessage = (message: string) => { - if (message.toLowerCase().includes("invalid token")) { - return "The GitHub token you entered is invalid. Please check your token and try again."; - } - if (message.toLowerCase().includes("unauthorized")) { - return "The GitHub token you entered is not authorized. Please check your token permissions."; - } - if (message.toLowerCase().includes("expired")) { - return "Your GitHub token has expired. Please generate a new token and try again."; - } - return "There was an error processing your request. Please try again."; - }; - return (
@@ -92,7 +79,7 @@ const GithubViewContent = () => {
- {response && ( + {/* Feedback message box */} + {feedback && feedback.message && (

- {response.status === "success" - ? response.message - : getErrorMessage(response.message)} + {feedback.message}

- {response.status === "success" && response.config && ( -
-

Command: {response.config.command}

- {response.config.args && ( -

Args: {response.config.args.join(" ")}

- )} -
- )}
)}
@@ -155,11 +133,7 @@ const GithubViewContent = () => { }; const GithubView = () => { - return ( - - - - ); + return ; }; export default GithubView; diff --git a/frontend/src/components/mcp/services/GoogleMapsView.tsx b/frontend/src/components/mcp/services/GoogleMapsView.tsx index 5193764..22ccec5 100644 --- a/frontend/src/components/mcp/services/GoogleMapsView.tsx +++ b/frontend/src/components/mcp/services/GoogleMapsView.tsx @@ -2,26 +2,22 @@ import { Input } from "@/components/ui/input"; import { Button } from "@/components/ui/button"; import { useState } from "react"; import { Loader2 } from "lucide-react"; -import ErrorBoundary from "../ErrorBoundary"; - -interface ServerConfig { - command?: string; - args?: string[]; -} - -interface ServerResponse { - status: "success" | "error"; - message: string; - config?: ServerConfig; -} const GoogleMapsViewContent = () => { const [apiKey, setApiKey] = useState(""); - const [response, setResponse] = useState(null); const [isLoading, setIsLoading] = useState(false); + const [feedback, setFeedback] = useState<{ + status: "success" | "error"; + message: string; + } | null>(null); const handleToggle = async (action: "enable" | "disable") => { + if (!apiKey.trim()) { + setFeedback({ status: "error", message: "Please enter an API key." }); + return; + } setIsLoading(true); + setFeedback(null); try { const response = await fetch( "http://localhost:8081/agent/mcp/servers", @@ -37,36 +33,31 @@ const GoogleMapsViewContent = () => { }), } ); - - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); + if (response.ok) { + setFeedback({ + status: "success", + message: + action === "enable" + ? "Google Maps API key enabled successfully!" + : "Google Maps API key disabled successfully!", + }); + } else { + setFeedback({ + status: "error", + message: + "Failed to update Google Maps API key. Please try again.", + }); } - - const data = await response.json(); - setResponse(data); - } catch (error) { - setResponse({ + } catch { + setFeedback({ status: "error", - message: - error instanceof Error - ? error.message - : "Failed to connect to the server. Please try again.", + message: "Network error. Please try again.", }); } finally { setIsLoading(false); } }; - const getErrorMessage = (message: string) => { - if (message.toLowerCase().includes("invalid key")) { - return "The Google Maps API key you entered is invalid. Please check your key and try again."; - } - if (message.toLowerCase().includes("quota exceeded")) { - return "You have exceeded your Google Maps API quota. Please check your usage limits."; - } - return "There was an error processing your request. Please try again."; - }; - return (
@@ -89,7 +80,7 @@ const GoogleMapsViewContent = () => {
- {response && ( + {/* Feedback message box */} + {feedback && feedback.message && (

- {response.status === "success" - ? response.message - : getErrorMessage(response.message)} + {feedback.message}

- {response.status === "success" && response.config && ( -
-

Command: {response.config.command}

- {response.config.args && ( -

Args: {response.config.args.join(" ")}

- )} -
- )}
)}
@@ -152,11 +134,7 @@ const GoogleMapsViewContent = () => { }; const GoogleMapsView = () => { - return ( - - - - ); + return ; }; export default GoogleMapsView; From 3b27475d710890f93a81674f404fcf3a1ff36e07 Mon Sep 17 00:00:00 2001 From: Soumyajit Mondal Date: Thu, 29 May 2025 15:02:39 +0530 Subject: [PATCH 34/35] fix: UI changes to make the service pages dynamic --- cortex_on/config/external_mcp_servers.json | 48 +++--- frontend/src/components/mcp/Sidebar.tsx | 17 +- .../components/mcp/services/ClaudeView.tsx | 139 ---------------- .../src/components/mcp/services/FigmaView.tsx | 140 ---------------- .../components/mcp/services/GithubView.tsx | 139 ---------------- .../mcp/services/GoogleMapsView.tsx | 140 ---------------- .../components/mcp/services/ServiceView.tsx | 156 ++++++++++++++++++ frontend/src/pages/MCP.tsx | 23 +-- 8 files changed, 186 insertions(+), 616 deletions(-) delete mode 100644 frontend/src/components/mcp/services/ClaudeView.tsx delete mode 100644 frontend/src/components/mcp/services/FigmaView.tsx delete mode 100644 frontend/src/components/mcp/services/GithubView.tsx delete mode 100644 frontend/src/components/mcp/services/GoogleMapsView.tsx create mode 100644 frontend/src/components/mcp/services/ServiceView.tsx diff --git a/cortex_on/config/external_mcp_servers.json b/cortex_on/config/external_mcp_servers.json index ba07964..7f4ef4a 100644 --- a/cortex_on/config/external_mcp_servers.json +++ b/cortex_on/config/external_mcp_servers.json @@ -1,24 +1,28 @@ { - "github": { - "command": "npx", - "args": [ - "-y", - "@modelcontextprotocol/server-github" - ], - "env": {}, - "description": "GitHub MCP server for repository operations", - "status": "disabled", - "secret_key": "GITHUB_PERSONAL_ACCESS_TOKEN" - }, - "google-maps": { - "command": "npx", - "args": [ - "-y", - "@modelcontextprotocol/server-google-maps" - ], - "env": {}, - "description": "Google Maps MCP server for geocoding, directions, and place search", - "status": "disabled", - "secret_key": "GOOGLE_MAPS_API_KEY" - } + "github": { + "command": "npx", + "args": [ + "-y", + "@modelcontextprotocol/server-github" + ], + "env": { + "GITHUB_PERSONAL_ACCESS_TOKEN": "hello" + }, + "description": "GitHub MCP server for repository operations", + "status": "enabled", + "secret_key": "GITHUB_PERSONAL_ACCESS_TOKEN" + }, + "google-maps": { + "command": "npx", + "args": [ + "-y", + "@modelcontextprotocol/server-google-maps" + ], + "env": { + "GOOGLE_MAPS_API_KEY": "hghg" + }, + "description": "Google Maps MCP server for geocoding, directions, and place search", + "status": "enabled", + "secret_key": "GOOGLE_MAPS_API_KEY" + } } \ No newline at end of file diff --git a/frontend/src/components/mcp/Sidebar.tsx b/frontend/src/components/mcp/Sidebar.tsx index 0749532..37456d3 100644 --- a/frontend/src/components/mcp/Sidebar.tsx +++ b/frontend/src/components/mcp/Sidebar.tsx @@ -11,19 +11,6 @@ interface Server { status: string; } -const getServiceIcon = (name: string) => { - switch(name.toLowerCase()) { - case 'github': - return Github; - case 'google-maps': - return Map; - case 'figma': - return Figma; - default: - return Bot; - } -}; - const Sidebar = ({ onServiceSelect }: SidebarProps) => { const [servers, setServers] = useState([]); const [loading, setLoading] = useState(true); @@ -47,7 +34,7 @@ const Sidebar = ({ onServiceSelect }: SidebarProps) => { return (
{servers.map((server) => { - const IconComponent = getServiceIcon(server.name); + const IconComponent = Bot; return ( ); })} diff --git a/frontend/src/components/mcp/services/ClaudeView.tsx b/frontend/src/components/mcp/services/ClaudeView.tsx deleted file mode 100644 index 105f1f6..0000000 --- a/frontend/src/components/mcp/services/ClaudeView.tsx +++ /dev/null @@ -1,139 +0,0 @@ -import { Input } from "@/components/ui/input"; -import { Button } from "@/components/ui/button"; -import { useState } from "react"; -import { Loader2 } from "lucide-react"; - -const ClaudeViewContent = () => { - const [apiKey, setApiKey] = useState(""); - const [isLoading, setIsLoading] = useState(false); - const [feedback, setFeedback] = useState<{ - status: "success" | "error"; - message: string; - } | null>(null); - - const handleToggle = async (action: "enable" | "disable") => { - if (!apiKey.trim()) { - setFeedback({ status: "error", message: "Please enter an API key." }); - return; - } - setIsLoading(true); - setFeedback(null); - try { - const response = await fetch( - "http://localhost:8081/agent/mcp/servers", - { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - server_name: "claude", - server_secret: apiKey, - action, - }), - } - ); - if (response.ok) { - setFeedback({ - status: "success", - message: - action === "enable" - ? "Claude API key enabled successfully!" - : "Claude API key disabled successfully!", - }); - } else { - setFeedback({ - status: "error", - message: "Failed to update Claude API key. Please try again.", - }); - } - } catch { - setFeedback({ - status: "error", - message: "Network error. Please try again.", - }); - } finally { - setIsLoading(false); - } - }; - - return ( -
-
-

Claude MCP

-

- Configure your Claude API key to enable Claude integration. - Create a key from your Anthropic account settings. -

-
- -
- setApiKey(e.target.value)} - /> -
- - -
-
- - {/* Feedback message box */} - {feedback && feedback.message && ( -
-

- {feedback.message} -

-
- )} -
- ); -}; - -const ClaudeView = () => { - return ; -}; - -export default ClaudeView; diff --git a/frontend/src/components/mcp/services/FigmaView.tsx b/frontend/src/components/mcp/services/FigmaView.tsx deleted file mode 100644 index 63bbee6..0000000 --- a/frontend/src/components/mcp/services/FigmaView.tsx +++ /dev/null @@ -1,140 +0,0 @@ -import { Input } from "@/components/ui/input"; -import { Button } from "@/components/ui/button"; -import { useState } from "react"; -import { Loader2 } from "lucide-react"; - -const FigmaViewContent = () => { - const [token, setToken] = useState(""); - const [isLoading, setIsLoading] = useState(false); - const [feedback, setFeedback] = useState<{ - status: "success" | "error"; - message: string; - } | null>(null); - - const handleToggle = async (action: "enable" | "disable") => { - if (!token.trim()) { - setFeedback({ status: "error", message: "Please enter a token." }); - return; - } - setIsLoading(true); - setFeedback(null); - try { - const response = await fetch( - "http://localhost:8081/agent/mcp/servers", - { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - server_name: "figma", - server_secret: token, - action, - }), - } - ); - if (response.ok) { - setFeedback({ - status: "success", - message: - action === "enable" - ? "Figma access token enabled successfully!" - : "Figma access token disabled successfully!", - }); - } else { - setFeedback({ - status: "error", - message: - "Failed to update Figma access token. Please try again.", - }); - } - } catch { - setFeedback({ - status: "error", - message: "Network error. Please try again.", - }); - } finally { - setIsLoading(false); - } - }; - - return ( -
-
-

Figma MCP

-

- Configure your Figma access token to enable Figma integration. - Create a token with file:read scope. -

-
- -
- setToken(e.target.value)} - /> -
- - -
-
- - {/* Feedback message box */} - {feedback && feedback.message && ( -
-

- {feedback.message} -

-
- )} -
- ); -}; - -const FigmaView = () => { - return ; -}; - -export default FigmaView; diff --git a/frontend/src/components/mcp/services/GithubView.tsx b/frontend/src/components/mcp/services/GithubView.tsx deleted file mode 100644 index 942d381..0000000 --- a/frontend/src/components/mcp/services/GithubView.tsx +++ /dev/null @@ -1,139 +0,0 @@ -import { Input } from "@/components/ui/input"; -import { Button } from "@/components/ui/button"; -import { useState } from "react"; -import { Loader2 } from "lucide-react"; - -const GithubViewContent = () => { - const [token, setToken] = useState(""); - const [isLoading, setIsLoading] = useState(false); - const [feedback, setFeedback] = useState<{ - status: "success" | "error"; - message: string; - } | null>(null); - - const handleToggle = async (action: "enable" | "disable") => { - if (!token.trim()) { - setFeedback({ status: "error", message: "Please enter a token." }); - return; - } - setIsLoading(true); - setFeedback(null); - try { - const response = await fetch( - "http://localhost:8081/agent/mcp/servers", - { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - server_name: "github", - server_secret: token, - action, - }), - } - ); - if (response.ok) { - setFeedback({ - status: "success", - message: - action === "enable" - ? "GitHub token enabled successfully!" - : "GitHub token disabled successfully!", - }); - } else { - setFeedback({ - status: "error", - message: "Failed to update GitHub token. Please try again.", - }); - } - } catch { - setFeedback({ - status: "error", - message: "Network error. Please try again.", - }); - } finally { - setIsLoading(false); - } - }; - - return ( -
-
-

GitHub MCP

-

- Configure your GitHub Personal Access Token to enable GitHub - integration. Create a token with repo and workflow scopes. -

-
- -
- setToken(e.target.value)} - /> -
- - -
-
- - {/* Feedback message box */} - {feedback && feedback.message && ( -
-

- {feedback.message} -

-
- )} -
- ); -}; - -const GithubView = () => { - return ; -}; - -export default GithubView; diff --git a/frontend/src/components/mcp/services/GoogleMapsView.tsx b/frontend/src/components/mcp/services/GoogleMapsView.tsx deleted file mode 100644 index 22ccec5..0000000 --- a/frontend/src/components/mcp/services/GoogleMapsView.tsx +++ /dev/null @@ -1,140 +0,0 @@ -import { Input } from "@/components/ui/input"; -import { Button } from "@/components/ui/button"; -import { useState } from "react"; -import { Loader2 } from "lucide-react"; - -const GoogleMapsViewContent = () => { - const [apiKey, setApiKey] = useState(""); - const [isLoading, setIsLoading] = useState(false); - const [feedback, setFeedback] = useState<{ - status: "success" | "error"; - message: string; - } | null>(null); - - const handleToggle = async (action: "enable" | "disable") => { - if (!apiKey.trim()) { - setFeedback({ status: "error", message: "Please enter an API key." }); - return; - } - setIsLoading(true); - setFeedback(null); - try { - const response = await fetch( - "http://localhost:8081/agent/mcp/servers", - { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - server_name: "google-maps", - server_secret: apiKey, - action, - }), - } - ); - if (response.ok) { - setFeedback({ - status: "success", - message: - action === "enable" - ? "Google Maps API key enabled successfully!" - : "Google Maps API key disabled successfully!", - }); - } else { - setFeedback({ - status: "error", - message: - "Failed to update Google Maps API key. Please try again.", - }); - } - } catch { - setFeedback({ - status: "error", - message: "Network error. Please try again.", - }); - } finally { - setIsLoading(false); - } - }; - - return ( -
-
-

Google Maps MCP

-

- Configure your Google Maps API key to enable Google Maps - integration. Create a key with Maps JavaScript API enabled. -

-
- -
- setApiKey(e.target.value)} - /> -
- - -
-
- - {/* Feedback message box */} - {feedback && feedback.message && ( -
-

- {feedback.message} -

-
- )} -
- ); -}; - -const GoogleMapsView = () => { - return ; -}; - -export default GoogleMapsView; diff --git a/frontend/src/components/mcp/services/ServiceView.tsx b/frontend/src/components/mcp/services/ServiceView.tsx new file mode 100644 index 0000000..436e563 --- /dev/null +++ b/frontend/src/components/mcp/services/ServiceView.tsx @@ -0,0 +1,156 @@ +import React from 'react'; +import { Input } from "@/components/ui/input"; +import { Button } from "@/components/ui/button"; +import DefaultView from "@/components/mcp/DefaultView"; +import { useState, useEffect } from "react"; +import { Loader2 } from "lucide-react"; + +interface ServiceViewProps { + service: string | null; +} + +const ServiceView: React.FC = ({ service }) => { + const [token, setToken] = useState(""); + const [isLoading, setIsLoading] = useState(false); + const [feedback, setFeedback] = useState<{ + status: "success" | "error"; + message: string; + } | null>(null); + + // Clear feedback message after 2 seconds whenever it changes + useEffect(() => { + if (feedback) { + const timer = setTimeout(() => { + setFeedback(null); + }, 2000); + + // Cleanup timeout on component unmount or when feedback changes + return () => clearTimeout(timer); + } + }, [feedback]); + + const handleToggle = async (action: "enable" | "disable") => { + if (!token.trim()) { + setFeedback({ status: "error", message: "Please enter a token." }); + return; + } + setIsLoading(true); + setFeedback(null); + try { + const response = await fetch( + "http://localhost:8081/agent/mcp/servers", + { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + server_name: service, + server_secret: token, + action, + }), + } + ); + if (response.ok) { + setFeedback({ + status: "success", + message: + action === "enable" + ? `${service.charAt(0).toUpperCase() + service.slice(1)} token enabled successfully!` + : `${service.charAt(0).toUpperCase() + service.slice(1)} token disabled successfully!`, + }); + } else { + setFeedback({ + status: "error", + message: `Failed to update ${service.charAt(0).toUpperCase() + service.slice(1)} token. Please try again.`, + }); + } + } catch { + setFeedback({ + status: "error", + message: "Network error. Please try again.", + }); + } finally { + setIsLoading(false); + } + }; + + if (!service) { + return ; + } + + return ( +
+
+

{service.charAt(0).toUpperCase() + service.slice(1)} MCP

+

+ Configure your {service.charAt(0).toUpperCase() + service.slice(1)} Personal Access Token to enable {service.charAt(0).toUpperCase() + service.slice(1)} mcp integration +

+
+ +
+ setToken(e.target.value)} + /> +
+ + +
+
+ + {/* Feedback message box */} + {feedback && feedback.message && ( +
+

+ {feedback.message} +

+
+ )} +
+ ); +}; + +export default ServiceView; \ No newline at end of file diff --git a/frontend/src/pages/MCP.tsx b/frontend/src/pages/MCP.tsx index fbe1a3e..5ce5d75 100644 --- a/frontend/src/pages/MCP.tsx +++ b/frontend/src/pages/MCP.tsx @@ -1,36 +1,17 @@ import { ScrollArea } from "@/components/ui/scroll-area"; import Sidebar from "@/components/mcp/Sidebar"; -import DefaultView from "@/components/mcp/DefaultView"; -import GithubView from "@/components/mcp/services/GithubView"; -import GoogleMapsView from "@/components/mcp/services/GoogleMapsView"; -import FigmaView from "@/components/mcp/services/FigmaView"; -import ClaudeView from "@/components/mcp/services/ClaudeView"; +import ServiceView from "@/components/mcp/services/ServiceView"; import { useState } from "react"; export const MCP = () => { const [selectedService, setSelectedService] = useState(null); - const renderContent = () => { - switch (selectedService) { - case "github": - return ; - case "google-maps": - return ; - case "figma": - return ; - case "claude": - return ; - default: - return ; - } - }; - return (
- {renderContent()} +
From 0263bd7e3203e9ebb719010a66dede7672274c72 Mon Sep 17 00:00:00 2001 From: Sakalya100 Date: Tue, 17 Jun 2025 13:08:16 +0530 Subject: [PATCH 35/35] (fix): Fixes for duplicate tool registration in MCP Server --- cortex_on/connect_to_external_server.py | 4 ++ cortex_on/instructor.py | 88 ++++++++++++++++++++++--- 2 files changed, 84 insertions(+), 8 deletions(-) diff --git a/cortex_on/connect_to_external_server.py b/cortex_on/connect_to_external_server.py index 35c6aee..6c2146b 100644 --- a/cortex_on/connect_to_external_server.py +++ b/cortex_on/connect_to_external_server.py @@ -88,6 +88,10 @@ async def load_servers(self) -> Tuple[List[MCPServerStdio], str]: print(f"Skipping {server_name} - no command specified") continue + if config['status'] == "disabled": + print(f"Skipping {server_name} - server is disabled") + continue + command = config['command'] args = config.get('args', []) env = config.get('env', {}) diff --git a/cortex_on/instructor.py b/cortex_on/instructor.py index 0fb8520..39ba608 100644 --- a/cortex_on/instructor.py +++ b/cortex_on/instructor.py @@ -21,6 +21,7 @@ from utils.stream_response_format import StreamResponse from agents.mcp_server import start_mcp_server, register_tools_for_main_mcp_server, server_manager, check_mcp_server_tools from connect_to_external_server import server_provider +from agents.orchestrator_agent import server as main_server load_dotenv() class DateTimeEncoder(json.JSONEncoder): @@ -123,6 +124,12 @@ def _reset_orchestrator_agent(self): # Keep only the main server (first one) and remove all external servers if len(orchestrator_agent._mcp_servers) > 1: main_server = orchestrator_agent._mcp_servers[0] + + # Log all servers that are being removed + for i, server in enumerate(orchestrator_agent._mcp_servers[1:], 1): + server_command = getattr(server, 'command', f'server_{i}') + logfire.info(f"Removing external MCP server: {server.__class__.__name__} with command: {server_command}") + orchestrator_agent._mcp_servers = [main_server] logfire.info("Reset orchestrator_agent MCP servers to just the main server") @@ -139,8 +146,23 @@ def _reset_orchestrator_agent(self): if hasattr(tool_manager, '_cached_tool_schemas'): tool_manager._cached_tool_schemas = None logfire.info(f"Cleared tool schema cache for server {server}") + + # Also clear any cached tools to ensure fresh registration + if hasattr(tool_manager, '_tools'): + # Don't clear main server tools, just log them + tool_count = len(tool_manager._tools) if tool_manager._tools else 0 + logfire.info(f"Server has {tool_count} registered tools") + except Exception as e: logfire.error(f"Error resetting orchestrator agent: {str(e)}") + # If reset fails, try more aggressive cleanup + try: + # Force reset to just the main server + if hasattr(orchestrator_agent, '_mcp_servers') and orchestrator_agent._mcp_servers: + orchestrator_agent._mcp_servers = orchestrator_agent._mcp_servers[:1] + logfire.info("Performed aggressive reset - kept only first server") + except Exception as cleanup_err: + logfire.error(f"Aggressive reset also failed: {str(cleanup_err)}") async def run(self, task: str, websocket: WebSocket, server_config: Optional[Dict[str, int]] = None) -> List[Dict[str, Any]]: """ @@ -151,8 +173,12 @@ async def run(self, task: str, websocket: WebSocket, server_config: Optional[Dic websocket: The active WebSocket connection server_config: Optional configuration for MCP servers {name: port} """ - # Reset the orchestrator agent to ensure we start fresh for each new chat - # self._reset_orchestrator_agent() + # Only reset if we have external servers to reset (i.e., this is not the first run) + if len(orchestrator_agent._mcp_servers) > 1: + logfire.info("Resetting orchestrator agent for new chat session (external servers detected)") + self._reset_orchestrator_agent() + else: + logfire.info("First run detected - skipping reset to allow initial server registration") self.websocket = websocket stream_output = StreamResponse( @@ -192,6 +218,20 @@ async def run(self, task: str, websocket: WebSocket, server_config: Optional[Dic # Start each configured external MCP server servers, system_prompt = await server_provider.load_servers() + logfire.info(f"Loaded {len(servers)} external servers from server_provider") + for i, server in enumerate(servers): + server_command = getattr(server, 'command', f'unknown_server_{i}') + logfire.info(f" Server {i}: {server.__class__.__name__} with command: {server_command}") + + # Verify we still have the main server after any operations + if not orchestrator_agent._mcp_servers: + logfire.error("No MCP servers found after reset - this should not happen!") + # Re-add the main server if somehow lost + orchestrator_agent._mcp_servers = [main_server] + logfire.info("Re-added main MCP server after unexpected loss") + + logfire.info(f"Starting server registration process. Current servers: {len(orchestrator_agent._mcp_servers)}, New external servers to process: {len(servers)}") + # Send status update for each server being loaded for i, server in enumerate(servers): server_name = server.command.split('/')[-1] if hasattr(server, 'command') else f"server_{i}" @@ -207,13 +247,45 @@ async def run(self, task: str, websocket: WebSocket, server_config: Optional[Dic main_server = orchestrator_agent._mcp_servers[0] check_mcp_server_tools(main_server, registered_tools) - # Now add each external server and check its tools + # Check if we already have external servers registered to avoid duplicates + existing_server_commands = set() + existing_server_ids = set() + + for existing_server in orchestrator_agent._mcp_servers[1:]: # Skip main server + if hasattr(existing_server, 'command'): + existing_server_commands.add(existing_server.command) + # Also track server object IDs to prevent adding the exact same object + existing_server_ids.add(id(existing_server)) + + logfire.info(f"Existing server commands: {existing_server_commands}") + logfire.info(f"Servers to register: {[getattr(s, 'command', str(s)) for s in servers]}") + + # Now add each external server only if it's not already registered + servers_added = 0 for server in servers: - # Check and deduplicate tools before adding - check_mcp_server_tools(server, registered_tools) - # Adding one at a time after checking - orchestrator_agent._mcp_servers.append(server) - logfire.info(f"Added MCP server: {server.__class__.__name__}") + server_command = getattr(server, 'command', str(server)) + server_id = id(server) + + logfire.info(f"Processing server with command: {server_command}, ID: {server_id}") + + # For the first run or when we have new servers, be more permissive + # Only skip if we find an exact command match AND it's the same object ID + should_skip = (server_command in existing_server_commands and + server_id in existing_server_ids) + + if not should_skip: + # Check and deduplicate tools before adding + check_mcp_server_tools(server, registered_tools) + # Adding one at a time after checking + orchestrator_agent._mcp_servers.append(server) + existing_server_commands.add(server_command) + existing_server_ids.add(server_id) + servers_added += 1 + logfire.info(f"✓ Added new MCP server: {server.__class__.__name__} with command: {server_command}") + else: + logfire.info(f"✗ Skipped duplicate MCP server with command: {server_command} (exact duplicate found)") + + logfire.info(f"Total MCP servers after registration: {len(orchestrator_agent._mcp_servers)} (added {servers_added} new servers)") # Properly integrate external server capabilities into the system prompt updated_system_prompt = orchestrator_system_prompt