Spaces:
Sleeping
Sleeping
File size: 12,722 Bytes
8edee29 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 | """
agents/detect_agent.py
-----------------------
Auto-Detect Agent for AutoDevAgent.
Makes a fast, cheap LLM call (Llama 3.1 8B) to classify the user's
task into a supported language with a confidence score and one-sentence
reason. The result pre-selects the language radio button in the UI
with a coloured badge so the user can verify or override.
Design:
- Uses the fast 8B model β classification is simple, 70B is overkill.
- Returns a DetectionResult Pydantic model β typed, validated, clean.
- If detection fails or returns UNKNOWN, the UI defaults to Python
(the most common language for generic tasks).
- The LLM is prompted to return JSON only. The _parse_json helper
from planning_agent is replicated here to keep the module
self-contained and avoid circular imports.
- Override is always available and prominent in the UI.
Any override is logged to session history.
Usage:
from agents.detect_agent import DetectAgent
from pipeline.state import PipelineState
agent = DetectAgent()
result = agent.detect("find duplicates in a list")
print(result.language) # Language.PYTHON
print(result.confidence) # "high"
print(result.reason) # "task involves list manipulation in Python"
"""
import json
import logging
from langchain_groq import ChatGroq
from langchain_core.messages import SystemMessage, HumanMessage
from config import settings
from pipeline.state import (
DetectionResult,
Language,
)
logger = logging.getLogger(__name__)
# ------------------------------------------------------------------ #
# Prompt #
# ------------------------------------------------------------------ #
DETECT_SYSTEM = """
You are a programming language classifier.
Classify the given task into one of these languages: python, sql, unknown.
Use "python" for: general coding tasks, algorithms, data manipulation,
file processing, APIs, machine learning, scripting.
Use "sql" for: database queries, SELECT/INSERT/UPDATE/DELETE statements,
aggregations, JOINs, schema design.
Use "unknown" for: tasks that are ambiguous or don't clearly fit either.
Return ONLY a JSON object in this exact format β no markdown, no explanation:
{
"language": "python",
"confidence": "high",
"reason": "one sentence explaining the detection"
}
confidence must be one of: "high", "medium", "low"
""".strip()
CODING_TASK_SYSTEM = """
You are a gate classifier for a code assistant that only handles Python and SQL programming tasks.
Decide whether the user's input is a programming/coding task or not.
A CODING task includes: writing code, debugging, algorithms, SQL queries, data structures,
functions, classes, scripts, APIs, database design, anything that requires generating code.
A NON-CODING task includes: general knowledge questions, factual questions, opinions,
math calculations without code, translations, creative writing, geography, history,
or anything that does not require writing Python or SQL code.
Return ONLY a JSON object β no markdown, no explanation:
{
"is_coding": true,
"confidence": "high",
"reason": "one sentence explaining the decision"
}
is_coding must be true or false.
confidence must be one of: "high", "medium", "low".
""".strip()
SCOPE_CHECK_SYSTEM = """
You are a scope classifier for a code assistant that generates focused Python or SQL code snippets.
The assistant can only produce a single self-contained file of at most ~100 lines.
Decide whether the user's task is realistically achievable as a SINGLE code snippet.
IN SCOPE (single snippet) examples:
- Write a Python function to reverse a string
- Sort a list of dictionaries by a key
- SQL query to find top 5 customers by revenue
- SQL query to find the second highest salary from an employees table
- SQL query with date filters: customers active in last 30 days but not in previous 30 days
- SQL query to calculate a running total using window functions or subqueries
- SQL query to de-duplicate a table keeping the latest row per user
- Fetch video titles from YouTube API using Python
- Parse a CSV and calculate column averages
- Implement binary search in Python
- Write a regex to validate email addresses
OUT OF SCOPE (full application / multi-file project) examples:
- Build a YouTube app / clone
- Build a social media platform
- Create a full e-commerce website
- Build a chat application
- Make an Android/iOS app
- Build a REST API with authentication, database, deployment
- Create a machine learning pipeline with training, evaluation, and deployment
Rules:
- A task is OUT OF SCOPE only if it requires: a frontend UI framework, multiple files/modules,
user authentication flows, deployment infrastructure, or weeks of engineering work.
- A task is IN SCOPE if it can be done in a single Python function/class or a single SQL query β
even if that query is complex (uses subqueries, CTEs, window functions, date ranges, JOINs).
- IMPORTANT: ANY single SQL query β no matter how complex the logic β is always IN SCOPE.
A SQL query never "requires a backend server" on its own; it is just a query.
- Be STRICT β "build an app" is always out of scope, but "write a query / function" is always in scope.
If OUT OF SCOPE, provide a short, friendly suggestion showing how to break the task into
a concrete single-snippet version the assistant CAN help with.
Return ONLY a JSON object β no markdown, no explanation:
{
"in_scope": true,
"confidence": "high",
"reason": "one sentence",
"suggestion": ""
}
If in_scope is false, suggestion must be a non-empty string with a concrete reframed example.
confidence must be one of: "high", "medium", "low".
""".strip()
# ------------------------------------------------------------------ #
# Agent #
# ------------------------------------------------------------------ #
class DetectAgent:
"""
Classifies a task description into a programming language.
Uses a single fast LLM call to return a DetectionResult with
the detected language, confidence level, and a brief reason.
Attributes:
llm: ChatGroq using the fast 8B model.
"""
def __init__(self) -> None:
"""Initialise with the fast LLM from config."""
self.llm = ChatGroq(
api_key = settings.groq_api_key,
model = settings.groq_model_fast,
temperature= 0.0, # Fully deterministic β classification task
max_tokens = 150, # Short response expected
)
def is_coding_task(self, task: str) -> tuple[bool, str]:
"""
Classify whether the task is a coding task at all.
Returns:
(is_coding: bool, reason: str)
is_coding=False means the task is non-technical and should
be rejected before the pipeline runs.
"""
logger.info("DetectAgent coding-gate check: '%s'", task[:60])
messages = [
SystemMessage(content=CODING_TASK_SYSTEM),
HumanMessage(content=f"Task: {task}"),
]
try:
response = self.llm.invoke(messages)
parsed = _parse_json(response.content.strip())
is_coding = bool(parsed.get("is_coding", True))
confidence = parsed.get("confidence", "low").lower().strip()
reason = parsed.get("reason", "")
logger.info("DetectAgent gate: is_coding=%s (%s) β %s", is_coding, confidence, reason)
# Only block when the model is confident it's NOT coding
if not is_coding and confidence in ("high", "medium"):
return False, reason
return True, reason
except Exception as e:
logger.warning("DetectAgent gate failed: %s β allowing task through", e)
return True, ""
def is_in_scope(self, task: str) -> tuple[bool, str]:
"""
Check whether the task is realistic for a single code snippet (~100 lines).
Tasks like "build a YouTube app" or "create a social media platform" are
full projects β out of scope for this assistant. We catch them here and
return a friendly reframing suggestion instead of wasting pipeline retries.
Returns:
(in_scope: bool, suggestion: str)
in_scope=False means the task is too large; suggestion holds a
concrete example of how the user could break it down.
"""
logger.info("DetectAgent scope check: '%s'", task[:80])
messages = [
SystemMessage(content=SCOPE_CHECK_SYSTEM),
HumanMessage(content=f"Task: {task}"),
]
try:
response = self.llm.invoke(messages)
parsed = _parse_json(response.content.strip())
in_scope = bool(parsed.get("in_scope", True))
confidence = parsed.get("confidence", "low").lower().strip()
reason = parsed.get("reason", "")
suggestion = parsed.get("suggestion", "").strip()
logger.info("DetectAgent scope: in_scope=%s (%s) β %s", in_scope, confidence, reason)
# Block at any confidence level when out-of-scope.
# The cost of letting a massive project through (wasted retries,
# broken output) outweighs occasionally rejecting a borderline task.
# Unlike the non-coding gate (low-confidence = probably fine),
# a low-confidence out-of-scope still means "probably too big".
if not in_scope:
return False, suggestion
return True, ""
except Exception as e:
logger.warning("DetectAgent scope check failed: %s β allowing task through", e)
return True, ""
def detect(self, task: str) -> DetectionResult:
"""
Detect the programming language for a given task description.
Args:
task: The user's task description string.
Returns:
DetectionResult with language, confidence, and reason.
Falls back to Language.UNKNOWN on any failure.
"""
logger.info("DetectAgent classifying task: '%s'", task[:60])
messages = [
SystemMessage(content=DETECT_SYSTEM),
HumanMessage(content=f"Task: {task}"),
]
try:
response = self.llm.invoke(messages)
raw = response.content.strip()
logger.debug("DetectAgent raw response: %s", raw)
parsed = _parse_json(raw)
# Validate and normalise language value
lang_str = parsed.get("language", "unknown").lower().strip()
try:
language = Language(lang_str)
except ValueError:
language = Language.UNKNOWN
confidence = parsed.get("confidence", "low").lower().strip()
if confidence not in ("high", "medium", "low"):
confidence = "low"
reason = parsed.get("reason", "Could not determine reason.")
result = DetectionResult(
language = language,
confidence = confidence,
reason = reason,
)
logger.info(
"DetectAgent: %s (%s confidence) β %s",
result.language.value,
result.confidence,
result.reason,
)
return result
except Exception as e:
logger.warning("DetectAgent failed: %s β defaulting to UNKNOWN", e)
return DetectionResult(
language = Language.UNKNOWN,
confidence = "low",
reason = "Auto-detection failed β please select language manually.",
)
# ------------------------------------------------------------------ #
# Helpers #
# ------------------------------------------------------------------ #
def _parse_json(raw: str) -> dict:
"""
Safely parse JSON from LLM response, stripping markdown fences.
Args:
raw: Raw string content from the LLM.
Returns:
Parsed dict.
Raises:
ValueError: If JSON cannot be parsed after cleaning.
"""
cleaned = raw.strip()
if cleaned.startswith("```"):
cleaned = cleaned.split("\n", 1)[-1]
if cleaned.endswith("```"):
cleaned = cleaned.rsplit("```", 1)[0]
cleaned = cleaned.strip()
try:
return json.loads(cleaned)
except json.JSONDecodeError as e:
raise ValueError(f"DetectAgent returned invalid JSON: {e}") from e
|