Coder preparations
This commit is contained in:
@@ -0,0 +1 @@
|
||||
class GiteaMCPClient: ...
|
||||
@@ -6,11 +6,14 @@ class LLMClient:
|
||||
self.model = model
|
||||
self.client = OpenAI(base_url=base_url, api_key=api_key or "ollama")
|
||||
|
||||
def chat_with_schema(self, prompt: str, schema: dict):
|
||||
def chat_with_schema(self, prompt: str, schema: dict, system: str | None = None) -> str | None:
|
||||
assert_strict_mode_clean(schema)
|
||||
kwargs = {
|
||||
"model": self.model,
|
||||
"messages": [
|
||||
{"role": "system", "content": system},
|
||||
{"role": "user", "content": prompt},
|
||||
] if system else [
|
||||
{"role": "user", "content": prompt},
|
||||
],
|
||||
"response_format": schema,
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
# orchestrator/time.py
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
# Single canonical format for all timestamps the agent writes.
|
||||
# ISO 8601, UTC, second precision, trailing "Z".
|
||||
# Examples: "2026-09-21T11:45:00Z"
|
||||
ISO_FORMAT = "%Y-%m-%dT%H:%M:%SZ"
|
||||
|
||||
|
||||
def now_iso() -> str:
|
||||
"""Current UTC time as an ISO 8601 string."""
|
||||
return datetime.now(timezone.utc).strftime(ISO_FORMAT)
|
||||
|
||||
|
||||
def now_iso_plus(
|
||||
*,
|
||||
days: int = 0,
|
||||
hours: int = 0,
|
||||
minutes: int = 0,
|
||||
seconds: int = 0,
|
||||
) -> str:
|
||||
"""Current UTC time plus a delta, as an ISO 8601 string."""
|
||||
delta = timedelta(days=days, hours=hours, minutes=minutes, seconds=seconds)
|
||||
return (datetime.now(timezone.utc) + delta).strftime(ISO_FORMAT)
|
||||
|
||||
|
||||
def parse_iso(s: str) -> datetime:
|
||||
"""Parse an ISO 8601 string produced by now_iso / now_iso_plus."""
|
||||
return datetime.strptime(s, ISO_FORMAT).replace(tzinfo=timezone.utc)
|
||||
@@ -1,5 +1,7 @@
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
from typing import Optional, Any
|
||||
|
||||
import httpx
|
||||
from contextlib import AsyncExitStack
|
||||
@@ -109,4 +111,50 @@ class YouTrackMCPClient:
|
||||
|
||||
async def list_tools(self):
|
||||
"""List all available tools from the MCP server."""
|
||||
return await self._client.list_tools()
|
||||
return await self._client.list_tools()
|
||||
|
||||
@classmethod
|
||||
def extract_comment_id(cls, result: Any) -> Optional[str]:
|
||||
"""
|
||||
Best-effort extraction of the created comment's ID from a
|
||||
CallToolResult (or any MCP-ish response).
|
||||
|
||||
Returns None if the ID can't be determined. Callers MUST tolerate
|
||||
None by falling back to marker-based lookup on the next Gather.
|
||||
"""
|
||||
# 1. Unwrap the MCP envelope.
|
||||
content = getattr(result, "content", None)
|
||||
if content is None and isinstance(result, dict):
|
||||
content = result.get("content")
|
||||
|
||||
if not content:
|
||||
return None
|
||||
|
||||
# 2. Take the first text block.
|
||||
first = content[0]
|
||||
text = getattr(first, "text", None)
|
||||
if text is None and isinstance(first, dict):
|
||||
text = first.get("text")
|
||||
|
||||
if not text:
|
||||
return None
|
||||
|
||||
# 3. Try JSON first.
|
||||
try:
|
||||
data = json.loads(text or "{}")
|
||||
except json.JSONDecodeError:
|
||||
# 4. Not JSON — assume the server returned a bare ID.
|
||||
return text.strip() or None
|
||||
|
||||
# 5. Common shapes: {"id": ...} or {"comment": {"id": ...}}.
|
||||
if isinstance(data, dict):
|
||||
if "id" in data:
|
||||
return str(data["id"])
|
||||
if "comment" in data and isinstance(data["comment"], dict):
|
||||
return str(data["comment"].get("id")) or None
|
||||
# YouTrack often uses "idReadable" for the human-facing ID.
|
||||
for key in ("idReadable", "commentId", "comment_id"):
|
||||
if key in data:
|
||||
return str(data[key])
|
||||
|
||||
return None
|
||||
Reference in New Issue
Block a user