Skip to content
Open
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
35 changes: 0 additions & 35 deletions .env.example

This file was deleted.

3 changes: 3 additions & 0 deletions ABB-Manual-Assistant/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Linamar-Vector-Bootcamp

## Applying agentic ai to robot troubleshooting.
5 changes: 5 additions & 0 deletions ABB-Manual-Assistant/Run_Eval_Info.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Upload dataset command:
python3 upload_test_data_from_file.py --dataset-name "SME Test Set" --input-file SME-TestSet.csv

Run evaluation command:
python3 run_eval_from_dataset.py --dataset-name "SME Test Set" --run-name "SME Eval Run"
46 changes: 46 additions & 0 deletions ABB-Manual-Assistant/SME-TestSet.csv

Large diffs are not rendered by default.

Empty file.
67 changes: 67 additions & 0 deletions ABB-Manual-Assistant/agent_utils/memory_store.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import datetime
import os
import sqlite3
import uuid


class MemoryStore:
def __init__(self, db_path="data/memory.db"):
os.makedirs(os.path.dirname(db_path), exist_ok=True)
self.conn = sqlite3.connect(db_path, check_same_thread=False)
self._init_db()

def _init_db(self):
cur = self.conn.cursor()
cur.execute("""
CREATE TABLE IF NOT EXISTS conversations (
id TEXT PRIMARY KEY,
title TEXT,
created_at TEXT
)
""")
cur.execute("""
CREATE TABLE IF NOT EXISTS messages (
id INTEGER PRIMARY KEY AUTOINCREMENT,
conversation_id TEXT,
role TEXT,
content TEXT,
timestamp TEXT
)
""")
self.conn.commit()

def create_conversation(self, title=None):
cid = str(uuid.uuid4())
if not title:
title = f"Chat {datetime.datetime.now().strftime('%Y-%m-%d %H:%M')}"
self.conn.execute(
"INSERT INTO conversations VALUES (?, ?, ?)", (cid, title, datetime.datetime.now().isoformat())
)
self.conn.commit()
return cid

def list_conversations(self):
return self.conn.execute("SELECT id, title, created_at FROM conversations ORDER BY created_at DESC").fetchall()

def get_history(self, conversation_id, limit=100):
rows = self.conn.execute(
"SELECT role, content FROM messages WHERE conversation_id=? ORDER BY id ASC LIMIT ?",
(conversation_id, limit),
).fetchall()
return [{"role": r[0], "content": r[1]} for r in rows]

def log_message(self, conversation_id, role, content):
self.conn.execute(
"INSERT INTO messages (conversation_id, role, content, timestamp) VALUES (?, ?, ?, ?)",
(conversation_id, role, content, datetime.datetime.now().isoformat()),
)
self.conn.commit()

def rename_conversation(self, conversation_id, new_title):
self.conn.execute("UPDATE conversations SET title=? WHERE id=?", (new_title, conversation_id))
self.conn.commit()

def delete_conversation(self, conversation_id):
self.conn.execute("DELETE FROM messages WHERE conversation_id=?", (conversation_id,))
self.conn.execute("DELETE FROM conversations WHERE id=?", (conversation_id,))
self.conn.commit()
211 changes: 211 additions & 0 deletions ABB-Manual-Assistant/configs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
"""Configuration settings for agent evaluations.

This module provides centralized configuration management using Pydantic settings,
supporting environment variables and .env file loading.
"""

from typing import Any

from pydantic import AliasChoices, BaseModel, Field, SecretStr, field_validator
from pydantic_settings import BaseSettings, SettingsConfigDict
from sqlalchemy.engine.url import URL


class DatabaseConfig(BaseModel):
"""Database connection configuration."""

driver: str = Field(
...,
description="SQLAlchemy dialect (e.g., 'sqlite', 'postgresql', 'mysql+pymysql').",
)
username: str | None = Field(
default=None,
description="Database username. For SQLite or integrated authentication, this can be None.",
)
host: str | None = Field(default=None, description="Database host address or file path for SQLite.")
password: SecretStr | None = Field(
default=None,
description="Database password. For SQLite or integrated authentication, this can be None.",
)
port: int | None = Field(default=None, description="Database port number.")
database: str | None = Field(default=None, description="Database name or file path for SQLite.")
query: dict[str, Any] = Field(
default_factory=dict,
description="URL query parameters (e.g. {'mode': 'ro'} for read-only SQLite).",
)

def build_uri(self) -> str:
"""Construct the SQLAlchemy connection URI safely using the official URL object.

This handles special character escaping in passwords automatically.

Returns
-------
str
The full database connection URI.
"""
return URL.create(
drivername=self.driver,
username=self.username,
password=self.password.get_secret_value() if self.password else None,
host=self.host,
port=self.port,
database=self.database,
query=self.query,
).render_as_string(hide_password=False)


class Configs(BaseSettings):
"""Central configuration for all agent evaluations.

This class automatically loads configuration values from environment variables
and a .env file. Service-specific fields are optional - agents validate
required fields at initialization.

Examples
--------
>>> from aieng.agent_evals.configs import Configs
>>> config = Configs()
>>> print(config.default_worker_model)
'gemini-2.5-flash'
"""

model_config = SettingsConfigDict(
env_file=".env",
env_file_encoding="utf-8",
env_ignore_empty=True,
env_nested_delimiter="__",
extra="ignore",
)

aml_db: DatabaseConfig | None = Field(
default=None,
description="Anti-Money Laundering database configuration. Used by the Fraud Investigation Agent.",
)

report_generation_db: DatabaseConfig | None = Field(
default=None,
description="Database configuration for the the Report Generation Agent.",
)

# === Core LLM Settings ===
openai_base_url: str = Field(
default="https://generativelanguage.googleapis.com/v1beta/openai/",
description="Base URL for OpenAI-compatible API (defaults to Gemini endpoint).",
)
openai_api_key: SecretStr = Field(
validation_alias=AliasChoices("OPENAI_API_KEY", "GEMINI_API_KEY", "GOOGLE_API_KEY"),
description="API key for OpenAI-compatible API (accepts OPENAI_API_KEY, GEMINI_API_KEY, or GOOGLE_API_KEY).",
)
google_api_key: SecretStr = Field(
validation_alias=AliasChoices("GEMINI_API_KEY", "GOOGLE_API_KEY"),
description="API key for Google/Gemini API (accepts GEMINI_API_KEY or GOOGLE_API_KEY).",
)
anthropic_api_key: SecretStr | None = Field(
default=None,
validation_alias="ANTHROPIC_API_KEY",
description="API key for Anthropic API access when using LiteLLM-backed Claude models.",
)
vector_inference_api_key: SecretStr | None = Field(
default=None,
validation_alias="VECTOR_INFERENCE_API_KEY",
description="API key for Vector's internal OpenAI-compatible inference endpoint.",
)
default_planner_model: str = Field(
default="gemini-2.5-pro",
description="Model name for planning/complex reasoning tasks.",
)
default_worker_model: str = Field(
default="gemini-2.5-flash",
description="Model name for worker/simple tasks.",
)
default_evaluator_model: str = Field(
default="gemini-2.5-pro",
description="Model name for LLM-as-judge evaluation tasks.",
)
default_temperature: float = Field(
default=1.0,
ge=0.0,
le=2.0,
description="Default temperature for LLM generation. Lower values (0.0-0.3) produce more consistent outputs.",
)
default_evaluator_temperature: float = Field(
default=0.0,
ge=0.0,
le=2.0,
description="Temperature for LLM-as-judge evaluations. Default 0.0 for deterministic judging.",
)

# === Tracing (Langfuse) ===
langfuse_public_key: str | None = Field(
default=None,
pattern=r"^pk-lf-.*$",
description="Langfuse public key for tracing (must start with 'pk-lf-').",
)
langfuse_secret_key: SecretStr | None = Field(
default=None,
description="Langfuse secret key for tracing (must start with 'sk-lf-').",
)
langfuse_host: str = Field(
default="https://us.cloud.langfuse.com",
validation_alias="LANGFUSE_HOST",
description="Langfuse base URL.",
)

# === Embedding Service ===
embedding_base_url: str | None = Field(default=None, description="Base URL for embedding API service.")
embedding_api_key: SecretStr | None = Field(default=None, description="API key for embedding service.")
embedding_model_name: str = Field(default="@cf/baai/bge-m3", description="Name of the embedding model.")

# === E2B Code Interpreter ===
e2b_api_key: SecretStr | None = Field(
default=None,
description="E2B.dev API key for code interpreter (must start with 'e2b_').",
)
default_code_interpreter_template: str | None = Field(
default="9p6favrrqijhasgkq1tv",
description="Default template name or ID for E2B.dev code interpreter.",
)

# === Web Search ===
web_search_base_url: str | None = Field(default=None, description="Base URL for web search service.")
web_search_api_key: SecretStr | None = Field(default=None, description="API key for web search service.")

# === Vertex AI Search (custom knowledge base) ===
google_cloud_location: str = Field(
default="us-central1",
description="GCP region for Vertex AI model calls. Must match a region that supports Gemini.",
)
vertex_datastore_id: str | None = Field(
default=None,
validation_alias="VERTEX_AI_DATASTORE_ID",
description=(
"Full Vertex AI Search data store resource name. "
"Format: projects/{project}/locations/global/collections/default_collection/dataStores/{id}. "
"Authentication uses Application Default Credentials (ADC) — no API key required."
),
)

# === Report Generation ===
# Defaults are set in the implementations/report_generation/env_vars.py file
report_generation_output_path: str | None = Field(
default=None,
description="Path to the directory where the report generation agent will save the reports.",
)

# Validators for the SecretStr fields
@field_validator("langfuse_secret_key")
@classmethod
def validate_langfuse_secret(cls, v: SecretStr | None) -> SecretStr | None:
"""Validate that the Langfuse secret key starts with 'sk-lf-'."""
if v is not None and not v.get_secret_value().startswith("sk-lf-"):
raise ValueError("Langfuse secret key must start with 'sk-lf-'")
return v

@field_validator("e2b_api_key")
@classmethod
def validate_e2b_key(cls, v: SecretStr | None) -> SecretStr | None:
"""Validate that the E2B API key starts with 'e2b_' if provided."""
if v is not None and not v.get_secret_value().startswith("e2b_"):
raise ValueError("E2B API key must start with 'e2b_'")
return v
73 changes: 73 additions & 0 deletions ABB-Manual-Assistant/conversation_manager.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# agents/conversation_manager.py
from __future__ import annotations

from typing import Any, Dict, List, Optional

from agent_utils.memory_store import MemoryStore


class ConversationManagerAgent:
"""
Minimal 'agent' wrapper around MemoryStore so other agents/tools
can call a stable interface. Keeps things simple:
- make sure a conversation exists
- append user/assistant turns
- read history
- rename/delete/list conversations
- optional: keep assistant partials in RAM; persist only on finalize
"""

def __init__(self, store: Optional[MemoryStore] = None):
self.store = store or MemoryStore()
# simple in-RAM buffer for current assistant partials per conversation
self._partials: Dict[str, str] = {}

# ---- conversation mgmt ----
def ensure_conversation(self, conversation_id: Optional[str]) -> str:
rows = self.store.list_conversations()
if not rows:
return self.store.create_conversation()
if conversation_id:
return conversation_id
# default to newest (list_conversations should return DESC by created_at)
return rows[0][0]

def list_conversations(self) -> List[Dict[str, Any]]:
rows = self.store.list_conversations()
return [{"id": r[0], "title": r[1], "created_at": r[2]} for r in rows]

def rename(self, conversation_id: str, title: str) -> Dict[str, Any]:
self.store.rename_conversation(conversation_id, title)
return {"ok": True, "conversation_id": conversation_id, "title": title}

def delete(self, conversation_id: str) -> Dict[str, Any]:
self.store.delete_conversation(conversation_id)
self._partials.pop(conversation_id, None)
return {"ok": True}

def create(self, title: Optional[str] = None) -> Dict[str, Any]:
cid = self.store.create_conversation(title)
return {"ok": True, "conversation_id": cid}

# ---- messages ----
def save_user(self, conversation_id: str, content: str) -> Dict[str, Any]:
cid = self.ensure_conversation(conversation_id)
self.store.log_message(cid, "user", content)
return {"ok": True, "conversation_id": cid}

def set_assistant_partial(self, conversation_id: str, partial_text: str) -> Dict[str, Any]:
# Keep partials in RAM for simplicity/quickness
cid = self.ensure_conversation(conversation_id)
self._partials[cid] = partial_text
return {"ok": True, "conversation_id": cid}

def finalize_assistant(self, conversation_id: str) -> Dict[str, Any]:
cid = self.ensure_conversation(conversation_id)
final_text = self._partials.pop(cid, "")
# Only persist once at the end of the stream
self.store.log_message(cid, "assistant", final_text)
return {"ok": True, "conversation_id": cid, "content_len": len(final_text)}

def get_history_messages(self, conversation_id: str, limit: int = 1000) -> List[Dict[str, Any]]:
cid = self.ensure_conversation(conversation_id)
return self.store.get_history(cid, limit=limit)
Loading