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
126 changes: 120 additions & 6 deletions devops_agent/cli.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import click
from rich.console import Console
from rich.panel import Panel
from pathlib import Path
from rich.prompt import Prompt
from devops_agent.core.devops_agent import execute_devops_agent

console = Console()

Expand All @@ -11,16 +12,33 @@ def cli():
"""DevOps Agent - Your AI-powered DevOps assistant"""
pass


def default_provider() -> str:
return "openai"


@cli.command()
@click.option('--log-file', type=click.Path(exists=True), help='Path to log file to analyze')
@click.option('--provider', type=str, help='Configure the agent with one of the enterprise grade providers like OpenAI, Anthropic, Gemini')
@click.option('--query', type=str, help='Query to ask the DevOps agent')
@click.option('--output', type=click.Path(), help='Output file path (optional)')
@click.option('--format', type=click.Choice(['text', 'json', 'markdown']), default='text', help='Output format')
def run(log_file, query, output, format):
@click.option('--interactive', '-i', is_flag=True, help='Run in interactive mode')
def run(log_file, provider, query, output, format, interactive):
"""Run the DevOps agent with specified options"""

if not provider:
console.print("[yellow]No provider specified, defaulting to openai[/yellow]")
provider = default_provider()

# Interactive mode
if interactive:
run_interactive_mode(provider, output, format)
return

# Single query mode (original behavior)
if not log_file and not query:
console.print("[red]Error: You must provide either --log-file or --query[/red]")
console.print("[red]Error: You must provide either --log-file, --query, or use --interactive mode[/red]")
raise click.Abort()

if log_file and query:
Expand All @@ -37,25 +55,121 @@ def run(log_file, query, output, format):
console.print("[green]✓[/green] Log analysis will be implemented here")

if query:
console.print(f"[yellow]Processing query:[/yellow] {query}")
process_query(provider, query, output, format)


def run_interactive_mode(provider: str, output: str = None, format: str = 'text'):
"""Run the agent in interactive mode with continuous conversation"""

console.print(Panel.fit(
"[bold cyan]DevOps Agent - Interactive Mode[/bold cyan]\n"
"[dim]Type your questions or commands.[/dim]\n"
"[dim]Type 'quit', 'exit', or 'bye' to exit.[/dim]",
border_style="cyan"
))

# Main interactive loop
while True:
try:
# Get user input
user_input = Prompt.ask("\n[bold green]You[/bold green]")

# Check for exit commands
if user_input.lower().strip() in ['quit', 'exit', 'bye', 'q']:
console.print("\n[yellow]Goodbye! 👋[/yellow]")
break

# Skip empty inputs
if not user_input.strip():
continue

# Process the query
console.print(Panel.fit(
"[bold cyan]DevOps Agent[/bold cyan] [dim]Thinking...[/dim]",
border_style="cyan"
))

try:
response = execute_devops_agent(provider=provider, user_query=user_input)
console.print(f"\n[bold cyan]Assistant:[/bold cyan]\n{response}")

# Save to output file if specified
if output:
save_to_file(output, user_input, response, format)
console.print(f"\n[dim]Response saved to {output}[/dim]")

except Exception as e:
console.print(f"\n[red]Error processing query:[/red] {str(e)}")

except KeyboardInterrupt:
console.print("\n\n[yellow]Interrupted. Type 'quit' to exit or continue with your next question.[/yellow]")
continue
except EOFError:
console.print("\n[yellow]Goodbye! 👋[/yellow]")
break


def process_query(provider: str, query: str, output: str = None, format: str = 'text'):
"""Process a single query"""
console.print(f"[yellow]Processing query:[/yellow] {query}")
console.print(Panel.fit(
"[bold cyan]DevOps Agent[/bold cyan] [dim]Thinking...[/dim]",
border_style="cyan"
))

try:
response = execute_devops_agent(provider=provider, user_query=query)
console.print(f"\n[bold cyan]Assistant:[/bold cyan]\n{response}")

if output:
save_to_file(output, query, response, format)
console.print(f"\n[dim]Response saved to {output}[/dim]")

except Exception as e:
console.print(f"\n[red]Error:[/red] {str(e)}")


def save_to_file(filepath: str, query: str, response: str, format: str):
"""Save the conversation to a file"""
import json
from pathlib import Path

output_path = Path(filepath)

if format == 'json':
content = json.dumps({
"query": query,
"response": response
}, indent=2)
elif format == 'markdown':
content = f"# Query\n\n{query}\n\n# Response\n\n{response}\n"
else: # text
content = f"Query: {query}\n\nResponse: {response}\n"

# Append mode for interactive sessions
mode = 'a' if output_path.exists() else 'w'
with open(output_path, mode) as f:
if mode == 'a':
f.write("\n" + "="*50 + "\n\n")
f.write(content)

if output:
console.print(f"[blue]Output will be saved to:[/blue] {output}")

@cli.command()
def config():
"""Configure the DevOps agent"""
console.print("[yellow]Configuration interface will be implemented here[/yellow]")


@cli.command()
@click.argument('template_type', type=click.Choice(['terraform', 'kubernetes', 'docker']))
def template(template_type):
"""Generate templates for various DevOps tools"""
console.print(f"[yellow]Generating {template_type} template...[/yellow]")


def main():
cli()


if __name__ == '__main__':
main()
56 changes: 54 additions & 2 deletions devops_agent/core/devops_agent.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,58 @@
import asyncio
import os

from agno.agent import Agent
from agno.knowledge import Knowledge
from agno.models.openai import OpenAIChat
from agno.models.anthropic import Claude
from agno.models.google.gemini import Gemini
from agno.vectordb.qdrant import Qdrant
from agno.knowledge.embedder.fastembed import FastEmbedEmbedder
from qdrant_client.http.models import VectorParams, Distance

from devops_agent.utils.prompt_generator_from_poml import prompt_from_poml
from qdrant_client.qdrant_client import QdrantClient

devops_prompt = prompt_from_poml('devops.poml')

def execute_devops_agent(user_query: str) -> str:
pass
qclient = QdrantClient(url=os.environ.get('QDRANT_URL'), api_key=os.environ.get('QDRANT_API_KEY'))
if not qclient.collection_exists("devops-memory"):
qclient.create_collection(collection_name="devops-memory", vectors_config=VectorParams(size=768, distance=Distance.COSINE))

vector_db = Qdrant(collection="devops-memory", url=os.environ.get('QDRANT_URL'),
api_key=os.environ.get('QDRANT_API_KEY'),
embedder=FastEmbedEmbedder(id="snowflake/snowflake-arctic-embed-m"))

# Create knowledge base
knowledge = Knowledge(vector_db=vector_db)

def execute_devops_agent(provider: str, user_query: str) -> str:
llm_provider = provider.lower().strip()
if llm_provider == 'openai':
model = OpenAIChat(id="gpt-5-mini", api_key=os.environ.get('OPENAI_API_KEY'))
elif llm_provider == 'anthropic':
model = Claude(id="claude-sonnet-4-5-20250929", temperature=0.6, api_key=os.environ.get('ANTHROPIC_API_KEY'))
elif llm_provider == 'google':
model = Gemini(id="gemini-2.5-flash", temperature=0.6, api_key=os.environ.get('GEMINI_API_KEY'))
else:
model = OpenAIChat(id="gpt-5-mini"), #default

devops_assist = Agent(
name="DevOps Assist",
model=model,
description="You help answer questions about the devops domain.",
instructions=devops_prompt,
knowledge=knowledge,
stream_intermediate_steps=True,
add_knowledge_to_context=True,
add_datetime_to_context=True,
add_session_summary_to_context=True,
markdown=True,
)

response = devops_assist.run(user_query, stream_intermediate_steps=True, retry=3)

asyncio.run(
knowledge.add_content_async(text_content=response.content, metadata={"agent_id": response.agent_id, "session_id": response.session_id})
)
return response.content
6 changes: 5 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,16 @@ dependencies = [
"click>=8.3.0",
"rich>=14.1.0",
"anthropic>=0.69.0",
"openai>=2.1.0",
"google-genai>=1.41.0",
"pyyaml>=6.0.3",
"python-dotenv>=1.1.1",
"requests>=2.32.5",
"colorama>=0.4.6",
"agno>=2.1.0",
"poml>=0.0.8"
"poml>=0.0.8",
"qdrant-client>=1.15.1",
"fastembed>=0.7.3"
]

[project.scripts]
Expand Down
6 changes: 6 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,17 @@ rich>=13.0.0

# LLM Integration
anthropic>=0.18.0
openai>=2.1.0
google-genai>=1.41.0

# File Processing
pyyaml>=6.0
python-dotenv>=1.0.0

#vector db
qdrant-client>=1.15.1
fastembed>=0.7.3

# Utilities
requests>=2.31.0
colorama>=0.4.6
Expand Down