From 178f9ed624b000605252312e9a74d95bbff0dc0d Mon Sep 17 00:00:00 2001 From: YassWorks Date: Tue, 30 Dec 2025 23:17:12 +0100 Subject: [PATCH 01/43] Added openai-compatible provider options (like OpenRouter and Github Models) --- app/src/agents/general/config/system_prompt.md | 1 + app/src/core/base.py | 3 ++- app/src/core/create_base_agent.py | 12 ++++++++++-- config.json | 4 ++-- main.py | 3 ++- 5 files changed, 17 insertions(+), 6 deletions(-) diff --git a/app/src/agents/general/config/system_prompt.md b/app/src/agents/general/config/system_prompt.md index 85ab077..f12cafc 100644 --- a/app/src/agents/general/config/system_prompt.md +++ b/app/src/agents/general/config/system_prompt.md @@ -1,3 +1,4 @@ +Your name is Ally. You are a pragmatic, general‑purpose assistant running in a CLI tool focused on solving user problems end‑to‑end using the tools at your disposal. Your goals are to produce correct, useful results, ask for missing details when needed, and keep communication concise and friendly. Current date: {date} diff --git a/app/src/core/base.py b/app/src/core/base.py index e6997a7..bdbc4b7 100644 --- a/app/src/core/base.py +++ b/app/src/core/base.py @@ -197,6 +197,7 @@ def start_chat( ) self.model_name = self.prev_model_name self._handle_model_change(self.model_name) + self.prev_model_name = None else: if self.ui.confirm( UI_MESSAGES["confirmations"]["change_model"], default=True @@ -213,7 +214,7 @@ def start_chat( return True except PermissionDeniedException: - continue # Do nothing. Let the user enter a new input. + continue # Do nothing. Let the user enter a new prompt. except lg_errors.GraphRecursionError: self.ui.warning(UI_MESSAGES["warnings"]["recursion_limit_reached"]) diff --git a/app/src/core/create_base_agent.py b/app/src/core/create_base_agent.py index 861263a..ff71243 100644 --- a/app/src/core/create_base_agent.py +++ b/app/src/core/create_base_agent.py @@ -70,7 +70,7 @@ def create_base_agent( case "google": os.environ["GRPC_VERBOSITY"] = "NONE" os.environ["GRPC_CPP_VERBOSITY"] = "NONE" - + from langchain_google_genai import ChatGoogleGenerativeAI llm = ChatGoogleGenerativeAI( @@ -80,15 +80,23 @@ def create_base_agent( max_retries=5, google_api_key=api_key, ) - case "openai": + case "openai" | "openrouter" | "github": from langchain_openai import ChatOpenAI + if provider.lower() == "openai": + base_url = None + elif provider.lower() == "openrouter": + base_url = "https://openrouter.ai/api/v1" + else: # github + base_url = "https://models.github.ai/inference" + llm = ChatOpenAI( model=model_name, temperature=temperature, timeout=None, max_retries=5, api_key=api_key, + base_url=base_url, ) case "anthropic": from langchain_anthropic import ChatAnthropic diff --git a/config.json b/config.json index c23a3f8..d1787bd 100644 --- a/config.json +++ b/config.json @@ -1,5 +1,5 @@ { - "provider": "google", + "provider": "github", "provider_per_model": { "general": null, "code_gen": null, @@ -7,7 +7,7 @@ "web_searcher": null }, - "model": "gemini-2.5-flash", + "model": "gpt-4o", "models": { "general": null, "code_gen": null, diff --git a/main.py b/main.py index d099ffb..5267bb6 100644 --- a/main.py +++ b/main.py @@ -19,11 +19,12 @@ api_keys = { - "cerebras": os.getenv("CEREBRAS_API_KEY"), "openai": os.getenv("OPENAI_API_KEY"), "anthropic": os.getenv("ANTHROPIC_API_KEY"), "google": os.getenv("GOOGLE_GEN_AI_API_KEY"), + "openrouter": os.getenv("OPENROUTER_API_KEY"), + "github": os.getenv("GITHUB_AI_API_KEY"), } From 685972df2754b65033f189cbe6e18eeeb614088a Mon Sep 17 00:00:00 2001 From: YassWorks Date: Tue, 30 Dec 2025 23:40:58 +0100 Subject: [PATCH 02/43] fix: concurrent tool call permission selectors UI overlap (when model launches > 1 tool calls per AIMessage) --- app/src/core/base.py | 3 - app/src/core/create_base_agent.py | 101 ++++++++++++++++++++++++++++-- app/src/core/ui.py | 19 ++++++ app/utils/ui_messages.py | 3 + 4 files changed, 119 insertions(+), 7 deletions(-) diff --git a/app/src/core/base.py b/app/src/core/base.py index bdbc4b7..8534d1c 100644 --- a/app/src/core/base.py +++ b/app/src/core/base.py @@ -502,9 +502,6 @@ def _handle_dict_chunk(self, chunk: dict): def _handle_ai_message(self, message: AIMessage): """Handle AI message display.""" - if message.tool_calls: - for tool_call in message.tool_calls: - self.ui.tool_call(tool_call["name"], tool_call["args"]) if message.content and message.content.strip(): self.ui.ai_response(message.content) diff --git a/app/src/core/create_base_agent.py b/app/src/core/create_base_agent.py index ff71243..47e6eb5 100644 --- a/app/src/core/create_base_agent.py +++ b/app/src/core/create_base_agent.py @@ -1,12 +1,13 @@ from app.utils.constants import DEFAULT_PATHS, LAST_N_TURNS from app.src.helpers.valid_dir import validate_dir_name from app.src.core.ui import default_ui +from app.utils.ui_messages import UI_MESSAGES from langgraph.graph.state import CompiledStateGraph -from langchain_core.messages import BaseMessage, HumanMessage, AIMessage +from langchain_core.messages import BaseMessage, HumanMessage, AIMessage, ToolMessage from langgraph.graph.message import add_messages from langgraph.graph import StateGraph, END, START -from typing import TypedDict, Annotated -from langgraph.prebuilt import ToolNode, tools_condition +from typing import TypedDict, Annotated, Callable +from langgraph.prebuilt import tools_condition from langgraph.checkpoint.sqlite import SqliteSaver from langchain_core.prompts import ChatPromptTemplate from pathlib import Path @@ -20,6 +21,98 @@ class State(TypedDict): messages: Annotated[list[BaseMessage], add_messages] +class SequentialToolNode: + """A custom tool node that processes tool calls one at a time. + + This prevents UI collisions when multiple tools require permission + confirmation by processing them sequentially instead of in parallel. + """ + + def __init__(self, tools: list, handle_tool_errors: bool = False): + self.tools_by_name: dict[str, Callable] = {tool.name: tool for tool in tools} + self.handle_tool_errors = handle_tool_errors + + def __call__(self, state: State) -> dict: + """Process tool calls from the last AI message sequentially.""" + messages = state.get("messages", []) + if not messages: + return {"messages": []} + + # Find the last AI message with tool calls + last_ai_message = None + for msg in reversed(messages): + if isinstance(msg, AIMessage) and msg.tool_calls: + last_ai_message = msg + break + + if not last_ai_message or not last_ai_message.tool_calls: + return {"messages": []} + + tool_calls = last_ai_message.tool_calls + total_tools = len(tool_calls) + result_messages = [] + + # Show pending tools notification if more than 1 + if total_tools > 1: + default_ui.pending_tools(total_tools) + + # Process each tool call one at a time + for idx, tool_call in enumerate(tool_calls, start=1): + tool_name = tool_call["name"] + tool_args = tool_call["args"] + tool_call_id = tool_call["id"] + + # Show progress if multiple tools + if total_tools > 1: + default_ui.processing_tool(idx, total_tools, tool_name, tool_args) + + tool = self.tools_by_name.get(tool_name) + if tool is None: + result_messages.append( + ToolMessage( + content=f"Tool '{tool_name}' not found.", + name=tool_name, + tool_call_id=tool_call_id, + ) + ) + continue + + try: + # Execute the tool (this will trigger the permission check inside the tool) + result = tool.invoke(tool_args) + result_messages.append( + ToolMessage( + content=str(result), + name=tool_name, + tool_call_id=tool_call_id, + ) + ) + except Exception as e: + error_content = f"Error: {e}" + # Check if it's a permission denied exception + if "PermissionDeniedException" in type(e).__name__: + error_content = UI_MESSAGES["tool"]["permission_denied"].format( + tool_name + ) + + result_messages.append( + ToolMessage( + content=error_content, + name=tool_name, + tool_call_id=tool_call_id, + ) + ) + + # Re-raise if not handling errors and it's not a permission issue + if ( + not self.handle_tool_errors + and "PermissionDeniedException" not in type(e).__name__ + ): + raise + + return {"messages": result_messages} + + _PATH_ERROR_PRINTED = False @@ -129,7 +222,7 @@ def llm_node(state: State): context = build_llm_context(state["messages"]) return {"messages": [llm_chain.invoke({"messages": context})]} - tool_node = ToolNode(tools=tools, handle_tool_errors=False) + tool_node = SequentialToolNode(tools=tools, handle_tool_errors=False) graph.add_node("llm", llm_node) diff --git a/app/src/core/ui.py b/app/src/core/ui.py index ce29447..8646be6 100644 --- a/app/src/core/ui.py +++ b/app/src/core/ui.py @@ -105,6 +105,7 @@ def tool_output(self, tool_name: str, content: str): except: rendered_content = markdown_content + self.console.print() self.console.print( f"[{self._style('secondary')}]{UI_MESSAGES['tool']['tool_complete'].format(tool_name)}[/{self._style('secondary')}]" ) @@ -301,5 +302,23 @@ def error(self, error_msg: str): style="error", ) + def pending_tools(self, count: int): + """Display notification about pending tool calls.""" + self.status_message( + title=UI_MESSAGES["titles"]["info"], + message=UI_MESSAGES["tool"]["pending_tools"].format(count), + style="primary", + ) + + def processing_tool( + self, current: int, total: int, tool_name: str, tool_args: dict + ): + """Display progress for sequential tool processing.""" + self.console.print() + self.console.print( + f"[dim]{UI_MESSAGES['tool']['processing_tool'].format(current, total)}[/dim]" + ) + self.tool_call(tool_name, tool_args) + default_ui = AgentUI(Console(width=CONSOLE_WIDTH)) diff --git a/app/utils/ui_messages.py b/app/utils/ui_messages.py index 53691c6..0405b76 100644 --- a/app/utils/ui_messages.py +++ b/app/utils/ui_messages.py @@ -149,5 +149,8 @@ "output_header": "**Output:**", "truncated": "\n... *(truncated)*", "tool_complete": "Tool Complete: {}", + "pending_tools": "{} tool(s) pending approval", + "processing_tool": "Processing tool {} of {}", + "permission_denied": "Permission denied for tool '{}'", }, } From e95867675da4534202b7c05c72c4394ae5333161 Mon Sep 17 00:00:00 2001 From: YassWorks Date: Tue, 30 Dec 2025 23:52:31 +0100 Subject: [PATCH 03/43] README update --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 3868ac0..dc2f0bd 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,7 @@ Ally was built a fully local agentic system using **[Ollama](https://ollama.com/ - Anthropic - Google GenAI - Cerebras +- OpenAI-compatible providers (OpenRouter, GitHub Models) - _(more integrations on the way!)_ This tool is best suited for scenarios where privacy is paramount and agentic capabilities are needed in the workflow. @@ -37,7 +38,7 @@ A general-purpose agent that can: - Access the internet. - Execute commands and code. - **_Note:_** Tools always ask for your permission before executing. + **_Note:_** Tools always ask for your permission before executing. Multiple tool calls are processed sequentially to ensure clear approval flow. ### RAG @@ -214,7 +215,7 @@ This file (located at `Ally/`) controls Ally's main settings and integrations. } ``` -> **Note**: Docling is _heavy_. And requires lots of dependencies. It's recommended to go with the local install if you wish to use Docling. +> **Note**: Docling is _heavy_. And requires lots of dependencies. It's recommended to go with the local install if you wish to use Docling. > **Alternatively**, you could setup a volume (for the parsing and the embedding models) between your machine and the container so that models are persisted across sessions. See below for information where the models are stored inside the container by default. From 1db5e4003a2541153516022a60f5e1184484aebd Mon Sep 17 00:00:00 2001 From: YassWorks Date: Wed, 31 Dec 2025 00:01:28 +0100 Subject: [PATCH 04/43] refactor: refactored the choice selector using prompt_toolkit library instead of the old manual approach --- app/src/core/ui.py | 88 ++++++++++++++-------------------------------- 1 file changed, 27 insertions(+), 61 deletions(-) diff --git a/app/src/core/ui.py b/app/src/core/ui.py index 8646be6..aa7df2d 100644 --- a/app/src/core/ui.py +++ b/app/src/core/ui.py @@ -1,7 +1,7 @@ import os from rich.console import Console from app.utils.constants import CONSOLE_WIDTH -from prompt_toolkit.shortcuts import prompt +from prompt_toolkit.shortcuts import prompt, choice from prompt_toolkit.key_binding import KeyBindings from rich.markdown import Markdown from rich.prompt import Confirm @@ -14,13 +14,6 @@ import sys -if os.name == "nt": - import msvcrt -else: - import tty - import termios - - class AgentUI: def __init__(self, console: Console): @@ -117,6 +110,7 @@ def ai_response(self, content: str): except: rendered_content = content + self.console.print() panel = Panel( rendered_content, title=f"[bold]{UI_MESSAGES['titles']['assistant']}[/bold]", @@ -203,60 +197,32 @@ def confirm(self, message: str, default: bool = True) -> bool: ) return default - def get_key(self): - """Read a single key press and return a string identifier.""" - if os.name == "nt": - key = msvcrt.getch() - if key == b"\xe0": # Special keys (arrows, F keys, etc.) - key = msvcrt.getch() - return { - b"H": "UP", - b"P": "DOWN", - }.get(key, None) - elif key in (b"\r", b"\n"): - return "ENTER" - else: - fd = sys.stdin.fileno() - old_settings = termios.tcgetattr(fd) - try: - tty.setraw(fd) - ch1 = sys.stdin.read(1) - if ch1 == "\x1b": # Escape sequence - ch2 = sys.stdin.read(1) - if ch2 == "[": - ch3 = sys.stdin.read(1) - return { - "A": "UP", - "B": "DOWN", - }.get(ch3, None) - elif ch1 in ("\r", "\n"): - return "ENTER" - finally: - termios.tcsetattr(fd, termios.TCSADRAIN, old_settings) - return None - def select_option(self, message: str, options: list[str]) -> int: - idx = 0 - self.console.print(f"\n{message}") - for i, opt in enumerate(options): - prefix = "▶ " if i == idx else " " - print(f"{prefix}{opt}") - - while True: - key = self.get_key() - if key == "UP" and idx > 0: - idx -= 1 - elif key == "DOWN" and idx < len(options) - 1: - idx += 1 - elif key == "ENTER": - return idx - - # Move cursor up to menu start - sys.stdout.write(f"\033[{len(options)}A") - for i, opt in enumerate(options): - prefix = "▶ " if i == idx else " " - sys.stdout.write(f"{prefix}{opt}\033[K\n") - sys.stdout.flush() + """Display an interactive inline selection menu using arrow keys. + + Args: + message: The prompt message to display + options: List of option strings to choose from + + Returns: + The index of the selected option (0-based) + """ + # Create value tuples: (index, display_text) + # choice() returns the key (index), not the display text + values = [(i, opt) for i, opt in enumerate(options)] + + try: + result = choice( + message=message, + options=values, + ) + return result + except KeyboardInterrupt: + self.session_interrupted() + sys.exit(0) + except Exception: + # Fallback to first option on error + return 0 def goodbye(self): self.status_message( From 3185de3fbfcbbc4ed4efc6a1df7ee5ed871786e8 Mon Sep 17 00:00:00 2001 From: YassWorks Date: Wed, 31 Dec 2025 00:50:23 +0100 Subject: [PATCH 05/43] UI rework --- app/src/core/create_base_agent.py | 2 + app/src/core/ui.py | 386 +++++++++++++++++------------- 2 files changed, 217 insertions(+), 171 deletions(-) diff --git a/app/src/core/create_base_agent.py b/app/src/core/create_base_agent.py index 47e6eb5..cc222c9 100644 --- a/app/src/core/create_base_agent.py +++ b/app/src/core/create_base_agent.py @@ -65,6 +65,8 @@ def __call__(self, state: State) -> dict: # Show progress if multiple tools if total_tools > 1: default_ui.processing_tool(idx, total_tools, tool_name, tool_args) + + default_ui.tool_call(tool_name, tool_args) tool = self.tools_by_name.get(tool_name) if tool is None: diff --git a/app/src/core/ui.py b/app/src/core/ui.py index aa7df2d..b713f9d 100644 --- a/app/src/core/ui.py +++ b/app/src/core/ui.py @@ -1,132 +1,236 @@ -import os +import sys +import time from rich.console import Console -from app.utils.constants import CONSOLE_WIDTH -from prompt_toolkit.shortcuts import prompt, choice -from prompt_toolkit.key_binding import KeyBindings from rich.markdown import Markdown -from rich.prompt import Confirm from rich.panel import Panel from rich.text import Text +from rich import box +from prompt_toolkit.shortcuts import prompt, choice +from prompt_toolkit.key_binding import KeyBindings +from rich.prompt import Confirm from typing import Any -from app.utils.constants import THEME + +from app.utils.constants import CONSOLE_WIDTH, THEME from app.utils.ui_messages import UI_MESSAGES -import time -import sys class AgentUI: + """Minimal, modern CLI interface for Ally.""" def __init__(self, console: Console): self.console = console + self._tool_start_time: float | None = None def _style(self, color_key: str) -> str: return THEME.get(color_key, THEME["text"]) + def _format_duration(self, seconds: float) -> str: + """Format duration in human-readable form.""" + if seconds < 1: + return f"{seconds * 1000:.0f}ms" + elif seconds < 60: + return f"{seconds:.1f}s" + else: + mins = int(seconds // 60) + secs = seconds % 60 + return f"{mins}m {secs:.0f}s" + + # ───────────────────────────────────────────────────────────── + # Branding + # ───────────────────────────────────────────────────────────── + def logo(self, ascii_art: str): + """Display ASCII logo with gradient.""" lines = ascii_art.split("\n") n = max(len(lines) - 1, 1) for i, line in enumerate(lines): progress = max(0.0, (i - 1) / n) - - red = int(139 + (239 - 139) * progress) - green = int(92 + (68 - 92) * progress) - blue = int(246 + (68 - 246) * progress) - + # Gradient from soft purple to soft coral + red = int(124 + (248 - 124) * progress) + green = int(138 + (113 - 138) * progress) + blue = int(255 + (113 - 255) * progress) color = f"#{red:02x}{green:02x}{blue:02x}" - text = Text(line, style=f"bold {color}") - self.console.print(text) + self.console.print(Text(line, style=f"bold {color}")) def help(self, model_name: str = None): + """Display help information.""" help_content = UI_MESSAGES["help"]["content"].copy() - if model_name: help_content.append("") help_content.append(UI_MESSAGES["help"]["model_suffix"].format(model_name)) - help_content.append(UI_MESSAGES["help"]["footer"]) - markdown_content = Markdown("\n".join(help_content)) - panel = Panel( - markdown_content, - title=f"[bold]{UI_MESSAGES['titles']['help']}[/bold]", - border_style=self._style("muted"), - padding=(1, 2), - ) - self.console.print(panel) + self.console.print() + self.console.rule("[bold]Help[/bold]", style="dim") + self.console.print(Markdown("\n".join(help_content))) + self.console.print() + + # ───────────────────────────────────────────────────────────── + # Tool Execution Display + # ───────────────────────────────────────────────────────────── def tool_call(self, tool_name: str, args: dict[str, Any]): - tool_name = UI_MESSAGES["tool"]["title"].format(tool_name) - content_parts = [tool_name] + """Display tool being executed with minimal chrome.""" + self._tool_start_time = time.time() + + self.console.print() + self.console.print( + f"[{self._style('dim')}]>[/{self._style('dim')}] " + f"[bold {self._style('accent')}]{tool_name}[/bold {self._style('accent')}]" + ) + if args: - content_parts.append(UI_MESSAGES["tool"]["arguments_header"]) for k, v in args.items(): - value_str = str(v) - if len(value_str) > 100 or "\n" in value_str: - content_parts.append( - f"- **{k}:**\n```\n{value_str[:500]}{'...' if len(value_str) > 500 else ''}\n```" - ) - else: - content_parts.append(f"- **{k}:** `{value_str}`") + # Truncate long values + if len(value_str) > 80: + value_str = value_str[:77] + "..." + elif "\n" in value_str: + value_str = value_str.split("\n")[0][:60] + "..." + self.console.print( + f" [{self._style('dim')}]{k}:[/{self._style('dim')}] " + f"[{self._style('muted')}]{value_str}[/{self._style('muted')}]" + ) + + def tool_output(self, tool_name: str, content: str): + """Display tool completion with elapsed time.""" + elapsed = "" + if self._tool_start_time: + duration = time.time() - self._tool_start_time + elapsed = f" [{self._style('dim')}]{self._format_duration(duration)}[/{self._style('dim')}]" + self._tool_start_time = None + + # Truncate long output + if len(content) > 800: + content = content[:800] + "\n..." + + self.blank() + self.console.print( + f" [{self._style('success')}]Done[/{self._style('success')}]{elapsed}" + ) + + # Only show output if there's meaningful content + if content.strip() and content.strip() not in ["None", "null", ""]: + # Show output in a subtle code block style + for line in content.strip().split("\n")[:10]: # Max 10 lines + self.console.print( + f" [{self._style('muted')}]{line}[/{self._style('muted')}]" + ) + if content.strip().count("\n") > 10: + self.console.print( + f" [{self._style('dim')}]...[/{self._style('dim')}]" + ) + + # ───────────────────────────────────────────────────────────── + # AI Response + # ───────────────────────────────────────────────────────────── - markdown_content = "\n".join(content_parts) + def ai_response(self, content: str): + """Display AI response with minimal framing.""" + self.console.print() try: - rendered_content = Markdown(markdown_content) - except: - rendered_content = markdown_content + rendered = Markdown(content) + except Exception: + rendered = content panel = Panel( - rendered_content, - title=f"[bold]{UI_MESSAGES['titles']['tool_executing']}[/bold]", - border_style=self._style("accent"), + rendered, + box=box.ROUNDED, + border_style=self._style("border"), padding=(1, 2), + title=f"[{self._style('primary')}]Ally[/{self._style('primary')}]", + title_align="left", ) self.console.print(panel) - def tool_output(self, tool_name: str, content: str): - tool_name = f"{tool_name}" - if len(content) > 1000: - content = content[:1000] + UI_MESSAGES["tool"]["truncated"] + # ───────────────────────────────────────────────────────────── + # Status & Messages + # ───────────────────────────────────────────────────────────── - markdown_content = ( - f"{UI_MESSAGES['tool']['output_header']}\n```\n{content}\n```" + def status_message(self, title: str, message: str, style: str = "primary"): + """Display a simple inline status message.""" + prefix_map = { + "primary": ".", + "success": "+", + "warning": "!", + "error": "x", + } + prefix = prefix_map.get(style, ".") + self.console.print( + f"[{self._style(style)}]{prefix}[/{self._style(style)}] " + f"[bold]{title}[/bold] " + f"[{self._style('muted')}]{message}[/{self._style('muted')}]" ) - try: - rendered_content = Markdown(markdown_content) - except: - rendered_content = markdown_content + def info(self, message: str): + """Display informational message.""" + self.console.print(f"[{self._style('dim')}].[/{self._style('dim')}] {message}") + + def thinking(self, message: str = "Thinking"): + """Display a thinking/processing indicator.""" + self.console.print( + f"[{self._style('dim')}]...[/{self._style('dim')}] [{self._style('muted')}]{message}[/{self._style('muted')}]" + ) + def goodbye(self): + """Display goodbye message.""" self.console.print() self.console.print( - f"[{self._style('secondary')}]{UI_MESSAGES['tool']['tool_complete'].format(tool_name)}[/{self._style('secondary')}]" + f"[{self._style('dim')}].[/{self._style('dim')}] {UI_MESSAGES['messages']['goodbye']}" ) - self.console.print(rendered_content) + self.console.print() - def ai_response(self, content: str): - try: - rendered_content = Markdown(content) - except: - rendered_content = content + def history_cleared(self): + """Display history cleared confirmation.""" + self.status_message( + "Cleared", UI_MESSAGES["messages"]["history_cleared"], "success" + ) + def session_interrupted(self): + """Display session interrupted message.""" self.console.print() - panel = Panel( - rendered_content, - title=f"[bold]{UI_MESSAGES['titles']['assistant']}[/bold]", - border_style=self._style("primary"), - padding=(1, 2), + self.status_message( + "Interrupted", UI_MESSAGES["messages"]["session_interrupted"], "warning" ) - self.console.print(panel) - def status_message(self, title: str, message: str, style: str = "primary"): - panel = Panel( - message, - title=f"[bold]{title}[/bold]", - border_style=self._style(style), - padding=(0, 1), + def recursion_warning(self): + """Display warning about extended processing.""" + self.console.print() + self.console.print( + f"[{self._style('warning')}]![/{self._style('warning')}] " + f"[bold]Extended Session[/bold]" ) - self.console.print(panel) + self.console.print( + f" [{self._style('muted')}]{UI_MESSAGES['messages']['recursion_warning']}[/{self._style('muted')}]" + ) + self.console.print() + + def warning(self, warning_msg: str): + """Display warning message.""" + self.status_message("Warning", warning_msg, "warning") + + def error(self, error_msg: str): + """Display error message.""" + self.status_message("Error", error_msg, "error") + + def pending_tools(self, count: int): + """Display pending tool count.""" + self.blank() + self.info(f"{count} tool(s) queued") + + def processing_tool( + self, current: int, total: int, tool_name: str, tool_args: dict + ): + """Display tool processing progress.""" + self.console.print() + self.console.print( + f"[{self._style('dim')}][{current}/{total}][/{self._style('dim')}]" + ) + + # ───────────────────────────────────────────────────────────── + # User Input + # ───────────────────────────────────────────────────────────── def get_input( self, @@ -135,30 +239,28 @@ def get_input( cwd: str | None = None, model: str | None = None, ) -> str: + """Get user input with minimal prompt.""" try: - info_parts = [] + self.console.print() + + # Show context info on its own line + context_parts = [] if cwd: - info_parts.append(f"[dim]{cwd}[/dim]") + context_parts.append(cwd) if model: - info_parts.append(f"[dim]{model}[/dim]") - - info_line = " • ".join(info_parts) if info_parts else "" + context_parts.append(model) - prompt_content = message or "" - if default: - prompt_content += f" [dim](default: {default})[/dim]" - - if info_line: - prompt_content += ( - f"\n{info_line}" if prompt_content.strip() else info_line + if context_parts: + self.console.print( + f"[{self._style('dim')}]{' | '.join(context_parts)}[/{self._style('dim')}]" ) - panel = Panel( - prompt_content, border_style=self._style("border"), padding=(0, 1) - ) - self.console.print(panel) + if message: + self.console.print( + f"[{self._style('muted')}]{message}[/{self._style('muted')}]" + ) - # using prompt-toolkit for multiline support + # Multiline key bindings key_binds = KeyBindings() @key_binds.add("c-n") @@ -169,7 +271,7 @@ def _(event): def _(event): event.current_buffer.validate_and_handle() - result = prompt(">> ", multiline=True, key_bindings=key_binds) + result = prompt("> ", multiline=True, key_bindings=key_binds) return result.strip() if result else (default or "") except KeyboardInterrupt: @@ -180,111 +282,53 @@ def _(event): return default or "" def confirm(self, message: str, default: bool = True) -> bool: + """Get yes/no confirmation.""" try: - panel = Panel(message, border_style=self._style("warning"), padding=(0, 1)) - self.console.print(panel) + self.console.print() + self.console.print( + f"[{self._style('warning')}]?[/{self._style('warning')}] {message}" + ) return Confirm.ask( - ">>", default=default, console=self.console, show_default=False + ">", default=default, console=self.console, show_default=True ) except KeyboardInterrupt: self.session_interrupted() sys.exit(0) except Exception: self.warning( - UI_MESSAGES["warnings"]["failed_confirm"].format( - "y" if default else "n" - ) + f"Confirmation failed, using default: {'yes' if default else 'no'}" ) return default def select_option(self, message: str, options: list[str]) -> int: - """Display an interactive inline selection menu using arrow keys. - - Args: - message: The prompt message to display - options: List of option strings to choose from - - Returns: - The index of the selected option (0-based) - """ - # Create value tuples: (index, display_text) - # choice() returns the key (index), not the display text + """Display selection menu.""" values = [(i, opt) for i, opt in enumerate(options)] - try: - result = choice( - message=message, - options=values, - ) + self.console.print() + result = choice(message=message, options=values) return result except KeyboardInterrupt: self.session_interrupted() sys.exit(0) except Exception: - # Fallback to first option on error return 0 - def goodbye(self): - self.status_message( - title=UI_MESSAGES["titles"]["goodbye"], - message=UI_MESSAGES["messages"]["goodbye"], - style="primary", - ) + # ───────────────────────────────────────────────────────────── + # Utility + # ───────────────────────────────────────────────────────────── - def history_cleared(self): - self.status_message( - title=UI_MESSAGES["titles"]["history_cleared"], - message=UI_MESSAGES["messages"]["history_cleared"], - style="success", - ) - - def session_interrupted(self): - self.status_message( - title=UI_MESSAGES["titles"]["interrupted"], - message=UI_MESSAGES["messages"]["session_interrupted"], - style="warning", - ) - - def recursion_warning(self): - panel = Panel( - UI_MESSAGES["messages"]["recursion_warning"], - title=f"[bold]{UI_MESSAGES['titles']['extended_session']}[/bold]", - border_style=self._style("warning"), - padding=(1, 2), - ) - self.console.print(panel) - - def warning(self, warning_msg: str): - self.status_message( - title=UI_MESSAGES["titles"]["warning"], - message=f"{warning_msg}", - style="warning", - ) - - def error(self, error_msg: str): - self.status_message( - title=UI_MESSAGES["titles"]["error"], - message=f"{error_msg}", - style="error", - ) - - def pending_tools(self, count: int): - """Display notification about pending tool calls.""" - self.status_message( - title=UI_MESSAGES["titles"]["info"], - message=UI_MESSAGES["tool"]["pending_tools"].format(count), - style="primary", - ) + def divider(self, title: str = None): + """Print a subtle divider line.""" + if title: + self.console.rule( + f"[{self._style('dim')}]{title}[/{self._style('dim')}]", style="dim" + ) + else: + self.console.rule(style="dim") - def processing_tool( - self, current: int, total: int, tool_name: str, tool_args: dict - ): - """Display progress for sequential tool processing.""" + def blank(self): + """Print a blank line.""" self.console.print() - self.console.print( - f"[dim]{UI_MESSAGES['tool']['processing_tool'].format(current, total)}[/dim]" - ) - self.tool_call(tool_name, tool_args) default_ui = AgentUI(Console(width=CONSOLE_WIDTH)) From 25183c259d0b3602f8eea4fb6fdc800f85b5f628 Mon Sep 17 00:00:00 2001 From: YassWorks Date: Wed, 31 Dec 2025 01:21:10 +0100 Subject: [PATCH 06/43] Refactor CLI class for improved readability and consistency --- app/src/cli/cli.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/app/src/cli/cli.py b/app/src/cli/cli.py index 27248ff..1d0660d 100644 --- a/app/src/cli/cli.py +++ b/app/src/cli/cli.py @@ -70,26 +70,30 @@ def __init__( self.embedding_function = OpenAIEmbedder(embedding_model).get_embeddings self.rag_available = True - + case "nlpcloud" | "nlp cloud" | "nlp_cloud": from app.src.embeddings.embedding_functions.nlp_cloud_embed import ( NLPCloudEmbedder, ) - self.embedding_function = NLPCloudEmbedder(embedding_model).get_embeddings + self.embedding_function = NLPCloudEmbedder( + embedding_model + ).get_embeddings self.rag_available = True case _: self.embedding_function = None self.rag_available = False - + match scraping_method.lower(): case "docling": from app.src.embeddings.scrapers.docling_scraper import DoclingScraper self.scraper = DoclingScraper() - case _: # default to simple scraper, even for unrecognized methods as it doesn't matter really. + case ( + _ + ): # default to simple scraper, even for unrecognized methods as it doesn't matter really. from app.src.embeddings.scrapers.simple_scraper import SimpleScraper self.scraper = SimpleScraper() From 732827d0a9f9f371a6760f26a31d9af1a9994365 Mon Sep 17 00:00:00 2001 From: YassWorks Date: Wed, 31 Dec 2025 01:21:11 +0100 Subject: [PATCH 07/43] Clarify important working directory note and improve note handling in CLI --- app/src/cli/cli.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/app/src/cli/cli.py b/app/src/cli/cli.py index 1d0660d..c2a9cd5 100644 --- a/app/src/cli/cli.py +++ b/app/src/cli/cli.py @@ -232,9 +232,12 @@ def start_chat(self, *args): active_dir, initial_prompt, thread_id = self._setup_environment(args) self.ui.logo(ASCII_ART) - self.ui.help() - wd_note = f"## Important:\nAlways place your work inside {active_dir} unless stated otherwise by the user.\n" + wd_note = ( + f"## Important Note:\n" + f"Always place your work inside {active_dir} unless stated otherwise by the user.\n" + f"Don't report this note to the user, just follow it." + ) # making the project generation a command for the general agent (an extra option for the user) self.general_agent.register_command( From 7b59817fd4b05c8232ddebb402fb95caca6c95ff Mon Sep 17 00:00:00 2001 From: YassWorks Date: Wed, 31 Dec 2025 01:21:11 +0100 Subject: [PATCH 08/43] Refactor DataBaseClient instantiation for readability --- app/src/cli/cli.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/src/cli/cli.py b/app/src/cli/cli.py index c2a9cd5..4833176 100644 --- a/app/src/cli/cli.py +++ b/app/src/cli/cli.py @@ -248,7 +248,9 @@ def start_chat(self, *args): if self.rag_available: from app.src.embeddings.db_client import DataBaseClient - _ = DataBaseClient(embedding_function=self.embedding_function, scraper=self.scraper) + _ = DataBaseClient( + embedding_function=self.embedding_function, scraper=self.scraper + ) self._integrate_rag(agent=self.general_agent, available=self.rag_available) From 7f93d93435337cdfa6f437d3fded6d3f0f38a6ca Mon Sep 17 00:00:00 2001 From: YassWorks Date: Wed, 31 Dec 2025 01:21:11 +0100 Subject: [PATCH 09/43] Condense warning and error message display logic in CLI --- app/src/cli/cli.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/app/src/cli/cli.py b/app/src/cli/cli.py index 4833176..5df27e5 100644 --- a/app/src/cli/cli.py +++ b/app/src/cli/cli.py @@ -302,9 +302,7 @@ def _integrate_rag(self, agent: BaseAgent, available: bool): def _enable_rag(self, agent: BaseAgent): """Enable RAG functionality.""" if not self.rag_available: - self.ui.warning( - UI_MESSAGES["warnings"]["rag_not_available"] - ) + self.ui.warning(UI_MESSAGES["warnings"]["rag_not_available"]) return agent._toggle_rag(enable=True) self.ui.status_message( @@ -338,9 +336,7 @@ def launch_coding_units(self, initial_prompt: str = None, active_dir: str = None ) if not codegen_unit_success: - self.ui.error( - UI_MESSAGES["errors"]["codegen_failed"] - ) + self.ui.error(UI_MESSAGES["errors"]["codegen_failed"]) except KeyboardInterrupt: self.ui.goodbye() From 805a98e344a5d278566d8365bb59c2145120c492 Mon Sep 17 00:00:00 2001 From: YassWorks Date: Wed, 31 Dec 2025 01:21:11 +0100 Subject: [PATCH 10/43] Improve invalid directory error handling formatting in CLI --- app/src/cli/cli.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/src/cli/cli.py b/app/src/cli/cli.py index 5df27e5..d6607f2 100644 --- a/app/src/cli/cli.py +++ b/app/src/cli/cli.py @@ -363,7 +363,9 @@ def _setup_environment(self, user_args: list[str] = None) -> tuple[str, str, str if parsed_args.d == ".": parsed_args.d = os.getcwd() elif not validate_dir_name(parsed_args.d): - self.ui.error(UI_MESSAGES["errors"]["invalid_directory"].format(parsed_args.d)) + self.ui.error( + UI_MESSAGES["errors"]["invalid_directory"].format(parsed_args.d) + ) sys.exit(1) active_dir = parsed_args.d From 0ce86e0d4da41f0cbf6c0ef5d9dadc3e7e8094eb Mon Sep 17 00:00:00 2001 From: YassWorks Date: Wed, 31 Dec 2025 01:21:11 +0100 Subject: [PATCH 11/43] Redesign help panel and update input prompt style in AgentUI --- app/src/core/ui.py | 44 +++++++++++++++++++++++++++----------------- 1 file changed, 27 insertions(+), 17 deletions(-) diff --git a/app/src/core/ui.py b/app/src/core/ui.py index b713f9d..9e9b2b4 100644 --- a/app/src/core/ui.py +++ b/app/src/core/ui.py @@ -58,11 +58,16 @@ def help(self, model_name: str = None): if model_name: help_content.append("") help_content.append(UI_MESSAGES["help"]["model_suffix"].format(model_name)) - help_content.append(UI_MESSAGES["help"]["footer"]) + markdown_content = Markdown("\n".join(help_content)) + panel = Panel( + markdown_content, + title=f"[bold]Help[/bold]", + border_style=self._style("primary"), + padding=(0, 1), + ) self.console.print() - self.console.rule("[bold]Help[/bold]", style="dim") - self.console.print(Markdown("\n".join(help_content))) + self.console.print(panel) self.console.print() # ───────────────────────────────────────────────────────────── @@ -239,27 +244,32 @@ def get_input( cwd: str | None = None, model: str | None = None, ) -> str: - """Get user input with minimal prompt.""" + """Get user input with terminal-style prompt.""" try: self.console.print() - # Show context info on its own line - context_parts = [] - if cwd: - context_parts.append(cwd) - if model: - context_parts.append(model) - - if context_parts: - self.console.print( - f"[{self._style('dim')}]{' | '.join(context_parts)}[/{self._style('dim')}]" - ) - if message: self.console.print( f"[{self._style('muted')}]{message}[/{self._style('muted')}]" ) + # Extract just the folder name from cwd + import os + + folder_name = "~" + if cwd: + folder_name = os.path.basename(cwd.rstrip(os.sep)) or "~" + + # Terminal-style prompt: > foldername$ + from prompt_toolkit.formatted_text import ANSI + + # Build the prefix with ANSI color codes + prompt_text = ( + f"\033[38;2;16;185;129m>\033[0m " + f"\033[38;2;6;182;212m {folder_name}\033[0m" + f"\033[38;2;16;185;129m$\033[0m " + ) + # Multiline key bindings key_binds = KeyBindings() @@ -271,7 +281,7 @@ def _(event): def _(event): event.current_buffer.validate_and_handle() - result = prompt("> ", multiline=True, key_bindings=key_binds) + result = prompt(ANSI(prompt_text), multiline=True, key_bindings=key_binds) return result.strip() if result else (default or "") except KeyboardInterrupt: From 020586d6c06f58c1e8e31bb1608472f9bd5d2a5f Mon Sep 17 00:00:00 2001 From: YassWorks Date: Wed, 31 Dec 2025 01:21:11 +0100 Subject: [PATCH 12/43] Minor code style updates in AgentUI --- app/src/core/ui.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/core/ui.py b/app/src/core/ui.py index 9e9b2b4..827f4e9 100644 --- a/app/src/core/ui.py +++ b/app/src/core/ui.py @@ -232,7 +232,7 @@ def processing_tool( self.console.print( f"[{self._style('dim')}][{current}/{total}][/{self._style('dim')}]" ) - + # ───────────────────────────────────────────────────────────── # User Input # ───────────────────────────────────────────────────────────── From 52f3b03ccc3a6badc352f12f9f98b0e8fbe8a4ac Mon Sep 17 00:00:00 2001 From: YassWorks Date: Wed, 31 Dec 2025 01:21:11 +0100 Subject: [PATCH 13/43] Revamp color theme for vibrant unified appearance --- app/utils/constants.py | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/app/utils/constants.py b/app/utils/constants.py index e59811b..58ddb3a 100644 --- a/app/utils/constants.py +++ b/app/utils/constants.py @@ -11,16 +11,18 @@ LAST_N_TURNS = 20 +# Vibrant unified theme built around purple accent THEME = { - "primary": "#4566db", - "secondary": "#9c79ee", - "accent": "#88c5d0", - "success": "#10b981", - "warning": "#ebac40", - "error": "#ef4444", - "muted": "#6b7280", - "text": "#f8fafc", - "border": "#374151", + "primary": "#a855f7", # Vibrant purple (main accent) + "secondary": "#ec4899", # Vibrant pink (complementary) + "accent": "#06b6d4", # Vibrant cyan (tertiary) + "success": "#10b981", # Vibrant emerald + "warning": "#f59e0b", # Vibrant amber + "error": "#ef4444", # Vibrant red + "muted": "#9ca3af", # Neutral gray + "dim": "#6b7280", # Dim gray + "text": "#f3f4f6", # Light gray + "border": "#6b21a8", # Deep purple border } PROMPTS = { From 6f6d31173d901430e709dcb3ca42e5643b8fd25c Mon Sep 17 00:00:00 2001 From: YassWorks Date: Wed, 31 Dec 2025 01:21:12 +0100 Subject: [PATCH 14/43] Revise help message content and formatting --- app/utils/ui_messages.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/app/utils/ui_messages.py b/app/utils/ui_messages.py index 0405b76..15ff2dd 100644 --- a/app/utils/ui_messages.py +++ b/app/utils/ui_messages.py @@ -129,18 +129,18 @@ # Help Content "help": { "content": [ - "Use Ctrl+n to enter a new line and Enter to submit your message.", + "Use `Ctrl+n` to enter a new line and Enter to submit your message.", "| Command | Description |", "|---------|-------------|", "| /quit, /exit, /q | Exit |", - "| /clear | Clear history* |", + "| /clear | Clear history |", "| /cls | Clear screen |", - "| /model (change) | Show/change AI model |", + "| /model | Show current AI model |", + "| /model change `name` | change AI model |", "| /project | Start project generation workflow |", "| /help, /h | Show this help message |", ], - "model_suffix": "Model: *{}*", - "footer": "\n> **Not recommended during long running tasks. Use at your own risk.*", + "model_suffix": "Model: **{}**", }, # Tool Messages "tool": { From c08cbd162386bcf847039ecc170dd8fad1c5aa2b Mon Sep 17 00:00:00 2001 From: YassWorks Date: Wed, 31 Dec 2025 12:11:43 +0100 Subject: [PATCH 15/43] removed extra line --- app/src/core/ui.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/src/core/ui.py b/app/src/core/ui.py index 827f4e9..3292d2b 100644 --- a/app/src/core/ui.py +++ b/app/src/core/ui.py @@ -246,8 +246,6 @@ def get_input( ) -> str: """Get user input with terminal-style prompt.""" try: - self.console.print() - if message: self.console.print( f"[{self._style('muted')}]{message}[/{self._style('muted')}]" From 9e970db3601950cecbef19ada6dafbfa2bbdcf46 Mon Sep 17 00:00:00 2001 From: YassWorks Date: Wed, 31 Dec 2025 12:56:53 +0100 Subject: [PATCH 16/43] fix: thinking block leak + new welcome message --- app/src/cli/cli.py | 1 + app/src/core/base.py | 10 +++++----- app/src/core/create_base_agent.py | 2 +- app/src/core/ui.py | 15 +++++++++++++++ app/utils/ui_messages.py | 2 ++ config.json | 4 ++-- 6 files changed, 26 insertions(+), 8 deletions(-) diff --git a/app/src/cli/cli.py b/app/src/cli/cli.py index d6607f2..d51989c 100644 --- a/app/src/cli/cli.py +++ b/app/src/cli/cli.py @@ -232,6 +232,7 @@ def start_chat(self, *args): active_dir, initial_prompt, thread_id = self._setup_environment(args) self.ui.logo(ASCII_ART) + self.ui.welcome() wd_note = ( f"## Important Note:\n" diff --git a/app/src/core/base.py b/app/src/core/base.py index 8534d1c..e3fcab2 100644 --- a/app/src/core/base.py +++ b/app/src/core/base.py @@ -13,6 +13,7 @@ from app.utils.ui_messages import UI_MESSAGES import uuid import os +import re import openai @@ -472,10 +473,7 @@ def _extract_response_content(self, raw_response: dict) -> str: def _remove_thinking_block(self, content: str) -> str: """Remove thinking block from response content.""" - think_end = content.find("") - if think_end != -1: - return content[think_end + len("") :].strip() - return content + return re.sub(r".*?\s*", "", content, flags=re.DOTALL) def _display_chunk(self, chunk: BaseMessage | dict): """Display chunk content in the UI.""" @@ -503,7 +501,9 @@ def _handle_dict_chunk(self, chunk: dict): def _handle_ai_message(self, message: AIMessage): """Handle AI message display.""" if message.content and message.content.strip(): - self.ui.ai_response(message.content) + # remove thinking blocks before displaying + content = self._remove_thinking_block(message.content) + self.ui.ai_response(content) def _handle_tool_message(self, message: ToolMessage): """Handle tool message display.""" diff --git a/app/src/core/create_base_agent.py b/app/src/core/create_base_agent.py index cc222c9..275305d 100644 --- a/app/src/core/create_base_agent.py +++ b/app/src/core/create_base_agent.py @@ -65,7 +65,7 @@ def __call__(self, state: State) -> dict: # Show progress if multiple tools if total_tools > 1: default_ui.processing_tool(idx, total_tools, tool_name, tool_args) - + default_ui.tool_call(tool_name, tool_args) tool = self.tools_by_name.get(tool_name) diff --git a/app/src/core/ui.py b/app/src/core/ui.py index 3292d2b..3a98f92 100644 --- a/app/src/core/ui.py +++ b/app/src/core/ui.py @@ -70,6 +70,21 @@ def help(self, model_name: str = None): self.console.print(panel) self.console.print() + def welcome(self): + """Display a brief welcome message.""" + parts = UI_MESSAGES["messages"]["welcome"].split("Ally") + welcome_text = Text() + welcome_text.append(parts[0], style=self._style("text")) + welcome_text.append("Ally", style=f"bold {self._style('primary')}") + welcome_text.append(parts[1] + "\n", style=self._style("text")) + + help_msg = UI_MESSAGES["messages"]["welcome_help_hint"].split("/help") + welcome_text.append(help_msg[0], style=self._style("muted")) + welcome_text.append("/help", style=f"bold {self._style('accent')}") + welcome_text.append(help_msg[1], style=self._style("muted")) + self.console.print(welcome_text) + self.console.print() + # ───────────────────────────────────────────────────────────── # Tool Execution Display # ───────────────────────────────────────────────────────────── diff --git a/app/utils/ui_messages.py b/app/utils/ui_messages.py index 15ff2dd..9bc9c29 100644 --- a/app/utils/ui_messages.py +++ b/app/utils/ui_messages.py @@ -48,6 +48,8 @@ "session_interrupted": "Session interrupted by user", "recursion_warning": "Agent has been processing for a while.\nContinue or refine your prompt?", "initializing_agents": "Initializing agents...", + "welcome": "Welcome to Ally - Your private AI assistant", + "welcome_help_hint": "Type /help for commands and usage", "working_on_task": "Working on the task...", "generation_starting_detail": "The CodeGen Agent is now generating code based on the context provided.", "brainstormer_ready_detail": "Type '/exit' or press Ctrl+C to continue to code generation", diff --git a/config.json b/config.json index d1787bd..0e12537 100644 --- a/config.json +++ b/config.json @@ -1,5 +1,5 @@ { - "provider": "github", + "provider": "cerebras", "provider_per_model": { "general": null, "code_gen": null, @@ -7,7 +7,7 @@ "web_searcher": null }, - "model": "gpt-4o", + "model": "zai-glm-4.6", "models": { "general": null, "code_gen": null, From 326b2439b5ff5f3a8ad78c1eb25ea7019ef61d2a Mon Sep 17 00:00:00 2001 From: YassWorks Date: Thu, 1 Jan 2026 01:43:08 +0100 Subject: [PATCH 17/43] feat: add Groq LLM provider support --- app/src/core/create_base_agent.py | 10 ++++++++++ main.py | 1 + requirements.txt | 28 +++++++++++++++++++++------- 3 files changed, 32 insertions(+), 7 deletions(-) diff --git a/app/src/core/create_base_agent.py b/app/src/core/create_base_agent.py index 275305d..f21f83e 100644 --- a/app/src/core/create_base_agent.py +++ b/app/src/core/create_base_agent.py @@ -143,6 +143,16 @@ def create_base_agent( llm = None match provider.lower(): + case "groq": + from langchain_groq import ChatGroq + + llm = ChatGroq( + model=model_name, + temperature=temperature, + timeout=None, + max_retries=5, + api_key=api_key, + ) case "cerebras": from langchain_cerebras import ChatCerebras diff --git a/main.py b/main.py index 5267bb6..503f95b 100644 --- a/main.py +++ b/main.py @@ -25,6 +25,7 @@ "google": os.getenv("GOOGLE_GEN_AI_API_KEY"), "openrouter": os.getenv("OPENROUTER_API_KEY"), "github": os.getenv("GITHUB_AI_API_KEY"), + "groq": os.getenv("GROQ_API_KEY"), } diff --git a/requirements.txt b/requirements.txt index 8a8597c..47b37d7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,15 +1,29 @@ -openai==2.8.1 -beautifulsoup4==4.14.2 -google-api-python-client==2.187.0 +# AI/LLM Providers +openai==2.14.0 +ollama==0.6.1 +langchain-anthropic==0.3.22 langchain-cerebras==0.6.0 -langchain-core==0.3.80 +langchain-groq==0.3.8 langchain-google-genai==2.1.12 langchain-ollama==0.3.10 -langchain-anthropic==0.3.22 + +# LangChain Framework langgraph==1.0.1 -langgraph-checkpoint-sqlite==3.0.0 +langgraph-checkpoint-sqlite==3.0.1 + +# Web Scraping & Parsing +beautifulsoup4==4.14.3 lxml==6.0.2 -ollama==0.6.1 + +# Document Processing +python-docx==1.2.0 +pymupdf4llm==0.2.7 +pymupdf-layout==1.26.6 + +# Google API +google-api-python-client==2.187.0 + +# Utilities & CLI prompt-toolkit==3.0.52 python-dotenv==1.2.1 PyYAML==6.0.3 From ae12da2aba79eade20dafdfeede7410f983ddde0 Mon Sep 17 00:00:00 2001 From: YassWorks Date: Thu, 1 Jan 2026 01:43:09 +0100 Subject: [PATCH 18/43] fix(cli): skip API key/model validation for Ollama provider --- app/src/cli/cli.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/app/src/cli/cli.py b/app/src/cli/cli.py index d51989c..4de3185 100644 --- a/app/src/cli/cli.py +++ b/app/src/cli/cli.py @@ -124,11 +124,14 @@ def __init__( sys.exit(1) try: - self._validate_config( - api_key=api_key, - models=models, - api_key_per_model=api_key_per_model, - ) + if ( + provider != "ollama" + ): # Ollama doesn't require validation of API keys and models + self._validate_config( + api_key=api_key, + models=models, + api_key_per_model=api_key_per_model, + ) except ValueError as ve: self.ui.error(UI_MESSAGES["errors"]["config_error"].format(ve)) sys.exit(1) From aef3a2d5c4a58b1e9f35f979090d1b39ba2a532d Mon Sep 17 00:00:00 2001 From: YassWorks Date: Thu, 1 Jan 2026 01:43:09 +0100 Subject: [PATCH 19/43] refactor(ui): update output formatting for AgentUI --- app/src/core/ui.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/app/src/core/ui.py b/app/src/core/ui.py index 3a98f92..3307352 100644 --- a/app/src/core/ui.py +++ b/app/src/core/ui.py @@ -108,7 +108,7 @@ def tool_call(self, tool_name: str, args: dict[str, Any]): elif "\n" in value_str: value_str = value_str.split("\n")[0][:60] + "..." self.console.print( - f" [{self._style('dim')}]{k}:[/{self._style('dim')}] " + f"[{self._style('dim')}]{k}:[/{self._style('dim')}]\n" f"[{self._style('muted')}]{value_str}[/{self._style('muted')}]" ) @@ -134,12 +134,10 @@ def tool_output(self, tool_name: str, content: str): # Show output in a subtle code block style for line in content.strip().split("\n")[:10]: # Max 10 lines self.console.print( - f" [{self._style('muted')}]{line}[/{self._style('muted')}]" + f"[{self._style('muted')}]{line}[/{self._style('muted')}]" ) if content.strip().count("\n") > 10: - self.console.print( - f" [{self._style('dim')}]...[/{self._style('dim')}]" - ) + self.console.print(f"[{self._style('dim')}]...[/{self._style('dim')}]") # ───────────────────────────────────────────────────────────── # AI Response From 452ff04972951be5cf87898965428f27b5b981a6 Mon Sep 17 00:00:00 2001 From: YassWorks Date: Thu, 1 Jan 2026 01:43:09 +0100 Subject: [PATCH 20/43] chore: improve code generation UI readability --- app/src/orchestration/units/orchestrated_codegen.py | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/orchestration/units/orchestrated_codegen.py b/app/src/orchestration/units/orchestrated_codegen.py index 10c331e..092d896 100644 --- a/app/src/orchestration/units/orchestrated_codegen.py +++ b/app/src/orchestration/units/orchestrated_codegen.py @@ -50,6 +50,7 @@ def run( working_dir = working_dir or self._setup_working_directory() + self.ui.blank() # Add a blank line for better readability user_input = ( prompt or self.ui.get_input( From 72bd12d41f3e7b6634e9f3ae1a49b62c8e798617 Mon Sep 17 00:00:00 2001 From: YassWorks Date: Thu, 1 Jan 2026 01:43:09 +0100 Subject: [PATCH 21/43] chore: update default provider and model in config.json --- config.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config.json b/config.json index 0e12537..f38bca5 100644 --- a/config.json +++ b/config.json @@ -1,5 +1,5 @@ { - "provider": "cerebras", + "provider": "ollama", "provider_per_model": { "general": null, "code_gen": null, @@ -7,7 +7,7 @@ "web_searcher": null }, - "model": "zai-glm-4.6", + "model": "devstral-small-2:24b-cloud", "models": { "general": null, "code_gen": null, From bd6a5425bf0cf5651e5eaf8fbca28555a2ead8c1 Mon Sep 17 00:00:00 2001 From: YassWorks Date: Thu, 1 Jan 2026 01:43:09 +0100 Subject: [PATCH 22/43] chore: update and deduplicate dependencies in requirements.txt --- requirements.txt | 3 --- 1 file changed, 3 deletions(-) diff --git a/requirements.txt b/requirements.txt index 47b37d7..dee0686 100644 --- a/requirements.txt +++ b/requirements.txt @@ -31,6 +31,3 @@ rapidfuzz==3.14.3 requests==2.32.5 rich==14.2.0 hf-xet==1.2.0 -python-docx==1.2.0 -pymupdf4llm==0.2.4 -pymupdf-layout==1.26.6 From 018409023c2fa52053ab2f8594689f951ddaa888 Mon Sep 17 00:00:00 2001 From: YassWorks Date: Thu, 1 Jan 2026 01:52:36 +0100 Subject: [PATCH 23/43] chore: updated help section with missing commands --- app/utils/ui_messages.py | 27 ++++++++++++++++++++++----- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/app/utils/ui_messages.py b/app/utils/ui_messages.py index 9bc9c29..48a02ad 100644 --- a/app/utils/ui_messages.py +++ b/app/utils/ui_messages.py @@ -132,15 +132,32 @@ "help": { "content": [ "Use `Ctrl+n` to enter a new line and Enter to submit your message.", + "", "| Command | Description |", "|---------|-------------|", - "| /quit, /exit, /q | Exit |", - "| /clear | Clear history |", - "| /cls | Clear screen |", + "| /quit, /exit, /q | Exit the application |", + "| /clear | Clear conversation history |", + "| /cls, /clearterm, /clearscreen | Clear screen |", + "| /help, /h | Show this help message |", "| /model | Show current AI model |", - "| /model change `name` | change AI model |", + "| /model change `name` | Change AI model |", + "| /id | Show current session ID |", + "| /id `` | Change to specific session ID |", "| /project | Start project generation workflow |", - "| /help, /h | Show this help message |", + "", + "**RAG** (Retrieval-Augmented Generation) Commands (requires RAG to be setup and enabled)", + "", + "| Command | Description |", + "|---------|-------------|", + "| /start_rag | Enable RAG functionality |", + "| /stop_rag | Disable RAG functionality |", + "| /embed `` `` | Embed documents into collection |", + "| /refs, /references | Show latest references |", + "| /index `` | Index a collection for RAG |", + "| /unindex `` | Unindex a collection |", + "| /list | List all collections |", + "| /delete `` | Delete a collection |", + "| /purge | Reset database (delete all collections) |", ], "model_suffix": "Model: **{}**", }, From 68aecb698eec6ce32a66bb688a002b17790d9d79 Mon Sep 17 00:00:00 2001 From: YassWorks Date: Thu, 1 Jan 2026 11:59:37 +0100 Subject: [PATCH 24/43] Add uv as Python dependency manager in Dockerfile and update requirements handling --- Dockerfile | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 6b36649..3142dd0 100644 --- a/Dockerfile +++ b/Dockerfile @@ -6,6 +6,9 @@ ENV PYTHONDONTWRITEBYTECODE=1 \ WORKDIR /app +##### Install uv +COPY --from=ghcr.io/astral-sh/uv:latest /uv /bin/uv + ##### Install system dependencies ##### nano is included to edit the config.json file easily if needed. RUN apt-get update \ @@ -15,7 +18,7 @@ RUN apt-get update \ ##### Install Python dependencies COPY requirements.txt . -RUN pip install --no-cache-dir -r requirements.txt +RUN uv pip install --system --no-cache -r requirements.txt ##### Copy project COPY . . From 82a9006a9f126b6e73b767d9a5561f7ad2e42070 Mon Sep 17 00:00:00 2001 From: YassWorks Date: Thu, 1 Jan 2026 11:59:37 +0100 Subject: [PATCH 25/43] Update requirements.txt to latest LangChain and provider package versions --- requirements.txt | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/requirements.txt b/requirements.txt index dee0686..b8b5b84 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,14 +1,14 @@ # AI/LLM Providers openai==2.14.0 ollama==0.6.1 -langchain-anthropic==0.3.22 -langchain-cerebras==0.6.0 -langchain-groq==0.3.8 -langchain-google-genai==2.1.12 -langchain-ollama==0.3.10 +langchain-anthropic==1.3.0 +langchain-cerebras==0.8.2 +langchain-groq==1.1.1 +langchain-google-genai==4.1.2 +langchain-ollama==1.0.1 # LangChain Framework -langgraph==1.0.1 +langgraph==1.0.5 langgraph-checkpoint-sqlite==3.0.1 # Web Scraping & Parsing From 46ca89f2c45f14a502686f3e1eea33bdb86efa85 Mon Sep 17 00:00:00 2001 From: YassWorks Date: Thu, 1 Jan 2026 11:59:37 +0100 Subject: [PATCH 26/43] Update .gitignore for uv, pyproject.toml, and Python version management --- .gitignore | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.gitignore b/.gitignore index 4942e52..a9561d3 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,7 @@ canvas/ compose.yml bin/ + +.python-version +uv.lock +pyproject.toml From 1006065a24bed614b51e043661fbc6f6a324efeb Mon Sep 17 00:00:00 2001 From: YassWorks Date: Thu, 1 Jan 2026 11:59:37 +0100 Subject: [PATCH 27/43] Document Groq provider and clarify provider/model options in README --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index dc2f0bd..31db839 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,7 @@ Ally was built a fully local agentic system using **[Ollama](https://ollama.com/ - OpenAI - Anthropic - Google GenAI +- Groq - Cerebras - OpenAI-compatible providers (OpenRouter, GitHub Models) - _(more integrations on the way!)_ @@ -176,6 +177,10 @@ This file (located at `Ally/`) controls Ally's main settings and integrations. **Example configuration:** +- Options for `"provider"` are "openai", "ollama", "anthropic", "groq", "github", "openrouter", "cerebras", and "google". + +- The model name depends on your chosen provider (often found within the provider's dashboard or models tab). + ```json { "provider": "openai", From 7e1c52a0e3c13c809670e2e728b08f19354d8380 Mon Sep 17 00:00:00 2001 From: YassWorks Date: Thu, 1 Jan 2026 11:59:37 +0100 Subject: [PATCH 28/43] Update default temperature settings for providers in config.json and README --- README.md | 6 +++--- config.json | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 31db839..5393ac0 100644 --- a/README.md +++ b/README.md @@ -200,10 +200,10 @@ This file (located at `Ally/`) controls Ally's main settings and integrations. }, "temperatures": { - "general": 0.7, - "code_gen": 0, + "general": 1, + "code_gen": 1, "brainstormer": 1, - "web_searcher": 0 + "web_searcher": 1 }, "system_prompts": { // (recommended) leave as-is to use Ally's defaults diff --git a/config.json b/config.json index f38bca5..f0732a2 100644 --- a/config.json +++ b/config.json @@ -16,10 +16,10 @@ }, "temperatures": { - "general": 0, - "code_gen": 0, - "brainstormer": 0, - "web_searcher": 0 + "general": 1, + "code_gen": 1, + "brainstormer": 1, + "web_searcher": 1 }, "system_prompts": { "general": null, From a8fdf3f50c0f177d67bb3d9693d38d043b8fc736 Mon Sep 17 00:00:00 2001 From: YassWorks Date: Thu, 1 Jan 2026 12:02:52 +0100 Subject: [PATCH 29/43] update README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 5393ac0..426c290 100644 --- a/README.md +++ b/README.md @@ -93,7 +93,7 @@ This workflow is still in its early stages. You have 2 options: Via Docker or locally on your machine. -### **1. Docker** +### **1. Docker (Recommended)** Create a `.env` file (or copy `.env.example`) in any location From 5c6e076ec7866c5b1ad9b245fcb31d47c9de0f64 Mon Sep 17 00:00:00 2001 From: YassWorks Date: Thu, 1 Jan 2026 12:09:43 +0100 Subject: [PATCH 30/43] logo colors edit --- app/src/core/ui.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/app/src/core/ui.py b/app/src/core/ui.py index 3307352..9b8fa17 100644 --- a/app/src/core/ui.py +++ b/app/src/core/ui.py @@ -40,17 +40,18 @@ def _format_duration(self, seconds: float) -> str: # ───────────────────────────────────────────────────────────── def logo(self, ascii_art: str): - """Display ASCII logo with gradient.""" + """Display ASCII logo with simple styling for terminal compatibility.""" lines = ascii_art.split("\n") - n = max(len(lines) - 1, 1) + # Use theme colors that fallback gracefully on limited color terminals + styles = [ + f"bold {self._style('primary')}", + f"bold {self._style('secondary')}", + ] + for i, line in enumerate(lines): - progress = max(0.0, (i - 1) / n) - # Gradient from soft purple to soft coral - red = int(124 + (248 - 124) * progress) - green = int(138 + (113 - 138) * progress) - blue = int(255 + (113 - 255) * progress) - color = f"#{red:02x}{green:02x}{blue:02x}" - self.console.print(Text(line, style=f"bold {color}")) + # Alternate between two theme colors for visual interest + style = styles[i % 2] if i > 0 else f"bold {self._style('primary')}" + self.console.print(Text(line, style=style)) def help(self, model_name: str = None): """Display help information.""" From 57de21f753b1d3c5d698ce28004b70dd5239aab5 Mon Sep 17 00:00:00 2001 From: YassWorks Date: Thu, 1 Jan 2026 12:26:57 +0100 Subject: [PATCH 31/43] feat: add centralized logging system (AllyLogger) and integrate logging throughout codebase --- app/src/cli/cli.py | 28 +++++-- app/src/cli/flags.py | 2 + app/src/core/base.py | 18 ++-- app/src/core/exception_handler.py | 16 +++- app/src/embeddings/db_client.py | 31 ++++--- app/src/embeddings/scrapers/docling_setup.py | 4 +- app/src/orchestration/units/base_unit.py | 4 +- .../units/orchestrated_codegen.py | 7 +- app/src/tools/file_tools.py | 13 ++- app/src/tools/find_tools.py | 3 + app/src/tools/git_tools.py | 2 + app/src/tools/web_tools.py | 3 + app/utils/logger.py | 84 +++++++++++++++++++ main.py | 10 ++- 14 files changed, 187 insertions(+), 38 deletions(-) create mode 100644 app/utils/logger.py diff --git a/app/src/cli/cli.py b/app/src/cli/cli.py index 4de3185..24bf0bc 100644 --- a/app/src/cli/cli.py +++ b/app/src/cli/cli.py @@ -1,6 +1,7 @@ from app.src.orchestration.integrate_web_search import integrate_web_search from app.src.orchestration.units.orchestrated_codegen import CodeGenUnit from app.utils.ui_messages import UI_MESSAGES +from app.utils.logger import logger from app.src.embeddings.handle_commands import ( handle_embed_request, handle_index_request, @@ -120,7 +121,8 @@ def __init__( }, ) except Exception as e: - self.ui.error(UI_MESSAGES["errors"]["failed_initialize_agents"].format(e)) + logger.exception("Failed to initialize default agents") + self.ui.error(UI_MESSAGES["errors"]["failed_initialize_agents"]) sys.exit(1) try: @@ -133,10 +135,12 @@ def __init__( api_key_per_model=api_key_per_model, ) except ValueError as ve: - self.ui.error(UI_MESSAGES["errors"]["config_error"].format(ve)) + logger.error("Configuration validation error", exc_info=ve) + self.ui.error(UI_MESSAGES["errors"]["config_error"]) sys.exit(1) except Exception as e: - self.ui.error(UI_MESSAGES["errors"]["failed_validate_config"].format(e)) + logger.exception("Failed to validate configuration") + self.ui.error(UI_MESSAGES["errors"]["failed_validate_config"]) sys.exit(1) try: @@ -150,10 +154,12 @@ def __init__( provider_per_model=provider_per_model, ) except ValueError as ve: - self.ui.error(UI_MESSAGES["errors"]["config_error"].format(ve)) + logger.error("Coding configuration error", exc_info=ve) + self.ui.error(UI_MESSAGES["errors"]["config_error"]) sys.exit(1) except Exception as e: - self.ui.error(UI_MESSAGES["errors"]["failed_setup_coding"].format(e)) + logger.exception("Failed to setup coding configuration") + self.ui.error(UI_MESSAGES["errors"]["failed_setup_coding"]) sys.exit(1) def _validate_config( @@ -273,7 +279,8 @@ def start_chat(self, *args): except KeyboardInterrupt: self.ui.goodbye() except Exception as e: - self.ui.error(UI_MESSAGES["errors"]["unexpected_error"].format(e)) + logger.exception("Unexpected error in interactive session") + self.ui.error(UI_MESSAGES["errors"]["unexpected_error"]) def _integrate_rag(self, agent: BaseAgent, available: bool): """Integrate Retrieval-Augmented Generation (RAG) into the agent.""" @@ -345,7 +352,8 @@ def launch_coding_units(self, initial_prompt: str = None, active_dir: str = None except KeyboardInterrupt: self.ui.goodbye() except Exception as e: - self.ui.error(UI_MESSAGES["errors"]["unexpected_error"].format(e)) + logger.exception("Unexpected error in project workflow") + self.ui.error(UI_MESSAGES["errors"]["unexpected_error"]) def _setup_environment(self, user_args: list[str] = None) -> tuple[str, str, str]: """Setup working environment and configuration.""" @@ -367,8 +375,9 @@ def _setup_environment(self, user_args: list[str] = None) -> tuple[str, str, str if parsed_args.d == ".": parsed_args.d = os.getcwd() elif not validate_dir_name(parsed_args.d): + logger.error(f"Invalid directory name: {parsed_args.d}") self.ui.error( - UI_MESSAGES["errors"]["invalid_directory"].format(parsed_args.d) + UI_MESSAGES["errors"]["invalid_directory"] ) sys.exit(1) active_dir = parsed_args.d @@ -478,5 +487,6 @@ def _run_codegen_unit(self, working_dir: str, initial_prompt: str = None) -> boo ) except Exception as e: - self.ui.error(UI_MESSAGES["errors"]["failed_initialize_coding"].format(e)) + logger.exception("Failed to initialize coding workflow") + self.ui.error(UI_MESSAGES["errors"]["failed_initialize_coding"]) return False diff --git a/app/src/cli/flags.py b/app/src/cli/flags.py index 997b5c4..5e5127d 100644 --- a/app/src/cli/flags.py +++ b/app/src/cli/flags.py @@ -1,5 +1,6 @@ import argparse from app.src.core.ui import AgentUI +from app.utils.logger import logger import sys @@ -27,6 +28,7 @@ def __init__(self, ui: AgentUI): ) def error(self, message): + logger.error(f"Argument parsing error: {message}") usage = self.format_help() self.ui.error(f"{message}\n{usage}") sys.exit(2) diff --git a/app/src/core/base.py b/app/src/core/base.py index e3fcab2..961d1c6 100644 --- a/app/src/core/base.py +++ b/app/src/core/base.py @@ -3,6 +3,7 @@ from app.src.core.exception_handler import AgentExceptionHandler from app.src.core.permissions import PermissionDeniedException from app.src.embeddings.rag_errors import SetupFailedError, DBAccessError +from app.utils.logger import logger from langchain_core.messages import AIMessage, ToolMessage, BaseMessage, HumanMessage from app.src.embeddings.db_client import DataBaseClient from langgraph.graph.state import CompiledStateGraph @@ -187,9 +188,10 @@ def start_chat( self.ui.goodbye() return True - except openai.NotFoundError: + except openai.NotFoundError as e: + logger.error(f"Model not found: {self.model_name}", exc_info=e) self.ui.error( - UI_MESSAGES["errors"]["model_not_found"].format(self.model_name) + UI_MESSAGES["errors"]["model_not_found"] ) if self.prev_model_name: self.ui.status_message( @@ -217,7 +219,8 @@ def start_chat( except PermissionDeniedException: continue # Do nothing. Let the user enter a new prompt. - except lg_errors.GraphRecursionError: + except lg_errors.GraphRecursionError as e: + logger.warning("Graph recursion limit reached", exc_info=e) self.ui.warning(UI_MESSAGES["warnings"]["recursion_limit_reached"]) if self.ui.confirm( UI_MESSAGES["confirmations"]["continue_from_left_off"], default=True @@ -226,7 +229,8 @@ def start_chat( else: return False - except openai.RateLimitError: + except openai.RateLimitError as e: + logger.error("OpenAI rate limit exceeded", exc_info=e) self.ui.error(UI_MESSAGES["errors"]["rate_limit_exceeded"]) if self.ui.confirm( UI_MESSAGES["confirmations"]["change_model"], default=True @@ -242,7 +246,8 @@ def start_chat( return True except Exception as e: - self.ui.error(UI_MESSAGES["errors"]["unexpected_error"].format(e)) + logger.exception("Unexpected error in chat loop") + self.ui.error(UI_MESSAGES["errors"]["unexpected_error"]) return False finally: @@ -329,8 +334,9 @@ def _handle_command(self, user_input: str, configuration: dict) -> bool: self.rag = False except Exception as e: + logger.exception(f"Command failed: {cmd}") self.ui.error( - UI_MESSAGES["errors"]["command_failed"].format(cmd, e) + UI_MESSAGES["errors"]["command_failed"] ) finally: diff --git a/app/src/core/exception_handler.py b/app/src/core/exception_handler.py index 41ddbba..702070c 100644 --- a/app/src/core/exception_handler.py +++ b/app/src/core/exception_handler.py @@ -1,5 +1,6 @@ from app.src.core.permissions import PermissionDeniedException from app.src.core.ui import AgentUI +from app.utils.logger import logger from typing import Callable, Any import langgraph.errors as lg_errors import openai @@ -24,10 +25,12 @@ def handle_agent_exceptions( try: return operation() - except PermissionDeniedException: + except PermissionDeniedException as e: if propagate: raise + logger.warning("Permission denied for operation", exc_info=e) + if reject_operation: operation = reject_operation continue @@ -35,15 +38,18 @@ def handle_agent_exceptions( ui.error("Permission denied") return None - except lg_errors.GraphRecursionError: + except lg_errors.GraphRecursionError as e: if propagate: raise + logger.warning(f"Graph recursion error, retries: {retries}", exc_info=e) + if retry_operation: ui.warning( "Agent processing took longer than expected (Max recursion limit reached)" ) if retries >= AgentExceptionHandler.MAX_RETRIES: + logger.error(f"Max retries ({AgentExceptionHandler.MAX_RETRIES}) reached") ui.status_message( title="Max Retries Reached", message="Agent has been running for a while now. Please make the necessary adjustments to your prompt.", @@ -61,10 +67,11 @@ def handle_agent_exceptions( ui.error("Max recursion limit reached. Operation cannot continue.") return None - except openai.RateLimitError: + except openai.RateLimitError as e: if propagate: raise + logger.error("OpenAI rate limit exceeded", exc_info=e) ui.error("Rate limit exceeded. Please try again later") return None @@ -72,5 +79,6 @@ def handle_agent_exceptions( if propagate: raise - ui.error(f"An unexpected error occurred: {e}") + logger.exception("Unexpected error in agent operation") + ui.error("An unexpected error occurred") return None diff --git a/app/src/embeddings/db_client.py b/app/src/embeddings/db_client.py index 587cc36..cfa8fab 100644 --- a/app/src/embeddings/db_client.py +++ b/app/src/embeddings/db_client.py @@ -3,6 +3,7 @@ from app.src.helpers.valid_dir import validate_dir_name from app.src.embeddings.rag_errors import DBAccessError, ScrapingFailedError from app.src.core.ui import default_ui +from app.utils.logger import logger from app.utils.ui_messages import UI_MESSAGES from typing import Callable, Any from pathlib import Path @@ -58,8 +59,9 @@ def __init__( ) except Exception as e: + logger.exception("Failed to install ChromaDB package") default_ui.error( - UI_MESSAGES["errors"]["failed_install_packages"].format(e) + UI_MESSAGES["errors"]["failed_install_packages"] ) raise DBAccessError() import chromadb @@ -94,8 +96,9 @@ def _ensure_db_directory_exists(self) -> None: try: DB_PATH.mkdir(parents=True, exist_ok=True) except Exception as e: + logger.error(f"Failed to create database directory: {DB_PATH}", exc_info=e) default_ui.error( - UI_MESSAGES["errors"]["failed_create_db_directory"].format(e) + UI_MESSAGES["errors"]["failed_create_db_directory"] ) raise @@ -115,7 +118,8 @@ def _save_indexed_collections(self) -> None: with open(self.indexed_collections_path, "w") as f: json.dump(self.indexed_collections, f, indent=2) except Exception as e: - default_ui.error(UI_MESSAGES["errors"]["failed_save_indexed"].format(e)) + logger.error(f"Failed to save indexed collections", exc_info=e) + default_ui.error(UI_MESSAGES["errors"]["failed_save_indexed"]) def index_collection(self, collection_name: str) -> None: """Mark a collection as indexed.""" @@ -236,8 +240,9 @@ def store_documents(self, directory_path: str, collection_name: str) -> None: """Store all documents from a directory into the database.""" if not validate_dir_name(directory_path): + logger.error(f"Invalid directory path: {directory_path}") default_ui.error( - UI_MESSAGES["errors"]["invalid_directory_path"].format(directory_path) + UI_MESSAGES["errors"]["invalid_directory_path"] ) return @@ -258,17 +263,19 @@ def store_documents(self, directory_path: str, collection_name: str) -> None: if self.was_modified(directory_path, collection_name): self.store_document(directory_path, collection_name) - except ScrapingFailedError: + except ScrapingFailedError as e: + logger.error(f"Failed to scrape file: {directory_path}", exc_info=e) default_ui.error( - UI_MESSAGES["errors"]["failed_scrape"].format(directory_path) + UI_MESSAGES["errors"]["failed_scrape"] ) except: raise if not os.path.exists(directory_path): + logger.error(f"Directory does not exist: {directory_path}") default_ui.error( - UI_MESSAGES["errors"]["directory_not_exist"].format(directory_path) + UI_MESSAGES["errors"]["directory_not_exist"] ) return @@ -279,9 +286,10 @@ def store_documents(self, directory_path: str, collection_name: str) -> None: if self.was_modified(file_path, collection_name): self.store_document(file_path, collection_name) - except ScrapingFailedError: + except ScrapingFailedError as e: + logger.error(f"Failed to scrape file: {file_path}", exc_info=e) default_ui.error( - UI_MESSAGES["errors"]["failed_scrape"].format(file_path) + UI_MESSAGES["errors"]["failed_scrape"] ) except: @@ -320,9 +328,10 @@ def delete_collection(self, collection_name: str) -> None: style="success", ) - except chromadb_errors.NotFoundError: + except chromadb_errors.NotFoundError as e: + logger.warning(f"Collection not found: {collection_name}", exc_info=e) default_ui.error( - UI_MESSAGES["errors"]["collection_not_exist"].format(collection_name) + UI_MESSAGES["errors"]["collection_not_exist"] ) except Exception: diff --git a/app/src/embeddings/scrapers/docling_setup.py b/app/src/embeddings/scrapers/docling_setup.py index 769848c..291280f 100644 --- a/app/src/embeddings/scrapers/docling_setup.py +++ b/app/src/embeddings/scrapers/docling_setup.py @@ -1,6 +1,7 @@ from app.src.core.ui import default_ui from app.utils.constants import DEFAULT_PATHS from app.src.helpers.valid_dir import validate_dir_name +from app.utils.logger import logger from pathlib import Path import os @@ -70,5 +71,6 @@ def setup(path: str = ARTIFACTS_PATH) -> None: ) except Exception as e: - default_ui.error(f"Failed to download parsing models: {e}") + logger.exception("Failed to download parsing models") + default_ui.error("Failed to download parsing models. Check logs for details.") raise diff --git a/app/src/orchestration/units/base_unit.py b/app/src/orchestration/units/base_unit.py index 1becd6d..70a4045 100644 --- a/app/src/orchestration/units/base_unit.py +++ b/app/src/orchestration/units/base_unit.py @@ -1,6 +1,7 @@ from abc import ABC, abstractmethod from typing import Any from app.src.core.ui import default_ui +from app.utils.logger import logger from app.utils.constants import RECURSION_LIMIT from app.src.core.base import BaseAgent from app.utils.ui_messages import UI_MESSAGES @@ -55,7 +56,8 @@ def _setup_working_directory(self, default_dir: str = None) -> str: ) os.makedirs(working_dir, exist_ok=True) return working_dir - except Exception: + except Exception as e: + logger.error(f"Failed to create directory: {working_dir}", exc_info=e) self.ui.error(UI_MESSAGES["errors"]["failed_create_directory"]) working_dir = None diff --git a/app/src/orchestration/units/orchestrated_codegen.py b/app/src/orchestration/units/orchestrated_codegen.py index 092d896..7ddf7ce 100644 --- a/app/src/orchestration/units/orchestrated_codegen.py +++ b/app/src/orchestration/units/orchestrated_codegen.py @@ -1,6 +1,7 @@ from app.src.orchestration.units.base_unit import BaseUnit from app.src.core.exception_handler import AgentExceptionHandler from app.src.orchestration.integrate_web_search import integrate_web_search +from app.utils.logger import logger from app.utils.constants import PROMPTS, RECURSION_LIMIT from app.utils.ui_messages import UI_MESSAGES from app.utils.ascii_art import ASCII_ART @@ -68,7 +69,8 @@ def run( return True except Exception as e: - self.ui.error(UI_MESSAGES["errors"]["workflow_execution_failed"].format(e)) + logger.exception("Workflow execution failed") + self.ui.error(UI_MESSAGES["errors"]["workflow_execution_failed"]) return False def _execute_generation_workflow( @@ -317,6 +319,7 @@ def _enhance_agents(self): web_searcher=self.agents["web_searcher"], ) except Exception as e: - error_msg = UI_MESSAGES["errors"]["failed_integrate_web_search"].format(e) + logger.exception("Failed to integrate web search capabilities") + error_msg = UI_MESSAGES["errors"]["failed_integrate_web_search"] self.ui.error(error_msg) raise RuntimeError(error_msg) diff --git a/app/src/tools/file_tools.py b/app/src/tools/file_tools.py index cd820ae..7ae6c05 100644 --- a/app/src/tools/file_tools.py +++ b/app/src/tools/file_tools.py @@ -1,4 +1,5 @@ from app.src.core.permissions import permission_manager, PermissionDeniedException +from app.utils.logger import logger from langchain_core.tools import tool import os import shutil @@ -28,6 +29,7 @@ def create_wd(path: str) -> str: os.makedirs(path, exist_ok=True) return f"Working directory created at {path}" except Exception as e: + logger.error(f"Failed to create working directory: {path}", exc_info=e) return f"Error creating working directory: {str(e)}" @@ -63,6 +65,7 @@ def create_file(file_path: str, content: str) -> str: f.write(content) return f"File created at {file_path}" except Exception as e: + logger.error(f"Failed to create file: {file_path}", exc_info=e) return f"[ERROR] Failed to create file: {str(e)}" @@ -106,6 +109,7 @@ def modify_file(file_path: str, old_content: str, new_content: str) -> str: f.write(contents) return f"File modified at {file_path}" except Exception as e: + logger.error(f"Failed to modify file: {file_path}", exc_info=e) return f"Error modifying file: {str(e)}" @@ -143,6 +147,7 @@ def append_file(file_path: str, content: str) -> str: f.write(content) return f"Content appended to {file_path}" except Exception as e: + logger.error(f"Failed to append to file: {file_path}", exc_info=e) return f"Error appending file: {str(e)}" @@ -172,6 +177,7 @@ def delete_file(file_path: str) -> str: os.remove(file_path) return f"File deleted at {file_path}" except Exception as e: + logger.error(f"Failed to delete file: {file_path}", exc_info=e) return f"Error deleting file: {str(e)}" @@ -201,6 +207,7 @@ def delete_directory(path: str) -> str: else: return f"Directory does not exist: {path}" except Exception as e: + logger.error(f"Failed to delete directory: {path}", exc_info=e) return f"Error deleting directory: {str(e)}" @@ -231,6 +238,7 @@ def read_file(file_path: str) -> str: contents = f.read() return contents except Exception as e: + logger.error(f"Failed to read file: {file_path}", exc_info=e) return f"Error reading file: {str(e)}" @@ -317,12 +325,14 @@ def _list_directory_recursive( else: items.append(f"{prefix}{item_name}") - except PermissionError: + except PermissionError as e: + logger.warning(f"Permission denied accessing: {current_path}", exc_info=e) if current_depth == 0: items.append("❌ Permission denied") else: items.append(f"{parent_prefix}❌ Permission denied") except Exception as e: + logger.error(f"Error listing directory: {current_path}", exc_info=e) if current_depth == 0: items.append(f"❌ Error: {str(e)}") else: @@ -338,6 +348,7 @@ def _list_directory_recursive( return "\n".join(result) except Exception as e: + logger.error(f"Failed to list directory: {path}", exc_info=e) return f"Error listing directory: {str(e)}" diff --git a/app/src/tools/find_tools.py b/app/src/tools/find_tools.py index 529e4a8..0952b4c 100644 --- a/app/src/tools/find_tools.py +++ b/app/src/tools/find_tools.py @@ -1,3 +1,4 @@ +from app.utils.logger import logger from langchain_core.tools import tool import os import re @@ -75,6 +76,7 @@ def find_references(dir_path: str, query: str) -> str: return "\n".join(out_lines) except Exception as e: + logger.error(f"Error searching for references: {query} in {dir_path}", exc_info=e) return f"Search error: {str(e)}" @@ -112,6 +114,7 @@ def find_declaration(dir_path: str, symbol: str) -> str: return "\n".join(out_lines) except Exception as e: + logger.error(f"Error searching for declaration: {symbol} in {dir_path}", exc_info=e) return f"Search error: {str(e)}" diff --git a/app/src/tools/git_tools.py b/app/src/tools/git_tools.py index dab41f9..a64f233 100644 --- a/app/src/tools/git_tools.py +++ b/app/src/tools/git_tools.py @@ -1,3 +1,4 @@ +from app.utils.logger import logger from langchain_core.tools import tool import subprocess import os @@ -53,6 +54,7 @@ def diff(commit1: str | None, commit2: str | None, cwd: str | None) -> str: ) except Exception as e: + logger.error(f"Git diff execution error in {cwd}", exc_info=e) return f"Execution error: {str(e)}" diff --git a/app/src/tools/web_tools.py b/app/src/tools/web_tools.py index aee0f60..0d850e7 100644 --- a/app/src/tools/web_tools.py +++ b/app/src/tools/web_tools.py @@ -1,3 +1,4 @@ +from app.utils.logger import logger from langchain_core.tools import tool from bs4 import BeautifulSoup from requests.adapters import HTTPAdapter, Retry @@ -44,8 +45,10 @@ def fetch(url: str) -> str: return text[:10000] except requests.RequestException as e: + logger.error(f"HTTP error fetching {url}", exc_info=e) return f"[ERROR] HTTP error for {url}: {e}" except Exception as e: + logger.error(f"Failed to parse {url}", exc_info=e) return f"[ERROR] Failed to parse {url}: {e}" diff --git a/app/utils/logger.py b/app/utils/logger.py new file mode 100644 index 0000000..40ba429 --- /dev/null +++ b/app/utils/logger.py @@ -0,0 +1,84 @@ +import logging +import os +from datetime import datetime +from pathlib import Path +from app.utils.constants import DEFAULT_PATHS + + +class AllyLogger: + """Centralized logging system for Ally.""" + + _instance = None + _logger = None + + def __new__(cls): + if cls._instance is None: + cls._instance = super(AllyLogger, cls).__new__(cls) + cls._instance._initialize_logger() + return cls._instance + + def _initialize_logger(self): + """Initialize the logger with file and console handlers.""" + # Expand environment variables and user home directory + log_dir = os.path.expandvars(DEFAULT_PATHS["logs"]) + log_dir = os.path.expanduser(log_dir) + + # Create log directory if it doesn't exist + Path(log_dir).mkdir(parents=True, exist_ok=True) + + # Create log filename with timestamp + log_filename = f"ally_{datetime.now().strftime('%Y%m%d')}.log" + log_path = os.path.join(log_dir, log_filename) + + # Create logger + self._logger = logging.getLogger("ally") + self._logger.setLevel(logging.DEBUG) + + # Avoid duplicate handlers if logger already configured + if not self._logger.handlers: + # File handler - logs everything + file_handler = logging.FileHandler(log_path, encoding="utf-8") + file_handler.setLevel(logging.DEBUG) + file_formatter = logging.Formatter( + "%(asctime)s | %(levelname)-8s | %(name)s | %(message)s", + datefmt="%Y-%m-%d %H:%M:%S", + ) + file_handler.setFormatter(file_formatter) + + # Console handler - only warnings and above (optional, can be removed if not needed) + console_handler = logging.StreamHandler() + console_handler.setLevel(logging.WARNING) + console_formatter = logging.Formatter("%(levelname)s: %(message)s") + console_handler.setFormatter(console_formatter) + + self._logger.addHandler(file_handler) + # Uncomment the next line if you want console logging for warnings/errors + # self._logger.addHandler(console_handler) + + def debug(self, message: str, **kwargs): + """Log debug message.""" + self._logger.debug(message, extra=kwargs) + + def info(self, message: str, **kwargs): + """Log info message.""" + self._logger.info(message, extra=kwargs) + + def warning(self, message: str, **kwargs): + """Log warning message.""" + self._logger.warning(message, extra=kwargs) + + def error(self, message: str, exc_info=None, **kwargs): + """Log error message with optional exception info.""" + self._logger.error(message, exc_info=exc_info, extra=kwargs) + + def critical(self, message: str, exc_info=None, **kwargs): + """Log critical message with optional exception info.""" + self._logger.critical(message, exc_info=exc_info, extra=kwargs) + + def exception(self, message: str, **kwargs): + """Log exception with traceback.""" + self._logger.exception(message, extra=kwargs) + + +# Create singleton instance +logger = AllyLogger() diff --git a/main.py b/main.py index 503f95b..0e8bddc 100644 --- a/main.py +++ b/main.py @@ -3,6 +3,7 @@ load_dotenv() from app import CLI, default_ui +from app.utils.logger import logger from warnings import filterwarnings import os @@ -36,14 +37,17 @@ try: with open(config_path) as f: config = json.load(f) -except FileNotFoundError: +except FileNotFoundError as e: + logger.error("Configuration file 'config.json' not found", exc_info=e) default_ui.error("Configuration file 'config.json' not found.") sys.exit(1) -except json.JSONDecodeError: +except json.JSONDecodeError as e: + logger.error("Configuration file 'config.json' is not valid JSON", exc_info=e) default_ui.error("Configuration file 'config.json' is not a valid JSON.") sys.exit(1) except Exception as e: - default_ui.error(f"An unexpected error occurred: {e}") + logger.exception("Unexpected error loading configuration") + default_ui.error("An unexpected error occurred") sys.exit(1) From 9821d1fc8f2fb06c62dd2e12b1ce01715f25ddb2 Mon Sep 17 00:00:00 2001 From: YassWorks Date: Thu, 1 Jan 2026 12:26:57 +0100 Subject: [PATCH 32/43] docs: update README and technical documentation for logging and error handling --- README.md | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 426c290..40b147a 100644 --- a/README.md +++ b/README.md @@ -264,7 +264,15 @@ Use `ally -h` for more help. ## Technical notes -1. Edit the following environment variable if needed: +1. **Logging**: Ally uses a centralized logging system that records detailed error information in log files. Error messages shown to users are kept concise for better readability, while full technical details (including stack traces) are logged for debugging purposes. + + - Log files are stored in: + - Windows: `%LOCALAPPDATA%\Ally\logs\` + - Linux/MacOS: `~/.local/share/Ally/logs/` + - Logs are organized by date: `ally_YYYYMMDD.log` + - When reporting issues, please include relevant log file excerpts + +2. Edit the following environment variable if needed: | Environment Variable | Purpose | | --------------------------- | --------------------------------------------------------------- | @@ -283,11 +291,11 @@ Linux & MacOS: ~/.local/share/Ally/... ``` -2. RAG-related tools used by Ally are large in size and are therefore downloaded only after RAG settings are enabled in the config.json file. As a result, Ally will perform additional downloads the next time it is launched following these configuration changes. +3. RAG-related tools used by Ally are large in size and are therefore downloaded only after RAG settings are enabled in the config.json file. As a result, Ally will perform additional downloads the next time it is launched following these configuration changes. -3. To save a chat, use /id to view the conversation ID. The next time you open Ally, continue the conversation by using the -i flag followed by the ID. You can do the same inside the CLI, just do `/id ` +4. To save a chat, use /id to view the conversation ID. The next time you open Ally, continue the conversation by using the -i flag followed by the ID. You can do the same inside the CLI, just do `/id ` -4. Embedding and scraping files that require OCR (such as PDFs and DOCX) currently use a CPU-only PyTorch installation. You can modify the configuration to utilize a GPU if desired, though this is typically only necessary for processing very large files. +5. Embedding and scraping files that require OCR (such as PDFs and DOCX) currently use a CPU-only PyTorch installation. You can modify the configuration to utilize a GPU if desired, though this is typically only necessary for processing very large files. ## License From af21e3cdd964752546628c1783527b54d0a31393 Mon Sep 17 00:00:00 2001 From: YassWorks Date: Thu, 1 Jan 2026 12:26:57 +0100 Subject: [PATCH 33/43] feat: update UI messages and constants for new logging system and improved error clarity --- app/utils/constants.py | 25 ++++++++------ app/utils/ui_messages.py | 72 +++++++++++++++++++++------------------- 2 files changed, 52 insertions(+), 45 deletions(-) diff --git a/app/utils/constants.py b/app/utils/constants.py index 58ddb3a..5b54729 100644 --- a/app/utils/constants.py +++ b/app/utils/constants.py @@ -13,16 +13,16 @@ # Vibrant unified theme built around purple accent THEME = { - "primary": "#a855f7", # Vibrant purple (main accent) - "secondary": "#ec4899", # Vibrant pink (complementary) - "accent": "#06b6d4", # Vibrant cyan (tertiary) - "success": "#10b981", # Vibrant emerald - "warning": "#f59e0b", # Vibrant amber - "error": "#ef4444", # Vibrant red - "muted": "#9ca3af", # Neutral gray - "dim": "#6b7280", # Dim gray - "text": "#f3f4f6", # Light gray - "border": "#6b21a8", # Deep purple border + "primary": "#a855f7", # Vibrant purple (main accent) + "secondary": "#ec4899", # Vibrant pink (complementary) + "accent": "#06b6d4", # Vibrant cyan (tertiary) + "success": "#10b981", # Vibrant emerald + "warning": "#f59e0b", # Vibrant amber + "error": "#ef4444", # Vibrant red + "muted": "#9ca3af", # Neutral gray + "dim": "#6b7280", # Dim gray + "text": "#f3f4f6", # Light gray + "border": "#6b21a8", # Deep purple border } PROMPTS = { @@ -51,6 +51,11 @@ if os.name == "nt" else "~/.local/share/Ally/parsing_models/" ), + "logs": ( + "%LOCALAPPDATA%\\Ally\\logs\\" + if os.name == "nt" + else "~/.local/share/Ally/logs/" + ), } diff --git a/app/utils/ui_messages.py b/app/utils/ui_messages.py index 48a02ad..487508d 100644 --- a/app/utils/ui_messages.py +++ b/app/utils/ui_messages.py @@ -67,44 +67,44 @@ # Warnings "warnings": { "failed_confirm": "Failed to confirm action. Continuing with default value ({})", - "rag_not_available": "RAG is not available. Please configure an embedding provider.", - "rag_enabled_no_client": "RAG is enabled but no database client is configured.", - "rag_features_disabled": "RAG features disabled.", - "invalid_db_path": "Invalid directory path found in $ALLY_DATABASE_DIR. Reverting to default path.", - "recursion_limit_reached": "Agent processing took longer than expected (Max recursion limit reached)", + "rag_not_available": "RAG is not available. Please configure an embedding provider in config.json.", + "rag_enabled_no_client": "RAG is enabled but no database client is configured. Check your setup.", + "rag_features_disabled": "RAG features have been disabled due to errors.", + "invalid_db_path": "Invalid directory path found in $ALLY_DATABASE_DIR environment variable. Reverting to default path.", + "recursion_limit_reached": "Agent processing took longer than expected. Recursion limit reached.", }, # Errors "errors": { - "failed_initialize_agents": "Failed to initialize default agents: {}", - "config_error": "Configuration error: {}", - "failed_validate_config": "Failed to validate configuration: {}", - "failed_setup_coding": "Failed to setup coding configuration: {}", - "unexpected_error": "An unexpected error occurred: {}", - "codegen_failed": "Code generation workflow failed to complete successfully", - "invalid_directory": "Invalid directory name: {}", - "failed_create_directory": "Failed to create directory", - "failed_initialize_coding": "Failed to initialize coding workflow: {}", - "workflow_execution_failed": "Workflow execution failed: {}", - "coding_session_failed": "The interactive coding session did not exit safely", - "failed_integrate_web_search": "Failed to integrate web search capabilities: {}", - "rate_limit_exceeded": "Rate limit exceeded. Please try again later", - "db_not_initialized": "Database client is not initialized. There might be an issue with your embeddings config.", - "invalid_directory_path": "Invalid directory path: {}", - "directory_not_exist": "Directory {} does not exist.", - "failed_scrape": "Failed to scrape file: {}. Skipping.", - "collection_not_exist": "Collection {} does not exist.", - "failed_create_db_directory": "Failed to create database directory: {}", - "failed_save_indexed": "Failed to save indexed collections: {}", - "failed_install_packages": "Failed to install required packages. Please install them manually. Error: {}", - "db_access_error": "Database access error occurred.", - "setup_failed": "Setup failed.", - "command_failed": "Command '{}' failed: {}", - "unknown_command": "Unknown command. Type /help for instructions.", - "specify_model": "Please specify a model to change to.", - "unknown_model_command": "Unknown model command. Type /help for instructions.", - "agent_execution_failed": "[ERROR] Agent execution failed.", - "no_messages_returned": "[ERROR] Agent did not return any messages.", - "model_not_found": "Model '{}' not found or not supported.", + "failed_initialize_agents": "Failed to initialize default agents. Check your API keys and configuration.", + "config_error": "Configuration error occurred. Please check your config.json file.", + "failed_validate_config": "Failed to validate configuration. Verify API keys and model names.", + "failed_setup_coding": "Failed to setup coding configuration. Check your models and API settings.", + "unexpected_error": "An unexpected error occurred. Check logs for details.", + "codegen_failed": "Code generation workflow failed to complete successfully. Check logs for details.", + "invalid_directory": "Invalid directory name. Please provide a valid path.", + "failed_create_directory": "Failed to create directory. Check path permissions and syntax.", + "failed_initialize_coding": "Failed to initialize coding workflow. Check your configuration and logs.", + "workflow_execution_failed": "Workflow execution failed. Check logs for details.", + "coding_session_failed": "The interactive coding session did not exit safely. Check logs for details.", + "failed_integrate_web_search": "Failed to integrate web search capabilities. Check logs for details.", + "rate_limit_exceeded": "API rate limit exceeded. Please wait and try again later.", + "db_not_initialized": "Database client is not initialized. Check your embeddings configuration in config.json.", + "invalid_directory_path": "Invalid directory path provided. Please use a valid file system path.", + "directory_not_exist": "The specified directory does not exist. Please check the path.", + "failed_scrape": "Failed to scrape file. Skipping. Check logs for details.", + "collection_not_exist": "The specified collection does not exist in the database.", + "failed_create_db_directory": "Failed to create database directory. Check file system permissions.", + "failed_save_indexed": "Failed to save indexed collections metadata. Check logs for details.", + "failed_install_packages": "Failed to install required packages. Please install them manually or check your internet connection.", + "db_access_error": "Database access error occurred. Check logs for details.", + "setup_failed": "Setup failed. Check logs for details.", + "command_failed": "Command execution failed. Check logs for details.", + "unknown_command": "Unknown command. Type /help to see available commands.", + "specify_model": "Please specify a model name. Usage: /model change ", + "unknown_model_command": "Unknown model command. Type /help to see available model commands.", + "agent_execution_failed": "Agent execution failed. Check logs for details.", + "no_messages_returned": "Agent did not return any messages. Check logs for details.", + "model_not_found": "Model not found or not supported. Verify the model name is correct.", "collection_name_too_short": "Collection name must be at least 3 characters long.", }, # Confirmations @@ -133,6 +133,8 @@ "content": [ "Use `Ctrl+n` to enter a new line and Enter to submit your message.", "", + "**Note**: Detailed error logs are saved to the logs directory for troubleshooting.", + "", "| Command | Description |", "|---------|-------------|", "| /quit, /exit, /q | Exit the application |", From 9e253a817614f9016d9cc5ea44d1ba52026adce8 Mon Sep 17 00:00:00 2001 From: YassWorks Date: Thu, 1 Jan 2026 12:30:52 +0100 Subject: [PATCH 34/43] chore: updated docs with the new providers additions --- .env.example | 3 +++ README.md | 8 ++++++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/.env.example b/.env.example index 2d3d2a0..54b21eb 100644 --- a/.env.example +++ b/.env.example @@ -3,7 +3,10 @@ OPENAI_API_KEY=... ANTHROPIC_API_KEY=... GOOGLE_GEN_AI_API_KEY=... +GROQ_API_KEY=... CEREBRAS_API_KEY=... +OPENROUTER_API_KEY=... +GITHUB_AI_API_KEY=... # Embedding providers APIs (As needed. Only add those you need.) diff --git a/README.md b/README.md index 40b147a..a94c811 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,8 @@ Ally was built a fully local agentic system using **[Ollama](https://ollama.com/ - Google GenAI - Groq - Cerebras -- OpenAI-compatible providers (OpenRouter, GitHub Models) +- OpenRouter +- GitHub Models - _(more integrations on the way!)_ This tool is best suited for scenarios where privacy is paramount and agentic capabilities are needed in the workflow. @@ -177,7 +178,7 @@ This file (located at `Ally/`) controls Ally's main settings and integrations. **Example configuration:** -- Options for `"provider"` are "openai", "ollama", "anthropic", "groq", "github", "openrouter", "cerebras", and "google". +- Options for `"provider"` are "openai", "ollama", "anthropic", "google", "groq", "cerebras", "openrouter", and "github". - The model name depends on your chosen provider (often found within the provider's dashboard or models tab). @@ -234,7 +235,10 @@ This file stores your API keys. OPENAI_API_KEY=... ANTHROPIC_API_KEY=... GOOGLE_GEN_AI_API_KEY=... +GROQ_API_KEY=... CEREBRAS_API_KEY=... +OPENROUTER_API_KEY=... +GITHUB_AI_API_KEY=... # Embedding providers APIs (As needed. Only add those you need.) From 5e42b02147deacae9e37a7036d64b2877ba40c54 Mon Sep 17 00:00:00 2001 From: YassWorks Date: Sat, 3 Jan 2026 18:48:13 +0100 Subject: [PATCH 35/43] updated the setup for linux/mac & removed logging to stdout --- app/src/cli/cli.py | 7 ++--- app/utils/logger.py | 10 ++----- setup.sh | 72 ++++++++++++++++++++------------------------- 3 files changed, 37 insertions(+), 52 deletions(-) diff --git a/app/src/cli/cli.py b/app/src/cli/cli.py index 24bf0bc..4a6c4fc 100644 --- a/app/src/cli/cli.py +++ b/app/src/cli/cli.py @@ -87,6 +87,7 @@ def __init__( self.rag_available = False match scraping_method.lower(): + case "docling": from app.src.embeddings.scrapers.docling_scraper import DoclingScraper @@ -121,7 +122,7 @@ def __init__( }, ) except Exception as e: - logger.exception("Failed to initialize default agents") + logger.error(f"Failed to initialize default agents: {e}") self.ui.error(UI_MESSAGES["errors"]["failed_initialize_agents"]) sys.exit(1) @@ -376,9 +377,7 @@ def _setup_environment(self, user_args: list[str] = None) -> tuple[str, str, str parsed_args.d = os.getcwd() elif not validate_dir_name(parsed_args.d): logger.error(f"Invalid directory name: {parsed_args.d}") - self.ui.error( - UI_MESSAGES["errors"]["invalid_directory"] - ) + self.ui.error(UI_MESSAGES["errors"]["invalid_directory"]) sys.exit(1) active_dir = parsed_args.d diff --git a/app/utils/logger.py b/app/utils/logger.py index 40ba429..faa334f 100644 --- a/app/utils/logger.py +++ b/app/utils/logger.py @@ -33,6 +33,8 @@ def _initialize_logger(self): # Create logger self._logger = logging.getLogger("ally") self._logger.setLevel(logging.DEBUG) + # Prevent logging from propagating to root logger (which outputs to console) + self._logger.propagate = False # Avoid duplicate handlers if logger already configured if not self._logger.handlers: @@ -45,15 +47,7 @@ def _initialize_logger(self): ) file_handler.setFormatter(file_formatter) - # Console handler - only warnings and above (optional, can be removed if not needed) - console_handler = logging.StreamHandler() - console_handler.setLevel(logging.WARNING) - console_formatter = logging.Formatter("%(levelname)s: %(message)s") - console_handler.setFormatter(console_formatter) - self._logger.addHandler(file_handler) - # Uncomment the next line if you want console logging for warnings/errors - # self._logger.addHandler(console_handler) def debug(self, message: str, **kwargs): """Log debug message.""" diff --git a/setup.sh b/setup.sh index d06dc75..f1d2f5c 100644 --- a/setup.sh +++ b/setup.sh @@ -1,60 +1,52 @@ -#!/bin/sh +#!/bin/bash set -e echo "=== Setting up Ally ===" -# =========================== Step 1: Create virtual environment =========================== +# script directory +INSTALL_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +# =========================== Step 1: Install requirements ================================= -echo "Creating virtual environment..." -python3 -m venv .venv +printf '\nInstalling dependencies...\n' +command -v uv >/dev/null 2>&1 || { + echo "Error: uv not found. Please install it from https://github.com/astral-sh/uv" + exit 1 +} -# =========================== Step 2: Install requirements ================================= +cd "$INSTALL_DIR" + +if [ ! -f "pyproject.toml" ]; then + uv init >/dev/null 2>&1 +fi -echo "Installing dependencies..." -.venv/bin/pip install -r requirements.txt +uv add -r requirements.txt >/dev/null 2>&1 -# =========================== Step 3: Create bin/ally launcher ============================= +# =========================== Step 2: Create bin/ally launcher ============================= -echo "Creating launcher script..." -CURR_DIR="$(pwd)" -mkdir -p bin -cat > bin/ally < ally </dev/null || echo "") +printf '\nInstalling ally to /usr/local/bin...\n' -if [[ "$PARENT_SHELL" == *"zsh"* ]] || [[ "$SHELL" == *"zsh"* ]]; then - SHELL_CONFIG="$HOME/.zshrc" -elif [[ "$PARENT_SHELL" == *"bash"* ]] || [[ "$SHELL" == *"bash"* ]]; then - if [ -f "$HOME/.bash_profile" ]; then - SHELL_CONFIG="$HOME/.bash_profile" - else - SHELL_CONFIG="$HOME/.bashrc" - fi -else - SHELL_CONFIG="" -fi +# check if we have permission to write to /usr/local/bin, use sudo if needed +TARGET_PATH="/usr/local/bin/ally" -# check PATH membership -if [[ ":$PATH:" == *":$BIN_DIR:"* ]]; then - echo "$BIN_DIR already in PATH" +if [ -w "/usr/local/bin" ]; then + ln -sf "$INSTALL_DIR/ally" "$TARGET_PATH" else - if [ -n "$SHELL_CONFIG" ] && ! grep -Fxq "export PATH=\"$BIN_DIR:\$PATH\"" "$SHELL_CONFIG" 2>/dev/null; then - echo "export PATH=\"$BIN_DIR:\$PATH\"" >> "$SHELL_CONFIG" - echo "Added $BIN_DIR to $SHELL_CONFIG" - echo "Run: source $SHELL_CONFIG (or restart your terminal)" - fi + echo "Sudo permissions required to install to /usr/local/bin" + sudo ln -sf "$INSTALL_DIR/ally" "$TARGET_PATH" fi -echo "\nAdd this line to your shell config if needed: export PATH=\"$BIN_DIR:\$PATH\"" echo "=== Setup complete! You can now run 'ally' in a new terminal window ===" From 9ea9adb7b826ff446d10b018c79e9725bc835716 Mon Sep 17 00:00:00 2001 From: YassWorks Date: Sat, 3 Jan 2026 20:35:46 +0100 Subject: [PATCH 36/43] chore: reworked windows installer --- README.md | 1 + app/utils/ui_messages.py | 3 ++- main.py | 10 +++++-- setup.cmd | 48 ---------------------------------- setup.ps1 | 56 ++++++++++++++++++++++++++++++++++++++++ 5 files changed, 67 insertions(+), 51 deletions(-) delete mode 100644 setup.cmd create mode 100644 setup.ps1 diff --git a/README.md b/README.md index a94c811..7dfbd2f 100644 --- a/README.md +++ b/README.md @@ -161,6 +161,7 @@ docker start -ai ally ### Prerequesites: - [Python](https://www.python.org/) +- [UV](https://github.com/astral-sh/uv) - [Git](https://git-scm.com/downloads) (or download the source code from this repo) - [Ollama](https://ollama.com/download) (optional) diff --git a/app/utils/ui_messages.py b/app/utils/ui_messages.py index 487508d..5bc55df 100644 --- a/app/utils/ui_messages.py +++ b/app/utils/ui_messages.py @@ -75,7 +75,8 @@ }, # Errors "errors": { - "failed_initialize_agents": "Failed to initialize default agents. Check your API keys and configuration.", + "failed_initialize_agents": "Failed to initialize default agents. Check your API keys and configuration." + + "\nIf you're using Ollama, ensure the Ollama app is running.", "config_error": "Configuration error occurred. Please check your config.json file.", "failed_validate_config": "Failed to validate configuration. Verify API keys and model names.", "failed_setup_coding": "Failed to setup coding configuration. Check your models and API settings.", diff --git a/main.py b/main.py index 0e8bddc..8df4ab1 100644 --- a/main.py +++ b/main.py @@ -99,8 +99,14 @@ def main(): - args = sys.argv[1:] - client.start_chat(*args) + try: + args = sys.argv[1:] + client.start_chat(*args) + except Exception as e: + logger.error(f"An unexpected error occurred during the chat session: {e}") + default_ui.error( + "An unexpected error occurred. Please check the logs for details." + ) if __name__ == "__main__": diff --git a/setup.cmd b/setup.cmd deleted file mode 100644 index 5c8a796..0000000 --- a/setup.cmd +++ /dev/null @@ -1,48 +0,0 @@ -@echo off -setlocal - -echo === Setting up Ally === - -:: Step 1: Create virtual environment -echo Creating virtual environment... -python -m venv .venv || ( - echo Failed to create virtual environment - exit /b 1 -) - -:: Step 2: Install requirements -echo Installing dependencies... -call .venv\Scripts\activate.bat -pip install -r requirements.txt || ( - echo Failed to install requirements - exit /b 1 -) - -:: Step 3: Create bin\ally.bat -echo Creating launcher script... -set "CURR_DIR=%CD%" -if not exist bin mkdir bin - -( - echo @echo off - echo "%CURR_DIR%\.venv\Scripts\activate.bat" ^&^& python "%CURR_DIR%\main.py" %%* -) > bin\ally.bat - -:: Step 4: Add bin to PATH (user scope) -set "BIN_DIR=%CURR_DIR%\bin" -for /f "tokens=2*" %%A in ('reg query "HKCU\Environment" /v PATH 2^>nul') do set "USER_PATH=%%B" - -echo ;%PATH%; | find /i ";%BIN_DIR%;" >nul -if errorlevel 1 ( - if not defined USER_PATH ( - reg add "HKCU\Environment" /v PATH /t REG_EXPAND_SZ /d "%BIN_DIR%" /f >nul - ) else ( - reg add "HKCU\Environment" /v PATH /t REG_EXPAND_SZ /d "%USER_PATH%;%BIN_DIR%" /f >nul - ) - echo Added %BIN_DIR% to PATH. Restart your terminal or log off/on. -) else ( - echo %BIN_DIR% is already in PATH. -) - -echo === Setup complete! You can now run "ally" in a new terminal window === -pause diff --git a/setup.ps1 b/setup.ps1 new file mode 100644 index 0000000..f06b50d --- /dev/null +++ b/setup.ps1 @@ -0,0 +1,56 @@ +$ErrorActionPreference = "Stop" + +Write-Host "=== Setting up Ally (Windows) ===" -ForegroundColor Cyan + +# script directory +$InstallDir = $PSScriptRoot + +# =========================== Step 1: Install requirements ================================= + +Write-Host "`nInstalling dependencies..." -ForegroundColor Yellow + +if (-not (Get-Command "uv" -ErrorAction SilentlyContinue)) { + Write-Error "uv not found. Please install it by running: irm https://astral.sh/uv/install.ps1 | iex" + exit 1 +} + +Set-Location -Path $InstallDir + +if (-not (Test-Path "pyproject.toml")) { + uv init | Out-Null +} + +cmd /c "uv add -r requirements.txt" | Out-Null + +# =========================== Step 2: Create wrapper (ally.cmd) ============================ + +Write-Host "`nCreating launcher script..." -ForegroundColor Yellow + +# wrapper script +$LauncherPath = Join-Path -Path $InstallDir -ChildPath "ally.bat" +$PythonPath = Join-Path -Path $InstallDir -ChildPath ".venv\Scripts\python.exe" +$MainScript = Join-Path -Path $InstallDir -ChildPath "main.py" + +$BatchContent = "@echo off`r`n`"$PythonPath`" `"$MainScript`" %*" + +Set-Content -Path $LauncherPath -Value $BatchContent +Write-Host "Launcher created at: $LauncherPath" + +# =========================== Step 3: Add to PATH ========================================== + +Write-Host "`nConfiguring PATH..." -ForegroundColor Yellow + +# Get current User PATH +$CurrentPath = [Environment]::GetEnvironmentVariable("Path", "User") + +# Check if the install directory is already in the PATH +if ($CurrentPath -split ';' -notcontains $InstallDir) { + # Append the directory to the User PATH + $NewPath = "$CurrentPath;$InstallDir" + [Environment]::SetEnvironmentVariable("Path", $NewPath, "User") + Write-Host "Success! Added $InstallDir to your User PATH." -ForegroundColor Green + Write-Host "NOTE: You must close and reopen your terminal for the 'ally' command to work." -ForegroundColor Magenta +} else { + Write-Host "Path already configured." -ForegroundColor Green + Write-Host "Setup complete! You can run 'ally' now." -ForegroundColor Green +} From b3890d0ee682a261d6797c14da580464c7b2fd40 Mon Sep 17 00:00:00 2001 From: YassWorks Date: Sat, 3 Jan 2026 20:51:36 +0100 Subject: [PATCH 37/43] more resilience to errors --- main.py | 47 +++++++++++++++++++++++++++++------------------ 1 file changed, 29 insertions(+), 18 deletions(-) diff --git a/main.py b/main.py index 8df4ab1..19cb3ce 100644 --- a/main.py +++ b/main.py @@ -1,9 +1,13 @@ -from dotenv import load_dotenv +try: + from dotenv import load_dotenv + + load_dotenv() -load_dotenv() + from app import CLI, default_ui + from app.utils.logger import logger -from app import CLI, default_ui -from app.utils.logger import logger +except ImportError as e: + print("Unexpected error happened...") from warnings import filterwarnings import os @@ -80,19 +84,26 @@ scraping_method = config.get("scraping_method") or "simple" -client = CLI( - provider=provider, - provider_per_model=provider_per_model, - models=models, - api_key=api_key, - api_key_per_model=api_key_per_model, - embedding_provider=embedding_provider, - embedding_model=embedding_model, - temperatures=temperatures, - system_prompts=system_prompts, - scraping_method=scraping_method, - stream=True, -) +client = None + +try: + client = CLI( + provider=provider, + provider_per_model=provider_per_model, + models=models, + api_key=api_key, + api_key_per_model=api_key_per_model, + embedding_provider=embedding_provider, + embedding_model=embedding_model, + temperatures=temperatures, + system_prompts=system_prompts, + scraping_method=scraping_method, + stream=True, + ) +except Exception as e: + logger.error(f"Failed to initialize the CLI client: {str(e)}") + default_ui.error("Failed to initialize the CLI client. Please check the logs.") + sys.exit(1) ########### run the CLI ########### @@ -103,7 +114,7 @@ def main(): args = sys.argv[1:] client.start_chat(*args) except Exception as e: - logger.error(f"An unexpected error occurred during the chat session: {e}") + logger.error(f"An unexpected error occurred during the chat session: {str(e)}") default_ui.error( "An unexpected error occurred. Please check the logs for details." ) From 8fd76a85b7c747dbc510b11540be944990218df1 Mon Sep 17 00:00:00 2001 From: YassWorks Date: Sat, 3 Jan 2026 20:54:17 +0100 Subject: [PATCH 38/43] error visibility in logs --- main.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/main.py b/main.py index 19cb3ce..ca1d2ea 100644 --- a/main.py +++ b/main.py @@ -101,7 +101,7 @@ stream=True, ) except Exception as e: - logger.error(f"Failed to initialize the CLI client: {str(e)}") + logger.error(f"Failed to initialize the CLI client: {str(e)}", exc_info=e) default_ui.error("Failed to initialize the CLI client. Please check the logs.") sys.exit(1) @@ -114,7 +114,7 @@ def main(): args = sys.argv[1:] client.start_chat(*args) except Exception as e: - logger.error(f"An unexpected error occurred during the chat session: {str(e)}") + logger.error(f"An unexpected error occurred during the chat session: {str(e)}", exc_info=e) default_ui.error( "An unexpected error occurred. Please check the logs for details." ) From 2032727e3a6b76fad583fc10f00425263f47c10c Mon Sep 17 00:00:00 2001 From: YassWorks Date: Sat, 3 Jan 2026 21:20:17 +0100 Subject: [PATCH 39/43] tweak to windows installation --- setup.ps1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.ps1 b/setup.ps1 index f06b50d..9bd9cc4 100644 --- a/setup.ps1 +++ b/setup.ps1 @@ -20,7 +20,7 @@ if (-not (Test-Path "pyproject.toml")) { uv init | Out-Null } -cmd /c "uv add -r requirements.txt" | Out-Null +cmd /c "uv add -q -r requirements.txt" # =========================== Step 2: Create wrapper (ally.cmd) ============================ @@ -52,5 +52,5 @@ if ($CurrentPath -split ';' -notcontains $InstallDir) { Write-Host "NOTE: You must close and reopen your terminal for the 'ally' command to work." -ForegroundColor Magenta } else { Write-Host "Path already configured." -ForegroundColor Green - Write-Host "Setup complete! You can run 'ally' now." -ForegroundColor Green + Write-Host "Setup complete! You can run 'ally' now. You may need to open a new terminal window." -ForegroundColor Green } From a8f5a3b049bc7340594e87d5032d682a69c75b02 Mon Sep 17 00:00:00 2001 From: YassWorks Date: Sat, 3 Jan 2026 21:25:09 +0100 Subject: [PATCH 40/43] docs edits --- README.md | 25 +++++++++++++++++-------- config.json | 4 ++-- 2 files changed, 19 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 7dfbd2f..995834a 100644 --- a/README.md +++ b/README.md @@ -183,22 +183,32 @@ This file (located at `Ally/`) controls Ally's main settings and integrations. - The model name depends on your chosen provider (often found within the provider's dashboard or models tab). +- **`provider_per_model` and `models` with `null` values**: These fields will be automatically filled with the default provider and model specified above. For example, if you set `"brainstormer": null`, it will use the main `"provider"` and `"model"` values. + +- **`system_prompts`**: Leave these as `null` to use Ally's built-in default prompts (recommended). You can override individual prompts by providing custom prompt text. + +- **`embedding_provider`**: Options include `"hf"` (Hugging Face - runs locally) or `"ollama"` (for locally running embedding models). + +- **`embedding_model`**: Examples: `"sentence-transformers/all-MiniLM-L6-v2"` (Hugging Face) or `"all-minilm"` (Ollama). + +- **`scraping_method`**: Use `"simple"` (lightweight) or `"docling"` (more powerful but requires additional dependencies). + ```json { "provider": "openai", "provider_per_model": { "general": "ollama", "code_gen": "anthropic", - "brainstormer": null, // autofilled with 'openai' - "web_searcher": null // autofilled with 'openai' + "brainstormer": null, + "web_searcher": null }, "model": "gpt-4o", "models": { "general": "gpt-oss:20b", "code_gen": "claude-sonnet-3.5", - "brainstormer": null, // autofilled with 'gpt-4o' - "web_searcher": null // autofilled with 'gpt-4o' + "brainstormer": null, + "web_searcher": null }, "temperatures": { @@ -208,17 +218,16 @@ This file (located at `Ally/`) controls Ally's main settings and integrations. "web_searcher": 1 }, "system_prompts": { - // (recommended) leave as-is to use Ally's defaults "general": null, "code_gen": null, "brainstormer": null, "web_searcher": null }, - "embedding_provider": null, // example: "hf" or "ollama" - "embedding_model": null, // example: "sentence-transformers/all-MiniLM-L6-v2" or "all-minilm" + "embedding_provider": null, + "embedding_model": null, - "scraping_method": "simple" // or "docling" + "scraping_method": "simple" } ``` diff --git a/config.json b/config.json index f0732a2..654525f 100644 --- a/config.json +++ b/config.json @@ -1,5 +1,5 @@ { - "provider": "ollama", + "provider": "openai", "provider_per_model": { "general": null, "code_gen": null, @@ -7,7 +7,7 @@ "web_searcher": null }, - "model": "devstral-small-2:24b-cloud", + "model": "gpt-5", "models": { "general": null, "code_gen": null, From cfd274f1bd1e543b2e39f94da9ad6d2a17f8cd65 Mon Sep 17 00:00:00 2001 From: YassWorks Date: Sat, 3 Jan 2026 21:58:59 +0100 Subject: [PATCH 41/43] added chromadb as a core dependency --- Dockerfile | 2 +- app/src/embeddings/db_client.py | 24 +++--------------------- main.py | 7 +++++-- requirements.txt | 3 +++ 4 files changed, 12 insertions(+), 24 deletions(-) diff --git a/Dockerfile b/Dockerfile index 3142dd0..4e2ae4b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -18,7 +18,7 @@ RUN apt-get update \ ##### Install Python dependencies COPY requirements.txt . -RUN uv pip install --system --no-cache -r requirements.txt +RUN uv add -n -r requirements.txt ##### Copy project COPY . . diff --git a/app/src/embeddings/db_client.py b/app/src/embeddings/db_client.py index cfa8fab..0e60b2b 100644 --- a/app/src/embeddings/db_client.py +++ b/app/src/embeddings/db_client.py @@ -45,27 +45,9 @@ def __init__( import chromadb from chromadb.config import Settings except ImportError: - with default_ui.console.status( - "Embedding config found. Installing additional required packages: ChromaDB" - ): - try: - import subprocess - import sys - - # in case the user didn't setup RAG from the beginning - # we lazy-install chromadb when needed - subprocess.check_call( - [sys.executable, "-m", "pip", "install", "chromadb", "-qqq"] - ) - - except Exception as e: - logger.exception("Failed to install ChromaDB package") - default_ui.error( - UI_MESSAGES["errors"]["failed_install_packages"] - ) - raise DBAccessError() - import chromadb - from chromadb.config import Settings + raise ImportError( + "ChromaDB is not installed. Please run 'pip install chromadb' to use the database features." + ) os.makedirs(DB_PATH, exist_ok=True) diff --git a/main.py b/main.py index ca1d2ea..09dc507 100644 --- a/main.py +++ b/main.py @@ -6,7 +6,7 @@ from app import CLI, default_ui from app.utils.logger import logger -except ImportError as e: +except Exception as e: print("Unexpected error happened...") from warnings import filterwarnings @@ -114,7 +114,10 @@ def main(): args = sys.argv[1:] client.start_chat(*args) except Exception as e: - logger.error(f"An unexpected error occurred during the chat session: {str(e)}", exc_info=e) + logger.error( + f"An unexpected error occurred during the chat session: {str(e)}", + exc_info=e, + ) default_ui.error( "An unexpected error occurred. Please check the logs for details." ) diff --git a/requirements.txt b/requirements.txt index b8b5b84..f7333b1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,6 +7,9 @@ langchain-groq==1.1.1 langchain-google-genai==4.1.2 langchain-ollama==1.0.1 +# Vector Databases +chromadb==1.4.0 + # LangChain Framework langgraph==1.0.5 langgraph-checkpoint-sqlite==3.0.1 From 4394e0710e79f33d2acf800e73ea41545104490d Mon Sep 17 00:00:00 2001 From: YassWorks Date: Sat, 3 Jan 2026 22:11:35 +0100 Subject: [PATCH 42/43] docling support discontinued --- README.md | 7 +- app/src/cli/cli.py | 15 +-- .../embeddings/scrapers/docling_scraper.py | 120 ------------------ app/src/embeddings/scrapers/docling_setup.py | 76 ----------- main.py | 1 - 5 files changed, 5 insertions(+), 214 deletions(-) delete mode 100644 app/src/embeddings/scrapers/docling_scraper.py delete mode 100644 app/src/embeddings/scrapers/docling_setup.py diff --git a/README.md b/README.md index 995834a..f541bce 100644 --- a/README.md +++ b/README.md @@ -191,7 +191,7 @@ This file (located at `Ally/`) controls Ally's main settings and integrations. - **`embedding_model`**: Examples: `"sentence-transformers/all-MiniLM-L6-v2"` (Hugging Face) or `"all-minilm"` (Ollama). -- **`scraping_method`**: Use `"simple"` (lightweight) or `"docling"` (more powerful but requires additional dependencies). +- **`scraping_method`**: `"simple"` is the only option for now. More powerful options coming in future versions. ```json { @@ -231,9 +231,7 @@ This file (located at `Ally/`) controls Ally's main settings and integrations. } ``` -> **Note**: Docling is _heavy_. And requires lots of dependencies. It's recommended to go with the local install if you wish to use Docling. - -> **Alternatively**, you could setup a volume (for the parsing and the embedding models) between your machine and the container so that models are persisted across sessions. See below for information where the models are stored inside the container by default. +> **Note**: You could setup a volume for the embedding models between your machine and the container so that models are persisted across sessions. See below for information where the models are stored inside the container by default. ### 3. Configure `.env` in `/Ally` @@ -293,7 +291,6 @@ Use `ally -h` for more help. | `ALLY_HISTORY_DIR` | Controls where Ally stores its history. | | `ALLY_DATABASE_DIR` | Controls where Ally stores its database. | | `ALLY_EMBEDDING_MODELS_DIR` | Controls where Ally stores its embedding models (Hugging Face). | -| `ALLY_PARSING_MODELS_DIR` | Controls where Ally stores its parsing models used by Docling. | Defaults are: diff --git a/app/src/cli/cli.py b/app/src/cli/cli.py index 4a6c4fc..4e426e3 100644 --- a/app/src/cli/cli.py +++ b/app/src/cli/cli.py @@ -86,19 +86,10 @@ def __init__( self.embedding_function = None self.rag_available = False - match scraping_method.lower(): + # simple scraper (only one available for now) + from app.src.embeddings.scrapers.simple_scraper import SimpleScraper - case "docling": - from app.src.embeddings.scrapers.docling_scraper import DoclingScraper - - self.scraper = DoclingScraper() - - case ( - _ - ): # default to simple scraper, even for unrecognized methods as it doesn't matter really. - from app.src.embeddings.scrapers.simple_scraper import SimpleScraper - - self.scraper = SimpleScraper() + self.scraper = SimpleScraper() try: self.general_agent: BaseAgent = AgentFactory.create_agent( diff --git a/app/src/embeddings/scrapers/docling_scraper.py b/app/src/embeddings/scrapers/docling_scraper.py deleted file mode 100644 index c2da138..0000000 --- a/app/src/embeddings/scrapers/docling_scraper.py +++ /dev/null @@ -1,120 +0,0 @@ -from app.src.core.ui import default_ui -from app.src.embeddings.scrapers.docling_setup import ARTIFACTS_PATH, setup -from app.src.embeddings.scrapers.abstract_scraper import Scraper -from app.utils.constants import REGULAR_FILE_EXTENSIONS -from app.src.embeddings.rag_errors import SetupFailedError, ScrapingFailedError -from pathlib import Path -import datetime - - -_RETRIED_DOCLING_SETUP = False -_DOC_CONVERTER = None - - -class DoclingScraper(Scraper): - - def get_converter(self): - """Initialize and return a Docling DocumentConverter.""" - setup_flag_file = Path(ARTIFACTS_PATH) / ".setup_flag" - if not setup_flag_file.exists(): - try: - setup(path=ARTIFACTS_PATH) - setup_flag_file.touch(exist_ok=True) # for thread safety - except: - raise SetupFailedError() - - from docling.document_converter import DocumentConverter - from docling.datamodel.base_models import InputFormat - from docling.datamodel.pipeline_options import ( - PdfPipelineOptions, - EasyOcrOptions, - ) - from docling.datamodel.pipeline_options import smolvlm_picture_description - from docling.document_converter import ( - PdfFormatOption, - ImageFormatOption, - WordFormatOption, - ) - - pipeline_options = PdfPipelineOptions( - artifacts_path=ARTIFACTS_PATH, - do_ocr=True, - ocr_options=EasyOcrOptions(force_full_page_ocr=True), - ) - - pipeline_options.do_table_structure = True - pipeline_options.do_formula_enrichment = True - pipeline_options.do_code_enrichment = True - pipeline_options.do_picture_description = True - pipeline_options.generate_picture_images = True - pipeline_options.do_picture_classification = True - - pipeline_options.picture_description_options = smolvlm_picture_description - - doc_converter = DocumentConverter( - format_options={ - InputFormat.PDF: PdfFormatOption(pipeline_options=pipeline_options), - InputFormat.IMAGE: ImageFormatOption(pipeline_options=pipeline_options), - InputFormat.DOCX: WordFormatOption(pipeline_options=pipeline_options), - } - ) - - return doc_converter - - def scrape(self, file_path: str | Path) -> dict: - """Extract text and metadata from a file using Docling.""" - global _DOC_CONVERTER - - if _DOC_CONVERTER is None: - _DOC_CONVERTER = self.get_converter() - - doc_converter = _DOC_CONVERTER - - if any(file_path.lower().endswith(x) for x in REGULAR_FILE_EXTENSIONS): - content = self.read_regular_file(file_path) - return { - "content": content, - "metadata": { - "file_path": Path(file_path).as_posix(), - "mod_date": datetime.datetime.fromtimestamp( - Path(file_path).stat().st_mtime - ).isoformat(), - "hash": self.get_hash(file_path), - }, - } - - try: - with default_ui.console.status(f"Scraping '{file_path}'..."): - doc = doc_converter.convert(file_path).document - except: - # trying to redownload models ONCE if first scraping fails - global _RETRIED_DOCLING_SETUP - if _RETRIED_DOCLING_SETUP: - raise ScrapingFailedError() - - default_ui.warning( - "Scraping failed. Attempting to redownload parsing models and retry..." - ) - - _RETRIED_DOCLING_SETUP = True - try: - setup(path=ARTIFACTS_PATH) - except: - raise SetupFailedError() - - try: - with default_ui.console.status(f"Scraping '{file_path}'..."): - doc = doc_converter.convert(file_path).document - except: - raise ScrapingFailedError() - - return { - "content": doc.export_to_markdown(), - "metadata": { - "file_path": Path(file_path).as_posix(), - "mod_date": datetime.datetime.fromtimestamp( - Path(file_path).stat().st_mtime - ).isoformat(), - "hash": self.get_hash(file_path), - }, - } diff --git a/app/src/embeddings/scrapers/docling_setup.py b/app/src/embeddings/scrapers/docling_setup.py deleted file mode 100644 index 291280f..0000000 --- a/app/src/embeddings/scrapers/docling_setup.py +++ /dev/null @@ -1,76 +0,0 @@ -from app.src.core.ui import default_ui -from app.utils.constants import DEFAULT_PATHS -from app.src.helpers.valid_dir import validate_dir_name -from app.utils.logger import logger -from pathlib import Path -import os - - -# configure parsing models path -ARTIFACTS_PATH = "" -if "ALLY_PARSING_MODELS_DIR" in os.environ: - ARTIFACTS_PATH = Path(os.getenv("ALLY_PARSING_MODELS_DIR")) - if not validate_dir_name(str(ARTIFACTS_PATH)): - ARTIFACTS_PATH = "" - default_ui.warning( - "Invalid directory path found in $ALLY_PARSING_MODELS_DIR. Reverting to default path." - ) - -if not ARTIFACTS_PATH: - ARTIFACTS_PATH = DEFAULT_PATHS["parsing_models"] - if os.name == "nt": - ARTIFACTS_PATH = Path(os.path.expandvars(ARTIFACTS_PATH)) - else: - ARTIFACTS_PATH = Path(os.path.expanduser(ARTIFACTS_PATH)) - - -def setup(path: str = ARTIFACTS_PATH) -> None: - """ - Prepare environment and download Docling parsing models. - - Ensures required packages are installed and downloads parsing artifacts - (parsing models, EasyOCR model, and smolvlm). - - Parameters - - path (str | Path): target directory for model files (defaults to ARTIFACTS_PATH). - - Notes - - OpenGL drivers may be required for some Docling components. - - Installs the `docling` package (uses CPU PyTorch wheel by default). - - Raises an exception on failure. - """ - - with default_ui.console.status( - "Docling setup found. Installing additional required packages: Docling" - ): - import subprocess - import sys - - subprocess.check_call( - [ - sys.executable, - "-m", - "pip", - "install", - "--extra-index-url", - "https://download.pytorch.org/whl/cpu", # CPU-only PyTorch. Adjust if needed. - "docling==2.55.1", # TODO: fix - "-qqq", - ] - ) - - try: - os.makedirs(path, exist_ok=True) - with default_ui.console.status("Downloading parsing models..."): - from docling.utils.model_downloader import download_models - - download_models( - output_dir=path, - progress=False, - with_smolvlm=True, - ) - - except Exception as e: - logger.exception("Failed to download parsing models") - default_ui.error("Failed to download parsing models. Check logs for details.") - raise diff --git a/main.py b/main.py index 09dc507..d8d9005 100644 --- a/main.py +++ b/main.py @@ -18,7 +18,6 @@ logging.basicConfig(level=logging.CRITICAL) filterwarnings("ignore", category=Warning, module="torch") -filterwarnings("ignore", category=Warning, module="docling") filterwarnings("ignore", category=Warning, module="huggingface_hub") filterwarnings("ignore", category=Warning, module="onnxruntime") From 1ed8a1f03feb327947f3aae659cbee2146571595 Mon Sep 17 00:00:00 2001 From: YassWorks Date: Sat, 3 Jan 2026 22:33:54 +0100 Subject: [PATCH 43/43] added torch & transformers to requirements.txt --- app/utils/ui_messages.py | 8 ++++---- requirements.txt | 4 ++++ 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/app/utils/ui_messages.py b/app/utils/ui_messages.py index 5bc55df..cd5c542 100644 --- a/app/utils/ui_messages.py +++ b/app/utils/ui_messages.py @@ -118,10 +118,10 @@ }, # Usage Messages "usage": { - "embed": "Usage: /embed ", - "index": "Usage: /index ", - "unindex": "Usage: /unindex ", - "delete": "Usage: /delete ", + "embed": "Usage: /embed 'directory_path' 'collection_name'", + "index": "Usage: /index 'collection_name'", + "unindex": "Usage: /unindex 'collection_name'", + "delete": "Usage: /delete 'collection_name'", }, # Success Messages "success": { diff --git a/requirements.txt b/requirements.txt index f7333b1..2287ddb 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,6 +7,10 @@ langchain-groq==1.1.1 langchain-google-genai==4.1.2 langchain-ollama==1.0.1 +# Embedding Models +torch==2.9.1 +transformers==4.57.3 + # Vector Databases chromadb==1.4.0