Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ Dandy is a Python Artificial Intelligence Framework that simplifies the developm
- **Imports**: Follow this order: standard library → third-party → local (dandy.*)
- **Type hints**: Always use type hints for function signatures
- **Imports**: Use absolute imports (e.g., `from dandy.bot.bot import Bot`)
- **Verbose**: Always use verbose names (e.g., `sub_command` not `subcmd`)

### Naming Conventions

Expand Down
10 changes: 5 additions & 5 deletions dandy/bot/bot.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ def __init__(
llm_config: str | None = None,
llm_temperature: float | None = None,
**kwargs,
):
) -> None:
super().__init__(
llm_config=llm_config,
llm_temperature=llm_temperature,
Expand All @@ -37,7 +37,7 @@ def __init__(

self.__post_init__()

def __init_subclass__(cls):
def __init_subclass__(cls) -> None:
super().__init_subclass__()

if ABC not in cls.__bases__:
Expand All @@ -52,15 +52,15 @@ def __getattribute__(self: Self, name: str) -> Any: # noqa: N807
and not hasattr(attr, '_wrapped')
):
wrapped = record_process_wrapper(self, attr)
wrapped._wrapped = True
setattr(wrapped, '_wrapped', True)

return wrapped

return attr

cls.__getattribute__ = __getattribute__

def __post_init__(self): # noqa: B027
def __post_init__(self) -> None: # noqa: B027
pass

@classmethod
Expand All @@ -84,5 +84,5 @@ def process(
def process_to_future(self, *args, **kwargs) -> AsyncFuture:
return process_to_future(self.process, *args, **kwargs)

def reset(self):
def reset(self) -> None:
super().reset()
12 changes: 6 additions & 6 deletions dandy/cache/memory/cache.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from typing import OrderedDict, Any

import dandy.constants
from dandy.cache.cache import BaseCache
from dandy.constants import CACHE_DEFAULT_NAME

_memory_cache = {}

Expand All @@ -23,23 +23,23 @@ def __len__(self) -> int:
def get(self, key: str) -> Any | None:
return self._cache.get(key)

def set(self, key: str, value: Any):
def set(self, key: str, value: Any) -> None:
self._cache[key] = value
self.clean()

def clean(self):
def clean(self) -> None:
if len(self._cache) > self.limit:
self._cache.popitem(last=False)

@classmethod
def clear(cls, cache_name: str = dandy.constants.CACHE_DEFAULT_NAME):
def clear(cls, cache_name: str = CACHE_DEFAULT_NAME) -> None:
if cache_name in _memory_cache:
_memory_cache[cache_name].clear()

@classmethod
def clear_all(cls):
def clear_all(cls) -> None:
_memory_cache.clear()

@classmethod
def destroy_all(cls):
def destroy_all(cls) -> None:
cls.clear_all()
4 changes: 2 additions & 2 deletions dandy/cache/memory/decorators.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
from functools import wraps
from typing import Callable

import dandy.constants
from dandy.cache.decorators import cache_decorator_function
from dandy.cache.memory.cache import MemoryCache
from dandy.conf import settings
from dandy.constants import CACHE_DEFAULT_NAME


def cache_to_memory(
cache_name: str = dandy.constants.CACHE_DEFAULT_NAME,
cache_name: str = CACHE_DEFAULT_NAME,
limit: int | None = None,
) -> Callable:
if limit is None:
Expand Down
5 changes: 2 additions & 3 deletions dandy/cache/sqlite/cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,9 @@
import sqlite3
from typing import Any

import dandy.constants
from dandy.cache.cache import BaseCache
from dandy.cache.sqlite.connection import SqliteConnection
from dandy.constants import SQLITE_CACHE_TABLE_NAME, SQLITE_CACHE_DB_NAME
from dandy.constants import SQLITE_CACHE_TABLE_NAME, SQLITE_CACHE_DB_NAME, CACHE_DEFAULT_NAME


class SqliteCache(BaseCache):
Expand Down Expand Up @@ -108,7 +107,7 @@ def clean(self):
connection.commit()

@classmethod
def clear(cls, cache_name: str = dandy.constants.CACHE_DEFAULT_NAME):
def clear(cls, cache_name: str = CACHE_DEFAULT_NAME):
if cls._table_exists():
with SqliteConnection(SQLITE_CACHE_DB_NAME) as connection:
cursor = connection.cursor()
Expand Down
4 changes: 2 additions & 2 deletions dandy/cache/sqlite/decorators.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
from functools import wraps
from typing import Callable

import dandy.constants
from dandy.cache.decorators import cache_decorator_function
from dandy.cache.sqlite.cache import SqliteCache
from dandy.conf import settings
from dandy.constants import CACHE_DEFAULT_NAME


def cache_to_sqlite(
cache_name: str = dandy.constants.CACHE_DEFAULT_NAME,
cache_name: str = CACHE_DEFAULT_NAME,
limit: int | None = None
) -> Callable:
if limit is None:
Expand Down
3 changes: 1 addition & 2 deletions dandy/cache/tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,10 @@ def generate_cache_key(func: object, *args, **kwargs) -> str:
hashable_kwargs,
)

hash_key = hashlib.shake_128(
return hashlib.shake_128(
str(hashable_tuple).encode()
).hexdigest(16)

return hash_key


def convert_to_hashable_str(obj: Any, hash_layer: int = 1) -> str:
Expand Down
10 changes: 3 additions & 7 deletions dandy/cli/actions/action.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,15 @@ class BaseAction(ABC):
description: str
calls: tuple[str, ...]

def __post_init__(self):
def __init_subclass__(cls, **kwargs) -> None:
check_attrs = ['name', 'description', 'calls']
for attr in check_attrs:
if not hasattr(self, attr):
if not hasattr(cls, attr):
message = f'Command `{attr}` is required'
raise ValueError(message)

@abstractmethod
def help(self):
def help(self) -> None:
raise NotImplementedError

@classmethod
Expand All @@ -32,7 +32,3 @@ def name_gerund(cls) -> str:
@abstractmethod
def run(self, user_input: str) -> str:
raise NotImplementedError

@abstractmethod
def render(self):
raise NotImplementedError
File renamed without changes.
144 changes: 144 additions & 0 deletions dandy/cli/actions/bot/action.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
import importlib
import inspect
import sys
from pathlib import Path
from typing import Callable

from dandy import Prompt
from dandy.bot.bot import Bot
from dandy.cli.actions.action import BaseAction
from dandy.cli.intelligence.bots.source_code_bot import SourceCodeBot
from dandy.cli.session import session
from dandy.cli.tui.tui import tui
from dandy.file.utils import make_directory


class BotAction(BaseAction):
name = 'Bot'
description = 'Bots at your service!'
calls = ('b', 'bot')

def __init__(self) -> None:
self.bots_path = Path(session.project_dandy_path, 'bots')

make_directory(self.bots_path)

self.sub_commands_methods: dict[str, Callable] = {
'build': self.build_bot,
'list': self.list_bots,
'help': self.help,
'run': self.run_bot,
}

@property
def help_string(self) -> str:
return f"""Usage: /bot run <BotName> [optional inline prompt]
If no prompt, enter multi-line (end with /end).
Other subcommands: {self.sub_commands_methods.keys()}
"""

def build_bot(self, user_input: str) -> str:
parts = user_input.split()

if len(parts) < 2:
bot_description = tui.get_user_input(question='Please describe the bot you want to build')
else:
bot_description = " ".join(parts[2:])

start_time = tui.printer.start_task('Building', 'create a new bot')

code_reference_prompt = (
Prompt()
.module_source('dandy.bot.bot')
.lb()
.module_source('dandy.llm.service')
.lb()
.module_source('dandy.file.service')
.lb()
.module_source('dandy.http.service')
.lb()
.module_source('dandy.intel.service')
.lb()
.sub_heading('Tutorials')
.lb()
.file(Path(session.project_base_path, 'docs', 'tutorials', 'bots.md'))
.lb()
.text('The file name for this code should be postfixed with `_bot` example `task_reviewer_bot.py`')
)

source_code_intel = SourceCodeBot().process(
user_input=bot_description,
code_reference_prompt=code_reference_prompt
)

source_code_intel.write_to_directory(self.bots_path)

tui.printer.end_task(start_time)

return f'Bot created at "{Path(self.bots_path, source_code_intel.file_name_with_extension)}"'

def help(self) -> None:
print(self.help_string)

def list_bots(self, user_input: str) -> str:
assert user_input
return "Available bots:\n" + '\n'.join(self.bot_files) if self.bot_files else "No bots found."

def run(self, user_input: str) -> str:
parts = user_input.split()

sub_command = parts[0].lower() if len(parts) > 0 else None

if sub_command in self.sub_commands_methods:
return self.sub_commands_methods[sub_command](
user_input=user_input
)

return self.help_string

def run_bot(self, user_input: str) -> str:
parts = user_input.split()

if len(parts) < 2:
return "Error: Missing bot name. Usage: /bot run <BotName>"

module_name = parts[1]

# Add bots dir to sys.path for import
sys.path.insert(0, str(self.bots_path))

try:
module = importlib.import_module(module_name)
except ImportError as e:
return f"Error: Could not import bot module '{module_name}.py' from {self.bots_path}: {e}"

# Find the first Bot subclass in the module
bot_class: type | None = None

for _, obj in inspect.getmembers(module):
if inspect.isclass(obj) and issubclass(obj, Bot) and obj != Bot:
bot_class: type = obj
break

if bot_class is None:
return f"Error: No Bot subclass found in {module_name}.py"

output = ''

try:
if issubclass(bot_class, Bot):
bot_class().process()

output = f'{bot_class.__name__} ran successfully!'
except Exception as e:
output = f"Bot failed with Error: {e}"

return output

@property
def bot_files(self) -> list[str] | None:
return [
file.stem.replace('_bot', 'Bot').title()
for file in self.bots_path.glob('*.py')
if file.stem != '__init__'
]
28 changes: 0 additions & 28 deletions dandy/cli/actions/build/action.py

This file was deleted.

Empty file.
25 changes: 25 additions & 0 deletions dandy/cli/actions/code/action.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
from time import sleep

from dandy.cli.actions.action import BaseAction
from dandy.cli.tui.tui import tui


class CodeAction(BaseAction):
name = 'Code'
description = 'Code something inside your project!'
calls = ('c', 'code')

def help(self) -> None:
print('Chat help')

def run(self, user_input: str) -> str:
if not user_input:
user_input = tui.get_user_input(question='What would you like to code?')

start_time = tui.printer.start_task('Coding', 'some sleepy time')

sleep(1.0)

tui.printer.end_task(start_time)

return f'Coding {user_input}...'
Empty file.
Empty file.
5 changes: 5 additions & 0 deletions dandy/cli/actions/code/intelligence/bots/coding_bot.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from dandy import Bot


class CodingBot(Bot):
pass
Loading
Loading