Skip to content

Commit fe4998e

Browse files
authored
Ai baby workbench (#13)
* refactor: update configuration from Azure OpenAI to OpenAI and enhance agent service initialization Signed-off-by: Andre Bossard <anbossar@microsoft.com> * refactor: update AgentChat component for OpenAI integration and enhance markdown support Signed-off-by: Andre Bossard <anbossar@microsoft.com> --------- Signed-off-by: Andre Bossard <anbossar@microsoft.com>
1 parent e691d02 commit fe4998e

7 files changed

Lines changed: 210 additions & 102 deletions

File tree

.env.example

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1-
# Azure OpenAI Configuration
2-
# Get your API key from: https://portal.azure.com
1+
# OpenAI Configuration
2+
# Get your API key from: https://platform.openai.com/api-keys
33

4-
# Your Azure OpenAI API key (Bearer token)
5-
AZURE_API_KEY=your-api-key-here
4+
OPENAI_API_KEY=your-openai-api-key-here
5+
OPENAI_MODEL=gpt-4o-mini
6+
# Optional override
7+
# OPENAI_BASE_URL=https://api.openai.com/v1
68

79
# Optional: Frontend build path override
810
# FRONTEND_DIST=/path/to/custom/frontend/dist

backend/agents.py

Lines changed: 135 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
"""
2-
Agent Management Module with Azure OpenAI and LangGraph
2+
Agent Management Module with OpenAI and LangGraph
33
44
Provides LangGraph-based agents for task automation:
55
- Type-safe data models with Pydantic
66
- Self-documenting schemas for REST and MCP
7-
- Azure OpenAI integration via langchain-openai
7+
- OpenAI integration via langchain-openai
88
- ReAct agent pattern with automatic tool discovery
99
1010
Following "Grokking Simplicity" and "A Philosophy of Software Design":
@@ -39,18 +39,28 @@
3939

4040
load_dotenv()
4141

42+
from uuid import UUID
43+
4244
# Ensure operations register before we request LangChain tools
4345
import operations # noqa: F401
46+
4447
# Local - Import operations registry for automatic tool discovery
4548
from api_decorators import get_langchain_tools
49+
50+
# Local CSV service
51+
from csv_data import get_csv_ticket_service
52+
4653
# Third-party - FastMCP client for external MCP servers
4754
from fastmcp import Client as MCPClient
4855
from langchain_core.tools import StructuredTool
56+
4957
# Third-party - LangChain and LangGraph
50-
from langchain_openai import AzureChatOpenAI
58+
from langchain_openai import ChatOpenAI
5159
from langgraph.prebuilt import create_react_agent
60+
5261
# Third-party - Pydantic for validation
5362
from pydantic import BaseModel, Field, create_model, field_validator
63+
from tickets import TicketStatus
5464

5565
# ============================================================================
5666
# DATA MODELS - Pydantic for validation and schema generation
@@ -133,14 +143,12 @@ class AgentResponse(BaseModel):
133143

134144

135145
# ============================================================================
136-
# CONFIGURATION - Azure OpenAI settings (hardcoded except API key)
146+
# CONFIGURATION - OpenAI settings
137147
# ============================================================================
138148

139-
# Azure OpenAI configuration - only API key from environment
140-
AZURE_OPENAI_ENDPOINT = "https://can-i-haz-houze-resource.cognitiveservices.azure.com"
141-
AZURE_OPENAI_API_KEY = os.getenv("AZURE_API_KEY", "")
142-
AZURE_OPENAI_DEPLOYMENT = os.getenv("AZURE_OPENAI_DEPLOYMENT", "gpt-5-mini")
143-
AZURE_OPENAI_API_VERSION = os.getenv("AZURE_OPENAI_API_VERSION", "2025-04-01-preview")
149+
OPENAI_API_KEY = os.getenv("OPENAI_API_KEY", "")
150+
OPENAI_MODEL = os.getenv("OPENAI_MODEL", "gpt-4o-mini")
151+
OPENAI_BASE_URL = os.getenv("OPENAI_BASE_URL", "") # optional override
144152

145153
# External MCP server URL for ticket management (hardcoded)
146154
TICKET_MCP_SERVER_URL = "https://yodrrscbpxqnslgugwow.supabase.co/functions/v1/mcp/a7f2b8c4-d3e9-4f1a-b5c6-e8d9f0123456"
@@ -241,7 +249,7 @@ class AgentService:
241249
Agent service handling all LLM agent operations.
242250
243251
Consolidated operations:
244-
- Agent initialization with Azure OpenAI
252+
- Agent initialization with OpenAI
245253
- Tool discovery from @operation registry
246254
- External MCP server tool integration (persistent connection)
247255
- ReAct agent execution with task tools
@@ -253,71 +261,39 @@ class AgentService:
253261

254262
def __init__(self):
255263
"""
256-
Initialize the agent service with Azure OpenAI.
264+
Initialize the agent service with OpenAI.
257265
258266
Validates that required environment variables are set and creates
259267
the LLM client for agent execution.
260268
261269
Raises:
262-
ValueError: If Azure OpenAI configuration is incomplete
270+
ValueError: If OpenAI configuration is incomplete
263271
"""
264272
# Validate configuration
265-
if not AZURE_OPENAI_API_KEY:
273+
if not OPENAI_API_KEY:
266274
raise ValueError(
267-
"Azure OpenAI API key not set. "
268-
"Please set AZURE_API_KEY environment variable."
275+
"OpenAI API key not set. "
276+
"Please set OPENAI_API_KEY environment variable."
269277
)
270278

271-
# Initialize AzureChatOpenAI with hardcoded endpoint
272-
self.llm = AzureChatOpenAI(
273-
azure_endpoint=AZURE_OPENAI_ENDPOINT,
274-
api_key=AZURE_OPENAI_API_KEY, # type: ignore
275-
azure_deployment=AZURE_OPENAI_DEPLOYMENT,
276-
api_version=AZURE_OPENAI_API_VERSION,
279+
# Initialize ChatOpenAI
280+
self.llm = ChatOpenAI(
281+
model=OPENAI_MODEL,
282+
api_key=OPENAI_API_KEY,
283+
base_url=OPENAI_BASE_URL or None,
284+
temperature=0.0,
277285
)
278286

279-
# Load LangGraph tools from operation registry
280-
# These are the actual Python functions decorated with @tool
281-
self.tools = get_langchain_tools()
282-
283-
# Ticket MCP client state (lazy initialization)
287+
# CSV tools only (do not expose operations or external MCP)
288+
self.tools = self._build_csv_tools()
289+
290+
# Ticket MCP client state (unused)
284291
self._ticket_mcp_client: Optional[MCPClient] = None
285292
self._ticket_mcp_tools_loaded = False
286293

287294
async def _ensure_ticket_mcp_connection(self):
288-
"""
289-
Ensure ticket MCP client is connected and tools are loaded.
290-
291-
Opens a persistent connection to the external ticket MCP server and
292-
converts its tools to LangChain format. Called lazily on first
293-
agent run.
294-
"""
295-
if self._ticket_mcp_tools_loaded:
296-
return
297-
298-
try:
299-
# Create and connect ticket MCP client (keep connection open)
300-
client = MCPClient(TICKET_MCP_SERVER_URL)
301-
await client.__aenter__()
302-
self._ticket_mcp_client = client
303-
304-
# Fetch and convert ticket MCP tools
305-
mcp_tools = await client.list_tools()
306-
print(f"\n{'='*60}")
307-
print(f"🎫 TICKET MCP SERVER CONNECTED")
308-
print(f"{'='*60}")
309-
print(f" URL: {TICKET_MCP_SERVER_URL}")
310-
print(f" Tools available: {len(mcp_tools)}")
311-
for tool in mcp_tools:
312-
lc_tool = _mcp_tool_to_langchain(client, tool)
313-
self.tools.append(lc_tool)
314-
print(f" ✓ {tool.name}: {(tool.description or '')[:60]}...")
315-
print(f"{'='*60}\n")
316-
self._ticket_mcp_tools_loaded = True
317-
318-
except Exception as e:
319-
print(f"WARNING: Failed to load ticket MCP tools from {TICKET_MCP_SERVER_URL}: {e}")
320-
# Continue without ticket MCP tools - local tools still work
295+
"""No-op: external MCP tools not exposed."""
296+
return
321297

322298
async def close(self):
323299
"""Close the ticket MCP client connection."""
@@ -327,7 +303,78 @@ async def close(self):
327303
except Exception:
328304
pass
329305
self._ticket_mcp_client = None
330-
306+
307+
def _build_csv_tools(self) -> list[StructuredTool]:
308+
"""Build LangChain tools backed by CSVTicketService."""
309+
import json
310+
service = get_csv_ticket_service()
311+
312+
def _csv_list_tickets(status: str | None = None, assigned_group: str | None = None, has_assignee: bool | None = None) -> str:
313+
try:
314+
status_enum = TicketStatus(status.lower()) if status else None
315+
except Exception:
316+
status_enum = None
317+
tickets = service.list_tickets(status=status_enum, assigned_group=assigned_group, has_assignee=has_assignee)
318+
return json.dumps([t.model_dump() for t in tickets[:200]], default=str)
319+
320+
def _csv_get_ticket(ticket_id: str) -> str:
321+
try:
322+
tid = UUID(ticket_id)
323+
except Exception:
324+
return json.dumps({"error": "invalid ticket id"})
325+
ticket = service.get_ticket(tid)
326+
if not ticket:
327+
return json.dumps({"error": "not found"})
328+
return json.dumps(ticket.model_dump(), default=str)
329+
330+
def _csv_search_tickets(query: str, limit: int = 50) -> str:
331+
q = query.lower()
332+
tickets = service.list_tickets()
333+
matched = []
334+
for t in tickets:
335+
text = " ".join([
336+
t.summary or "",
337+
t.description or "",
338+
t.notes or "",
339+
t.resolution or "",
340+
t.requester_name or "",
341+
t.assigned_group or "",
342+
t.city or "",
343+
]).lower()
344+
if q in text:
345+
matched.append(t.model_dump())
346+
if len(matched) >= limit:
347+
break
348+
return json.dumps(matched, default=str)
349+
350+
def _csv_ticket_fields() -> str:
351+
# Use Ticket model fields as schema
352+
from tickets import Ticket
353+
return json.dumps(list(Ticket.model_fields.keys()))
354+
355+
return [
356+
StructuredTool.from_function(
357+
func=_csv_list_tickets,
358+
name="csv_list_tickets",
359+
description="List tickets from CSV with optional filters: status (new, assigned, in_progress, pending, resolved, closed, cancelled), assigned_group, has_assignee (true/false). Returns JSON array.",
360+
),
361+
StructuredTool.from_function(
362+
func=_csv_get_ticket,
363+
name="csv_get_ticket",
364+
description="Get a ticket by UUID (id). Returns JSON object including notes/resolution.",
365+
),
366+
StructuredTool.from_function(
367+
func=_csv_search_tickets,
368+
name="csv_search_tickets",
369+
description="Search tickets by text across summary, description, notes, resolution, requester, group, city. Returns JSON array.",
370+
),
371+
StructuredTool.from_function(
372+
func=_csv_ticket_fields,
373+
name="csv_ticket_fields",
374+
description="List available ticket fields (schema) as JSON array of field names.",
375+
),
376+
]
377+
331378
async def run_agent(self, request: AgentRequest) -> AgentResponse:
332379
"""
333380
Run a ReAct agent with the given request using LangGraph.
@@ -352,35 +399,40 @@ async def run_agent(self, request: AgentRequest) -> AgentResponse:
352399
Raises:
353400
ValueError: If agent execution fails
354401
"""
355-
# Ensure ticket MCP tools are loaded (lazy initialization)
356-
await self._ensure_ticket_mcp_connection()
357-
358402
try:
359403
# Create ReAct agent with LangGraph tools
360404
# The tools are the actual Python functions with @tool decorator
361405
agent = create_react_agent(self.llm, self.tools)
362406

363407
# System message to guide the agent's behavior
364-
system_msg = (
365-
"You are a support ticket management assistant. "
366-
"Your primary role is to help users manage, search, and evaluate support tickets. "
367-
"You MUST use the available tools to perform all actions - NEVER simulate or pretend. "
368-
"\n\n"
369-
"TICKET CAPABILITIES:\n"
370-
"- Search and list tickets by status, priority, city, service\n"
371-
"- Get detailed ticket information and work logs\n"
372-
"- Analyze ticket statistics and trends\n"
373-
"- Request modifications to tickets (status, priority, assignee, etc.)\n"
374-
"- Review and approve/reject modification requests\n"
375-
"\n"
376-
"TASK CAPABILITIES:\n"
377-
"- Create, update, delete, and list tasks\n"
378-
"- Get task statistics\n"
379-
"\n"
380-
"When users ask about tickets, use the ticket tools (list_tickets, get_ticket, search_tickets, etc.). "
381-
"When users ask about tasks, use task tools (create_task, list_tasks, etc.). "
382-
"Always confirm actions based on actual tool results."
383-
)
408+
tool_lines = []
409+
for t in self.tools:
410+
name = t.name if hasattr(t, 'name') else str(t)
411+
desc = (t.description if hasattr(t, 'description') else "") or ""
412+
tool_lines.append(f"- `{name}`: {desc}".strip())
413+
tools_md = "\n".join(tool_lines) if tool_lines else "- (none)"
414+
415+
system_msg = f"""
416+
Du bist ein freundlicher CSV-Ticket-Assistent. Sprich **Deutsch**.
417+
418+
Antwortstil:
419+
- Starte immer mit einer kurzen Begrüßung.
420+
- Liste sofort die verfügbaren Tools (Markdown-Bullets).
421+
- Nutze **Markdown** mit klaren Überschriften (##), Bullet-Listen und Tabellen, wenn sinnvoll.
422+
- Für JSON-Daten nutze fenced Code-Blöcke:
423+
```json
424+
{{"example": "value"}}
425+
```
426+
- Halte Antworten knapp und gut strukturiert.
427+
428+
Verfügbare Tools:
429+
{tools_md}
430+
431+
Verhalten:
432+
- Verwende ausschließlich die csv_* Tools für Ticket-Informationen. Keine Daten erfinden.
433+
- Falls Daten fehlen, sage das explizit.
434+
- Fasse Ergebnisse klar zusammen; für Listen sind kompakte Tabellen ideal.
435+
"""
384436

385437
# Execute agent with user prompt
386438
print(f"\n{'='*60}")

backend/app.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929

3030
# Import unified operation system
3131

32-
# Agent service for Azure OpenAI LangGraph agents
32+
# Agent service for OpenAI LangGraph agents
3333
from agents import AgentRequest, AgentResponse, agent_service
3434
from api_decorators import operation
3535

@@ -157,12 +157,12 @@ async def rest_get_stats():
157157

158158

159159
# ============================================================================
160-
# AGENT ENDPOINT - Azure OpenAI LangGraph Agent
160+
# AGENT ENDPOINT - OpenAI LangGraph Agent
161161
# ============================================================================
162162

163163
@app.route("/api/agents/run", methods=["POST"])
164164
async def rest_run_agent():
165-
"""REST wrapper: run AI agent with Azure OpenAI.
165+
"""REST wrapper: run AI agent with OpenAI.
166166
167167
The agent has access to task tools and ticket MCP tools.
168168
"""

backend/test_agents.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
Test script for LangGraph agent integration.
44
55
This script tests:
6-
1. Agent service initialization (without Azure OpenAI)
6+
1. Agent service initialization (without OpenAI)
77
2. Operation to LangChain tool conversion
88
3. MCP tool schema generation
99
@@ -66,7 +66,7 @@ def test_agent_import():
6666
print()
6767
except ImportError as e:
6868
print(f" ⚠ Could not import agents module: {e}")
69-
print(" This is expected if Azure OpenAI dependencies are not configured")
69+
print(" This is expected if OpenAI dependencies are not configured")
7070
print()
7171

7272
def test_task_service():
@@ -103,7 +103,7 @@ def main():
103103
print()
104104
print("Next steps:")
105105
print("1. Copy .env.example to .env")
106-
print("2. Configure Azure OpenAI credentials in .env")
106+
print("2. Configure OPENAI_API_KEY in .env")
107107
print("3. Start the server: python app.py")
108108
print("4. Test agent: POST /api/agents/run with {\"prompt\": \"List all tasks\"}")
109109
print()

frontend/package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,9 @@
2323
"@nivo/stream": "^0.99.0",
2424
"react": "^18.2.0",
2525
"react-dom": "^18.2.0",
26-
"react-router-dom": "^7.9.6"
26+
"react-markdown": "^10.1.0",
27+
"react-router-dom": "^7.9.6",
28+
"remark-gfm": "^4.0.1"
2729
},
2830
"devDependencies": {
2931
"@playwright/test": "^1.42.1",

0 commit comments

Comments
 (0)