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
8 changes: 4 additions & 4 deletions backend/agents.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,6 @@ def _env_int(name: str, default: int) -> int:

# Local CSV service
from csv_data import get_csv_ticket_service

from langchain_core.tools import StructuredTool

# Third-party - LangGraph
Expand Down Expand Up @@ -186,10 +185,11 @@ def __init__(self):
"""
Initialize the agent service.

Uses OpenAI when OPENAI_API_KEY is set, otherwise falls back
to LiteLLM (supports GitHub Copilot, Ollama, etc.).
Defaults to LiteLLM with GitHub Copilot backend.
Set AGENT_BACKEND=openai to force OpenAI SDK (requires OPENAI_API_KEY).
"""
if OPENAI_API_KEY:
force_openai = os.getenv("AGENT_BACKEND", "").lower() == "openai"
if force_openai and OPENAI_API_KEY:
from langchain_openai import ChatOpenAI
self.llm = ChatOpenAI(
model=OPENAI_MODEL,
Expand Down
31 changes: 19 additions & 12 deletions backend/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,19 @@
# CSV ticket service
from csv_data import Ticket, get_csv_ticket_service

# FastMCP client for direct ticket MCP calls (no AI)
from fastmcp import Client as MCPClient

# KBA Drafter
from kba_exceptions import (
DraftNotFoundError,
DuplicateKBADraftError,
InvalidLLMOutputError,
InvalidStatusError,
LLMAuthenticationError,
LLMRateLimitError,
LLMTimeoutError,
LLMUnavailableError,
PublishFailedError,
TicketNotFoundError,
)
Expand All @@ -53,15 +60,6 @@
KBADraftUpdate,
KBAPublishRequest,
)
from kba_exceptions import (
LLMTimeoutError,
LLMUnavailableError,
LLMRateLimitError,
LLMAuthenticationError,
)

# FastMCP client for direct ticket MCP calls (no AI)
from fastmcp import Client as MCPClient
from mcp_handler import handle_mcp_request
from operations import (
CSV_TICKET_FIELDS,
Expand Down Expand Up @@ -111,8 +109,9 @@
@app.before_serving
async def startup():
"""Initialize scheduler on application startup"""
from scheduler import start_scheduler
import logging

from scheduler import start_scheduler

logger = logging.getLogger(__name__)
logger.info("Starting auto-generation scheduler...")
Expand All @@ -127,8 +126,9 @@ async def startup():
@app.after_serving
async def shutdown():
"""Cleanup scheduler on application shutdown"""
from scheduler import stop_scheduler
import logging

from scheduler import stop_scheduler

logger = logging.getLogger(__name__)
logger.info("Stopping auto-generation scheduler...")
Expand Down Expand Up @@ -585,6 +585,12 @@ async def get_qa_tickets():
if _csv_data_path.exists():
_csv_loaded = _csv_ticket_service.load_csv(_csv_data_path)
print(f"📊 Loaded {_csv_loaded} tickets from CSV")
else:
print(
f"⚠️ CSV data file not found: {_csv_data_path.resolve()}\n"
f" Ticket features will be unavailable.\n"
f" To fix: place your BMC Remedy/ITSM CSV export at csv/data.csv"
)
Comment on lines +588 to +593


@app.route("/api/csv-tickets/fields", methods=["GET"])
Expand Down Expand Up @@ -974,6 +980,7 @@ async def rest_kba_publish_draft(draft_id: str):
async def rest_kba_list_drafts():
"""REST wrapper: list KBA drafts with filtering."""
from operations import op_kba_list_drafts

# Parse query parameters
filters = KBADraftFilter(
status=request.args.get("status"),
Expand Down Expand Up @@ -1039,8 +1046,8 @@ async def rest_kba_get_auto_gen_settings():
@app.route("/api/kba/auto-gen/settings", methods=["PATCH"])
async def rest_kba_update_auto_gen_settings():
"""REST wrapper: update auto-generation settings."""
from operations import op_kba_update_auto_gen_settings
from auto_gen_models import AutoGenSettingsUpdate
from operations import op_kba_update_auto_gen_settings

data = await request.get_json()
updates = AutoGenSettingsUpdate(**data)
Expand Down
20 changes: 10 additions & 10 deletions backend/llm_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,14 +30,13 @@
import os
from typing import Any, Optional, Type

from pydantic import BaseModel

from kba_exceptions import (
LLMUnavailableError,
LLMTimeoutError,
LLMAuthenticationError,
LLMRateLimitError,
LLMAuthenticationError
LLMTimeoutError,
LLMUnavailableError,
)
from pydantic import BaseModel

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -89,7 +88,8 @@ def __init__(
"""
self.timeout = timeout

# Determine backend: LiteLLM is the default, OpenAI only when forced
# Determine backend: LiteLLM with Copilot is the default.
# OpenAI only when explicitly forced via backend="openai".
resolved_api_key = api_key or OPENAI_API_KEY
if backend == "openai":
self._backend = "openai"
Expand All @@ -112,9 +112,9 @@ def __init__(
client_kwargs["base_url"] = self.base_url
self._client = AsyncOpenAI(**client_kwargs)
else:
# LiteLLM backend
# LiteLLM backend — always default to Copilot model
self.api_key = resolved_api_key or None
self.model = model or (OPENAI_MODEL if resolved_api_key else LITELLM_MODEL)
self.model = model or LITELLM_MODEL
self.base_url = base_url or OPENAI_BASE_URL or None
self._client = None
# Build fallback chain: primary model + configured fallbacks (deduplicated)
Expand Down Expand Up @@ -295,9 +295,9 @@ def _handle_openai_error(self, error: Exception) -> Exception:
from openai import (
APIConnectionError,
APITimeoutError,
RateLimitError,
AuthenticationError,
BadRequestError
BadRequestError,
RateLimitError,
)

if isinstance(error, APITimeoutError):
Expand Down
17 changes: 14 additions & 3 deletions backend/operations.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,8 +85,9 @@ def _get_kba_session() -> Session:
global _kba_db_engine, _kba_session
if _kba_db_engine is None:
from pathlib import Path
from kba_models import KBADraftTable, KBAAuditLog

from auto_gen_models import AutoGenSettingsTable
from kba_models import KBAAuditLog, KBADraftTable
from sqlmodel import SQLModel

db_path = Path(__file__).parent / "data" / "kba.db"
Expand Down Expand Up @@ -162,8 +163,18 @@ def _ensure_csv_loaded() -> None:
if default_csv_path.exists():
try:
_csv_service.load_csv(default_csv_path)
except Exception:
pass
except Exception as exc:
import logging
logging.getLogger(__name__).warning(
"Failed to load CSV data from %s: %s", default_csv_path, exc
)
else:
import logging
logging.getLogger(__name__).warning(
"CSV data file not found: %s — ticket operations will return empty results. "
"Place your BMC Remedy/ITSM CSV export at csv/data.csv to enable ticket features.",
default_csv_path.resolve(),
)
_csv_loaded = True


Expand Down
Loading