# Dracula 🧛

A simple, elegant Python library and Mini SDK for Google Gemini with powerful features.
Built for developers who want to integrate AI into their projects without
dealing with complex API setup.

## Installation
```bash
pip install dracula-ai
```

## Quick Start
```python
from dracula import Dracula, GeminiModel
from dotenv import load_dotenv
import os

load_dotenv()

ai = Dracula(api_key=os.getenv("GEMINI_API_KEY"))
response = ai.chat("Hello, who are you?")
print(response)
```

## Parameters

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| api_key | str | required | Your Google Gemini API key |
| model | GeminiModel or str | GeminiModel.FLASH | Gemini model to use |
| max_messages | int | 10 | Maximum number of messages to remember |
| prompt | str | "You are a helpful assistant." | System prompt |
| temperature | float | 1.0 | Response creativity (0.0 - 2.0) |
| max_output_tokens | int | 8192 | Maximum response length |
| stats_filepath | str | "dracula_stats.json" | Path to save usage stats |
| language | str | "English" | Language for responses |
| logging | bool | False | Enable or disable logging |
| log_level | str | "DEBUG" | Logging level |
| log_file | str | None | Path to log file |
| log_max_bytes | int | 5MB | Maximum log file size |
| log_backup_count | int | 5 | Number of backup log files |
| tools | list | None | List of tools to register |

## Features

### 💬 Text Chat
> The most basic feature of Dracula. Send a message to Gemini and get a
> response back. Every message you send and every response you receive is
> automatically stored in memory, so Gemini always knows the context of
> your conversation.
```python
ai = Dracula(api_key="your-api-key")
response = ai.chat("What is Python?")
print(response)
```

### 🌊 Streaming
> Normally, Dracula waits for Gemini to finish generating the full response
> before returning it. Streaming changes this behavior — instead of waiting,
> you receive the response word by word as it is being generated, just like
> ChatGPT does. This is especially useful for long responses or when you want
> a more interactive feel in your app.
```python
for chunk in ai.stream("Tell me a long story."):
    print(chunk, end="", flush=True)
```

### 🧠 Conversation Memory
> Dracula automatically remembers the conversation history so Gemini can
> refer back to previous messages. For example, if you tell it your name
> in one message, it will remember it in the next. You can control how many
> messages are remembered with the `max_messages` parameter. When you want
> to start a completely fresh conversation, use `clear_memory()`.
```python
ai.chat("My name is Ahmet.")
response = ai.chat("What is my name?")
print(response)  # It remembers! ✅

ai.clear_memory()  # Wipe memory
```

### 💾 Save & Load History
> By default, conversation history only exists while your program is running.
> Once you stop the program, the history is lost. Save & Load History solves
> this by letting you save the conversation to a JSON file and reload it
> later, so your AI can continue right where it left off — even in a
> completely new run of your program.
```python
ai.save_history("conversation.json")

# Later, in a new run of your program:
ai.load_history("conversation.json")
```

### 📜 Pretty Print History
> `get_history()` returns the raw conversation history as a list of
> dictionaries, which can be hard to read. `print_history()` formats the
> same data into a clean, human-readable layout with clear labels for each
> message, making it much easier to follow the conversation at a glance.
```python
ai.print_history()
```

### 🎭 System Prompt
> The system prompt is a set of instructions you give to Gemini before the
> conversation starts. It defines the AI's personality, role, and behavior
> for the entire conversation. For example, you can tell it to act as a
> pirate, a chef, a formal assistant, or anything else you can imagine.
> The user will never see this prompt — it works silently in the background.
```python
ai = Dracula(
    api_key="your-api-key",
    prompt="You are a pirate who answers everything dramatically."
)

# You can also change it anytime during the conversation:
ai.set_prompt("You are now a formal assistant.")
```

### 🌡️ Temperature Control
> Temperature controls how creative and random Gemini's responses are.
> A low temperature (close to 0.0) makes responses more focused,
> predictable, and factual — great for technical questions. A high
> temperature (close to 2.0) makes responses more creative, surprising,
> and varied — great for storytelling or brainstorming. The default value
> of 1.0 is a balanced middle ground.
```python
ai = Dracula(api_key="your-api-key", temperature=0.2)  # Focused
ai = Dracula(api_key="your-api-key", temperature=1.8)  # Creative

# You can also change it anytime:
ai.set_temperature(0.5)
```

### 📏 Max Output Tokens
> Tokens are small chunks of text — roughly one token per word.
> `max_output_tokens` controls the maximum length of Gemini's responses.
> If you want short, concise answers set it low. If you want long, detailed
> responses set it high. The default is 8192 which is large enough for most
> use cases.
```python
ai = Dracula(api_key="your-api-key", max_output_tokens=256)  # Short
ai = Dracula(api_key="your-api-key", max_output_tokens=8192) # Long

# You can also change it anytime:
ai.set_max_output_tokens(512)
```

### 🌍 Response Language
> By default Gemini responds in whatever language the user writes in.
> The language feature overrides this behavior and forces Gemini to always
> respond in a specific language, regardless of what language the user
> writes in. Use "Auto" to let Gemini detect the language automatically.
```python
ai = Dracula(api_key="your-api-key", language="Turkish")
response = ai.chat("Hello!")
print(response)  # Merhaba! ✅

# Auto detect language
ai = Dracula(api_key="your-api-key", language="Auto")

# Change it anytime:
ai.set_language("Spanish")
```

### 🤖 GeminiModel Enum
> Instead of typing model names as strings which can cause typos and break
> when Google updates model names, Dracula provides a GeminiModel enum with
> all available models. You can still use strings for advanced use cases,
> but the enum is the recommended approach.
```python
from dracula import Dracula, GeminiModel

ai = Dracula(api_key="your-api-key", model=GeminiModel.FLASH)
ai = Dracula(api_key="your-api-key", model=GeminiModel.PRO)
ai = Dracula(api_key="your-api-key", model=GeminiModel.FLASH_LITE)

# Change model anytime:
ai.set_model(GeminiModel.PRO)

# Discover all available models in real time:
print(ai.list_available_models())
```

### 🛠️ Function Calling / Tools
> Function calling is one of the most powerful features of Dracula.
> It lets you give Gemini access to your own Python functions — like
> checking the weather, searching the web, querying a database, or
> anything else you can write in Python. Gemini will automatically
> decide when and how to call your functions based on the user's message.
> Use auto_call=True to let Dracula handle everything automatically,
> or auto_call=False to handle tool calls yourself.
```python
from dracula import Dracula, tool, ToolResult, GeminiModel

@tool(description="Get the current weather for a city")
def get_weather(city: str) -> str:
    return f"It's 25°C and sunny in {city}"

@tool(description="Search the web for information")
def search_web(query: str) -> str:
    return f"Search results for: {query}"

ai = Dracula(
    api_key="your-api-key",
    tools=[get_weather, search_web]
)

# Auto call — Dracula handles everything automatically
response = ai.chat("What's the weather in Istanbul?")
print(response)
# "The weather in Istanbul is currently 25°C and sunny!"

# Manual call — you handle the tool call yourself
result = ai.chat("What's the weather in Ankara?", auto_call=False)
if result.requires_tool_call:
    print(f"Tool: {result.tool_name}")   # get_weather
    print(f"Args: {result.tool_args}")   # {"city": "Ankara"}

# Add tools after initialization
ai.add_tool(search_web)

# List registered tools
print(ai.list_tools())  # ["get_weather", "search_web"]
```

### 📊 Usage Stats
> Dracula automatically tracks how many messages you've sent and received,
> and how many characters were exchanged in total. These stats are saved
> to a JSON file and persist across sessions, so they accumulate over time.
```python
print(ai.get_stats())
# {
#   "total_messages": 5,
#   "total_responses": 5,
#   "total_characters_sent": 120,
#   "total_characters_received": 3400
# }

ai.reset_stats()
```

### 📝 Logging
> Dracula has a built-in logging system that you can turn on or off.
> By default logging is completely silent. When enabled, it shows detailed
> information about what Dracula is doing internally — useful for debugging.
> You can also save logs to a file with automatic log rotation, which means
> when the log file gets too big, a new one is created automatically.
```python
# Enable terminal logging
ai = Dracula(api_key="your-api-key", logging=True)

# Enable logging with a specific level
ai = Dracula(api_key="your-api-key", logging=True, log_level="WARNING")

# Enable file logging with rotation
ai = Dracula(
    api_key="your-api-key",
    logging=True,
    log_file="dracula.log",
    log_max_bytes=1 * 1024 * 1024,  # 1MB per file
    log_backup_count=3               # keep 3 backups
)

# Change log level anytime
ai.set_log_level("ERROR")
```

### 🔗 Chainable Methods
> Instead of calling each setter method on a separate line, chainable methods
> let you combine multiple settings into a single, clean line of code.
```python
ai.set_prompt("You are a chef.").set_temperature(0.9).set_language("Turkish")
```

### 🧹 Context Manager
> A context manager lets you use Dracula with Python's `with` statement.
> The benefit is automatic cleanup — when the `with` block ends, Dracula
> automatically clears the memory and resets the stats.
```python
with Dracula(api_key="your-api-key") as ai:
    ai.chat("Hello!")
    ai.print_history()
# Memory and stats automatically reset here ✅
```

### 🎭 Role Playing Mode
> Dracula comes with a set of built-in personas that you can switch between
> instantly. Each persona has its own predefined prompt, temperature, and
> language settings.
```python
print(ai.list_personas())
# ['assistant', 'pirate', 'chef', 'shakespeare', 'scientist', 'comedian']

ai.set_persona("pirate")
print(ai.chat("Hello, who are you?"))
# Arrr, I be a fearsome pirate! 🏴‍☠️
```

### 🖥️ Desktop Chat UI
> Dracula comes with a ready-made PyQt6 desktop chat UI that you can use
> in your Windows apps. It supports dark and light themes, markdown
> rendering, and syntax highlighting for code blocks.
```python
from dracula import Dracula, launch

ai = Dracula(api_key="your-api-key")

launch(ai, title="My AI App", theme="dark")   # Dark theme
launch(ai, title="My AI App", theme="light")  # Light theme
```

### 🖥️ CLI Tool
> Dracula comes with a built-in CLI tool that lets you chat with Gemini
> directly from the terminal without writing any Python code.
```bash
dracula chat "Hello!"
dracula chat "Tell me a joke" --persona comedian
dracula chat "Merhaba" --language Turkish --stream
dracula list-personas
dracula stats
dracula --version
```

### ⚡ Async Support with AsyncDracula
> For async applications like Discord bots, FastAPI, and Telegram bots,
> use AsyncDracula instead of Dracula. It has all the same features
> but with full async support including async tool calling.
```python
import asyncio
from dracula import AsyncDracula, tool

@tool(description="Get the weather for a city")
async def get_weather(city: str) -> str:
    return f"25°C and sunny in {city}"

async def main():
    async with AsyncDracula(
        api_key="your-api-key",
        tools=[get_weather]
    ) as ai:
        response = await ai.chat("What's the weather in Istanbul?")
        print(response)

        async for chunk in ai.stream("Tell me a story."):
            print(chunk, end="", flush=True)

asyncio.run(main())
```

### 🤖 Discord Bot Example
> Thanks to async support, building an AI-powered Discord bot with
> Dracula takes just a few lines of code.
```python
import discord
from discord.ext import commands
from dracula import AsyncDracula, tool
from dotenv import load_dotenv
import os

load_dotenv()

bot = commands.Bot(command_prefix="!", intents=discord.Intents.all())
ai = AsyncDracula(api_key=os.getenv("GEMINI_API_KEY"))

@bot.command()
async def chat(ctx, *, message: str):
    response = await ai.chat(message)
    await ctx.send(response)

bot.run(os.getenv("DISCORD_TOKEN"))
```

### 🌐 FastAPI Example
> Dracula works perfectly with FastAPI thanks to async support.
```python
from fastapi import FastAPI
from dracula import AsyncDracula
from dotenv import load_dotenv
import os

load_dotenv()

app = FastAPI()
ai = AsyncDracula(api_key=os.getenv("GEMINI_API_KEY"))

@app.get("/chat")
async def chat(message: str):
    response = await ai.chat(message)
    return {"response": response}

@app.get("/stream")
async def stream(message: str):
    from fastapi.responses import StreamingResponse
    return StreamingResponse(
        ai.stream(message),
        media_type="text/plain"
    )
```

## Error Handling
> Dracula provides custom exceptions so you can handle different types of
> errors separately and give your users clear, meaningful error messages.
```python
from dracula import (
    ValidationException,
    ChatException,
    InvalidAPIKeyException,
    PersonaException,
    ToolException
)

try:
    ai = Dracula(api_key="", temperature=5.0)
except ValidationException as e:
    print(f"Validation error: {e}")
except InvalidAPIKeyException as e:
    print(f"API key error: {e}")
except ChatException as e:
    print(f"Chat error: {e}")
except PersonaException as e:
    print(f"Persona error: {e}")
except ToolException as e:
    print(f"Tool error: {e}")
```

## Known Issues

### AsyncDracula — aiohttp connector warning
When using `AsyncDracula`, you may see this warning after your program ends:
```
Exception ignored in: <function BaseApiClient._get_aiohttp_session...>
AttributeError: 'NoneType' object has no attribute 'from_iterable'
```
This is a **known bug in the `google-genai` library** and is not caused by
Dracula. It does not affect functionality in any way.

## Getting Your API Key

1. Go to [https://aistudio.google.com](https://aistudio.google.com)
2. Sign in with your Google account
3. Click "Get API Key"
4. Store it safely in a `.env` file:
```
GEMINI_API_KEY=your-api-key
```

## License

MIT License — feel free to use this in your own projects!

## Author

Your Name