diff --git a/api.py b/api.py index f326c39..fa82896 100755 --- a/api.py +++ b/api.py @@ -128,6 +128,12 @@ async def is_active(): logger.info("Is active endpoint called") return {"is_active": interaction.is_active} +@api.get("/stop") +async def stop(): + logger.info("Stop endpoint called") + interaction.current_agent.request_stop() + return JSONResponse(status_code=200, content={"status": "stopped"}) + @api.get("/latest_answer") async def get_latest_answer(): global query_resp_history @@ -138,6 +144,7 @@ async def get_latest_answer(): query_resp = { "done": "false", "answer": interaction.current_agent.last_answer, + "reasoning": interaction.current_agent.last_reasoning, "agent_name": interaction.current_agent.agent_name if interaction.current_agent else "None", "success": interaction.current_agent.success, "blocks": {f'{i}': block.jsonify() for i, block in enumerate(interaction.get_last_blocks_result())} if interaction.current_agent else {}, @@ -145,6 +152,7 @@ async def get_latest_answer(): "uid": uid } interaction.current_agent.last_answer = "" + interaction.current_agent.last_reasoning = "" query_resp_history.append(query_resp) return JSONResponse(status_code=200, content=query_resp) if query_resp_history: @@ -158,6 +166,7 @@ async def think_wrapper(interaction, query): success = await interaction.think() if not success: interaction.last_answer = "Error: No answer from agent" + interaction.last_reasoning = "Error: No reasoning from agent" interaction.last_success = False else: interaction.last_success = True @@ -166,7 +175,8 @@ async def think_wrapper(interaction, query): return success except Exception as e: logger.error(f"Error in think_wrapper: {str(e)}") - interaction.last_answer = f"Error: {str(e)}" + interaction.last_answer = f"" + interaction.last_reasoning = f"Error: {str(e)}" interaction.last_success = False raise e @@ -177,6 +187,7 @@ async def process_query(request: QueryRequest): query_resp = QueryResponse( done="false", answer="", + reasoning="", agent_name="Unknown", success="false", blocks={}, @@ -194,6 +205,7 @@ async def process_query(request: QueryRequest): if not success: query_resp.answer = interaction.last_answer + query_resp.reasoning = interaction.last_reasoning return JSONResponse(status_code=400, content=query_resp.jsonify()) if interaction.current_agent: @@ -208,6 +220,7 @@ async def process_query(request: QueryRequest): logger.info(f"Blocks: {blocks_json}") query_resp.done = "true" query_resp.answer = interaction.last_answer + query_resp.reasoning = interaction.last_reasoning query_resp.agent_name = interaction.current_agent.agent_name query_resp.success = str(interaction.last_success) query_resp.blocks = blocks_json diff --git a/frontend/agentic-seek-front/src/App.css b/frontend/agentic-seek-front/src/App.css index d9ac01c..b43c965 100644 --- a/frontend/agentic-seek-front/src/App.css +++ b/frontend/agentic-seek-front/src/App.css @@ -167,6 +167,88 @@ body { margin-bottom: 8px; } +/* Message header layout */ +.message-header { + display: flex; + flex-direction: column; + align-items: flex-start; + justify-content: space-between; + align-items: center; + margin-bottom: 8px; +} + +.reasoning-toggle { + background: rgba(255, 255, 255, 0.1); + border: 1px solid rgba(255, 255, 255, 0.2); + border-radius: 4px; + color: #fff; + padding: 4px 8px; + font-size: 12px; + cursor: pointer; + transition: all 0.2s ease; + display: flex; + align-items: center; + gap: 4px; + align-self: flex-start; +} + +.reasoning-toggle:hover { + background: rgba(255, 255, 255, 0.2); + border-color: rgba(255, 255, 255, 0.3); +} + +.reasoning-toggle:active { + transform: translateY(1px); +} + +/* Reasoning content container */ +.reasoning-content { + margin-top: 12px; + padding: 12px; + background: rgba(0, 0, 0, 0.2); + border-left: 3px solid rgba(255, 255, 255, 0.3); + border-radius: 0 4px 4px 0; + font-size: 0.9em; + line-height: 1.4; +} + +.reasoning-content h1, +.reasoning-content h2, +.reasoning-content h3, +.reasoning-content h4, +.reasoning-content h5, +.reasoning-content h6 { + font-size: 1em; + margin: 8px 0 4px 0; + color: rgba(255, 255, 255, 0.9); +} + +.reasoning-content p { + margin: 6px 0; + color: rgba(255, 255, 255, 0.8); +} + +/* Alternative light theme styles */ +.message.user-message .reasoning-toggle { + background: rgba(0, 0, 0, 0.05); + border-color: rgba(0, 0, 0, 0.1); + color: #333; +} + +.message.user-message .reasoning-toggle:hover { + background: rgba(0, 0, 0, 0.1); + border-color: rgba(0, 0, 0, 0.2); +} + +.message.user-message .reasoning-content { + background: rgba(0, 0, 0, 0.03); + border-left-color: rgba(0, 0, 0, 0.2); +} + +.message.user-message .reasoning-content p { + color: rgba(0, 0, 0, 0.7); +} + .placeholder { text-align: center; color: #64748b; /* lighter gray */ diff --git a/frontend/agentic-seek-front/src/App.js b/frontend/agentic-seek-front/src/App.js index 7521a70..c1e6ba8 100644 --- a/frontend/agentic-seek-front/src/App.js +++ b/frontend/agentic-seek-front/src/App.js @@ -13,6 +13,7 @@ function App() { const [responseData, setResponseData] = useState(null); const [isOnline, setIsOnline] = useState(false); const [status, setStatus] = useState('Agents ready'); + const [expandedReasoning, setExpandedReasoning] = useState(new Set()); const messagesEndRef = useRef(null); useEffect(() => { @@ -75,6 +76,18 @@ function App() { messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); }; + const toggleReasoning = (messageIndex) => { + setExpandedReasoning(prev => { + const newSet = new Set(prev); + if (newSet.has(messageIndex)) { + newSet.delete(messageIndex); + } else { + newSet.add(messageIndex); + } + return newSet; + }); + }; + const fetchLatestAnswer = async () => { try { const res = await axios.get('http://127.0.0.1:8000/latest_answer'); @@ -94,6 +107,7 @@ function App() { { type: 'agent', content: data.answer, + reasoning: data.reasoning, agentName: data.agent_name, status: data.status, uid: data.uid, @@ -121,6 +135,19 @@ function App() { })); }; + const handleStop = async (e) => { + e.preventDefault(); + checkHealth(); + setIsLoading(false); + setError(null); + try { + const res = await axios.get('http://127.0.0.1:8000/stop'); + setStatus("Requesting stop..."); + } catch (err) { + console.error('Error stopping the agent:', err); + } + } + const handleSubmit = async (e) => { e.preventDefault(); checkHealth(); @@ -172,8 +199,6 @@ function App() {
- -

Chat Interface

@@ -191,10 +216,28 @@ function App() { : 'error-message' }`} > - {msg.type === 'agent' && ( - {msg.agentName} - )} - {msg.content} +
+ {msg.type === 'agent' && ( + {msg.agentName} + )} + {msg.type === 'agent' && msg.reasoning && expandedReasoning.has(index) && ( +
+ {msg.reasoning} +
+ )} + {msg.type === 'agent' && ( + + )} +
+
+ {msg.content} +
)) )} @@ -213,6 +256,9 @@ function App() { +
@@ -277,4 +323,4 @@ function App() { ); } -export default App; +export default App; \ No newline at end of file diff --git a/sources/agents/agent.py b/sources/agents/agent.py index 5f49cec..0e6bfaf 100644 --- a/sources/agents/agent.py +++ b/sources/agents/agent.py @@ -44,7 +44,9 @@ class Agent(): self.blocks_result = [] self.success = True self.last_answer = "" + self.last_reasoning = "" self.status_message = "Haven't started yet" + self.stop = False self.verbose = verbose self.executor = ThreadPoolExecutor(max_workers=1) @@ -64,6 +66,10 @@ class Agent(): def get_last_answer(self) -> str: return self.last_answer + @property + def get_last_reasoning(self) -> str: + return self.last_reasoning + @property def get_blocks(self) -> list: return self.blocks_result @@ -114,6 +120,13 @@ class Agent(): except Exception as e: raise e + def request_stop(self) -> None: + """ + Request the agent to stop. + """ + self.stop = True + self.status_message = "Stopped" + @abstractmethod def process(self, prompt, speech_module) -> str: """ diff --git a/sources/agents/browser_agent.py b/sources/agents/browser_agent.py index 20dc7c9..3817fb3 100644 --- a/sources/agents/browser_agent.py +++ b/sources/agents/browser_agent.py @@ -181,6 +181,7 @@ class BrowserAgent(Agent): animate_thinking("Thinking...", color="status") self.memory.push('user', prompt) answer, reasoning = await self.llm_request() + self.last_reasoning = reasoning if show_reasoning: pretty_print(reasoning, color="failure") pretty_print(answer, color="output") @@ -349,11 +350,13 @@ class BrowserAgent(Agent): self.show_search_results(search_result) prompt = self.make_newsearch_prompt(user_prompt, search_result) unvisited = [None] - while not complete and len(unvisited) > 0: - + while not complete and len(unvisited) > 0 and not self.stop: self.memory.clear() unvisited = self.select_unvisited(search_result) answer, reasoning = await self.llm_decide(prompt, show_reasoning = False) + if self.stop: + pretty_print(f"Requested stop.", color="failure") + break if self.last_answer == answer: prompt = self.stuck_prompt(user_prompt, unvisited) continue @@ -424,4 +427,4 @@ class BrowserAgent(Agent): return answer, reasoning if __name__ == "__main__": - pass \ No newline at end of file + pass diff --git a/sources/agents/code_agent.py b/sources/agents/code_agent.py index f7ba3b9..f75baec 100644 --- a/sources/agents/code_agent.py +++ b/sources/agents/code_agent.py @@ -51,10 +51,12 @@ class CoderAgent(Agent): self.memory.push('user', prompt) clarify_trigger = "REQUEST_CLARIFICATION" - while attempt < max_attempts: + while attempt < max_attempts and not self.stop: + print("Stopped?", self.stop) animate_thinking("Thinking...", color="status") await self.wait_message(speech_module) answer, reasoning = await self.llm_request() + self.last_reasoning = reasoning if clarify_trigger in answer: self.last_answer = answer await asyncio.sleep(0) diff --git a/sources/agents/file_agent.py b/sources/agents/file_agent.py index 8dbd8e8..d88b898 100644 --- a/sources/agents/file_agent.py +++ b/sources/agents/file_agent.py @@ -28,10 +28,11 @@ class FileAgent(Agent): exec_success = False prompt += f"\nYou must work in directory: {self.work_dir}" self.memory.push('user', prompt) - while exec_success is False: + while exec_success is False and not self.stop: await self.wait_message(speech_module) animate_thinking("Thinking...", color="status") answer, reasoning = await self.llm_request() + self.last_reasoning = reasoning exec_success, _ = self.execute_modules(answer) answer = self.remove_blocks(answer) self.last_answer = answer diff --git a/sources/agents/planner_agent.py b/sources/agents/planner_agent.py index 7955e32..a588604 100644 --- a/sources/agents/planner_agent.py +++ b/sources/agents/planner_agent.py @@ -83,11 +83,15 @@ class PlannerAgent(Agent): self.logger.warning(f"Agent {task['agent']} does not exist.") pretty_print(f"Agent {task['agent']} does not exist.", color="warning") return [] - agent = { - 'agent': task['agent'], - 'id': task['id'], - 'task': task['task'] - } + try: + agent = { + 'agent': task['agent'], + 'id': task['id'], + 'task': task['task'] + } + except: + self.logger.warning("Missing field in json plan.") + return [] self.logger.info(f"Created agent {task['agent']} with task: {task['task']}") if 'need' in task: self.logger.info(f"Agent {task['agent']} was given info:\n {task['need']}") @@ -156,6 +160,7 @@ class PlannerAgent(Agent): return [] agents_tasks = self.parse_agent_tasks(answer) if agents_tasks == []: + self.show_plan(agents_tasks, answer) prompt = f"Failed to parse the tasks. Please write down your task followed by a json plan within ```json. Do not ask for clarification.\n" pretty_print("Failed to make plan. Retrying...", color="warning") continue @@ -178,7 +183,11 @@ class PlannerAgent(Agent): last_agent_work = agents_work_result[id] tool_success_str = "success" if success else "failure" pretty_print(f"Agent {id} work {tool_success_str}.", color="success" if success else "failure") - if int(id) == len(agents_tasks): + try: + id_int = int(id) + except Exception as e: + return agents_tasks + if id_int == len(agents_tasks): next_task = "No task follow, this was the last step. If it failed add a task to recover." else: next_task = f"Next task is: {agents_tasks[int(id)][0]}." @@ -221,8 +230,9 @@ class PlannerAgent(Agent): agent_prompt = self.make_prompt(task['task'], required_infos) pretty_print(f"Agent {task['agent']} started working...", color="status") self.logger.info(f"Agent {task['agent']} started working on {task['task']}.") - answer, _ = await self.agents[task['agent'].lower()].process(agent_prompt, None) + answer, reasoning = await self.agents[task['agent'].lower()].process(agent_prompt, None) self.last_answer = answer + self.last_reasoning = reasoning self.blocks_result = self.agents[task['agent'].lower()].blocks_result agent_answer = self.agents[task['agent'].lower()].raw_answer_blocks(answer) success = self.agents[task['agent'].lower()].get_success @@ -257,7 +267,7 @@ class PlannerAgent(Agent): return "Failed to parse the tasks.", "" i = 0 steps = len(agents_tasks) - while i < steps: + while i < steps and not self.stop: task_name, task = agents_tasks[i][0], agents_tasks[i][1] self.status_message = "Starting agents..." pretty_print(f"I will {task_name}.", color="info") @@ -271,6 +281,8 @@ class PlannerAgent(Agent): answer, success = await self.start_agent_process(task, required_infos) except Exception as e: raise e + if self.stop: + pretty_print(f"Requested stop.", color="failure") agents_work_result[task['id']] = answer agents_tasks = await self.update_plan(goal, agents_tasks, agents_work_result, task['id'], success) steps = len(agents_tasks) diff --git a/sources/browser.py b/sources/browser.py index e33f1df..3604e9b 100644 --- a/sources/browser.py +++ b/sources/browser.py @@ -206,7 +206,11 @@ class Browser: def setup_tabs(self): self.tabs = self.driver.window_handles - self.driver.get("https://www.google.com") + try: + self.driver.get("https://www.google.com") + except Exception as e: + self.logger.log(f"Failed to setup initial tab:" + str(e)) + pass self.screenshot() def switch_control_tab(self): @@ -215,7 +219,11 @@ class Browser: def load_anticatpcha_manually(self): pretty_print("You might want to install the AntiCaptcha extension for captchas.", color="warning") - self.driver.get(self.anticaptcha) + try: + self.driver.get(self.anticaptcha) + except Exception as e: + self.logger.log(f"Failed to setup initial tab:" + str(e)) + pass def human_move(element): actions = ActionChains(driver) @@ -685,15 +693,16 @@ class Browser: input_elements = self.driver.execute_script(script) if __name__ == "__main__": - driver = create_driver(headless=False, stealth_mode=True) - browser = Browser(driver, anticaptcha_manual_install=False) + driver = create_driver(headless=False, stealth_mode=True, crx_path="../crx/nopecha.crx") + browser = Browser(driver, anticaptcha_manual_install=True) input("press enter to continue") print("AntiCaptcha / Form Test") + browser.go_to("https://www.google.com/recaptcha/api2/demo") + time.sleep(50) browser.go_to("https://bot.sannysoft.com") time.sleep(5) #txt = browser.get_text() - #browser.go_to("https://www.google.com/recaptcha/api2/demo") browser.go_to("https://home.openweathermap.org/users/sign_up") inputs_visible = browser.get_form_inputs() print("inputs:", inputs_visible) diff --git a/sources/interaction.py b/sources/interaction.py index e878d5d..c5893d7 100644 --- a/sources/interaction.py +++ b/sources/interaction.py @@ -22,6 +22,7 @@ class Interaction: self.current_agent = None self.last_query = None self.last_answer = None + self.last_reasoning = None self.agents = agents self.tts_enabled = tts_enabled self.stt_enabled = stt_enabled @@ -158,7 +159,7 @@ class Interaction: tmp = self.last_answer self.current_agent = agent self.is_generating = True - self.last_answer, _ = await agent.process(self.last_query, self.speech) + self.last_answer, self.last_reasoning = await agent.process(self.last_query, self.speech) self.is_generating = False if push_last_agent_memory: self.current_agent.memory.push('user', self.last_query) diff --git a/sources/llm_provider.py b/sources/llm_provider.py index e59bcaa..7884bb5 100644 --- a/sources/llm_provider.py +++ b/sources/llm_provider.py @@ -58,6 +58,38 @@ class Provider: exit(1) return api_key + def anthropic_fn(self, history, verbose=False): + """ + Use Anthropic to generate text. + """ + from anthropic import Anthropic + + client = Anthropic(api_key=self.api_key) + system_message = None + messages = [] + for message in history: + clean_message = {'role': message['role'], 'content': message['content']} + if message['role'] == 'system': + system_message = message['content'] + else: + messages.append(clean_message) + + try: + response = client.messages.create( + model=self.model, + max_tokens=1024, + messages=messages, + system=system_message + ) + if response is None: + raise Exception("Anthropic response is empty.") + thought = response.content[0].text + if verbose: + print(thought) + return thought + except Exception as e: + raise Exception(f"Anthropic API error: {str(e)}") from e + def respond(self, history, verbose=True): """ Use the choosen provider to generate text. diff --git a/sources/schemas.py b/sources/schemas.py index 29410ba..81a6fda 100644 --- a/sources/schemas.py +++ b/sources/schemas.py @@ -19,6 +19,7 @@ class QueryRequest(BaseModel): class QueryResponse(BaseModel): done: str answer: str + reasoning: str agent_name: str success: str blocks: dict @@ -32,6 +33,7 @@ class QueryResponse(BaseModel): return { "done": self.done, "answer": self.answer, + "reasoning": self.reasoning, "agent_name": self.agent_name, "success": self.success, "blocks": self.blocks, diff --git a/sources/tools/BashInterpreter.py b/sources/tools/BashInterpreter.py index 9d56584..be70a4a 100644 --- a/sources/tools/BashInterpreter.py +++ b/sources/tools/BashInterpreter.py @@ -8,7 +8,7 @@ if __name__ == "__main__": # if running as a script for individual testing sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))) from sources.tools.tools import Tools -from sources.tools.safety import is_unsafe +from sources.tools.safety import is_any_unsafe class BashInterpreter(Tools): """ @@ -43,9 +43,9 @@ class BashInterpreter(Tools): for command in commands: command = f"cd {self.work_dir} && {command}" command = command.replace('\n', '') - if self.safe_mode and is_unsafe(commands): + if self.safe_mode and is_any_unsafe(commands): print(f"Unsafe command rejected: {command}") - return "Unsafe command detected, execution aborted." + return "\nUnsafe command: {command}. Execution aborted. This is beyond allowed capabilities report to user." if self.language_bash_attempt(command) and self.allow_language_exec_bash == False: continue try: @@ -100,6 +100,7 @@ class BashInterpreter(Tools): r"not permitted", r"not installed", r"not found", + r"aborted", r"no such", r"too many", r"too few", diff --git a/sources/tools/safety.py b/sources/tools/safety.py index 5009331..42485e7 100644 --- a/sources/tools/safety.py +++ b/sources/tools/safety.py @@ -66,6 +66,15 @@ unsafe_commands_windows = [ "bootcfg" ] +def is_any_unsafe(cmds): + """ + check if any bash command is unsafe. + """ + for cmd in cmds: + if is_unsafe(cmd): + return True + return False + def is_unsafe(cmd): """ check if a bash command is unsafe.