File size: 7,766 Bytes
210ef30
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""The developer-assistant tools. Master prompt 6: a capability inside BOTH modes.

"The tone follows the mode; the substance is identical." So each tool runs a specialist,
mode-free sub-call that returns JSON, and Prova's own turn presents that result in whichever
voice the mode calls for. Nothing here knows or cares which mode it is in.

Each result is also returned as an *artifact* — the web app renders a review as a diff view and
an API design as a table (master prompt 6's UI list), rather than as prose.
"""

import json
import logging
from typing import Any

import httpx

from .. import config
from ..llm.base import user_content
from ..tools.registry import Tool, ToolContext, ToolOutcome, failed, ok, schema, string
from . import github, prompts
from .attachments import resolve

log = logging.getLogger(__name__)

SUBCALL_TEMPERATURE = 0.4
MAX_CODE_CHARS = 120_000


async def _specialist(ctx: ToolContext, instruction: str, material: str) -> dict[str, Any] | None:
    turn = await ctx.llm.step(
        instruction.strip(),
        [user_content(material[:MAX_CODE_CHARS])],
        temperature=SUBCALL_TEMPERATURE,
        max_output_tokens=config.MAX_OUTPUT_TOKENS_TECHNICAL,
        response_mime_type="application/json",
    )
    return parse_json(turn.text)


def parse_json(text: str) -> dict[str, Any] | None:
    """JSON mode is requested, but a fenced or prefixed reply must not lose the whole result."""
    raw = text.strip()
    if raw.startswith("```"):
        raw = raw.strip("`")
        raw = raw.split("\n", 1)[1] if "\n" in raw else raw
    start, end = raw.find("{"), raw.rfind("}")
    if start == -1 or end == -1:
        return None
    try:
        value = json.loads(raw[start : end + 1])
    except json.JSONDecodeError:
        return None
    return value if isinstance(value, dict) else None


async def _review_code(args: dict[str, Any], ctx: ToolContext) -> ToolOutcome:
    label, code = resolve(args.get("target"), ctx.attachments, ctx.user_message)
    if not code.strip():
        return ToolOutcome(failed("there is no code to review — ask him to paste or attach it"))
    material = f"FILE: {label}\n\n{code}"
    if args.get("focus"):
        material = f"FOCUS: {args['focus']}\n\n{material}"

    result = await _specialist(ctx, prompts.REVIEW, material)
    if result is None:
        return ToolOutcome(failed("the review came back unreadable"))

    findings = [f for f in result.get("findings", []) if isinstance(f, dict)][:12]
    artifact = {
        "type": "review",
        "target": label,
        "summary": result.get("summary", ""),
        "findings": findings,
    }
    return ToolOutcome(ok("reviewed", **artifact), artifacts=[artifact])


async def _explain_code(args: dict[str, Any], ctx: ToolContext) -> ToolOutcome:
    label, code = resolve(args.get("target"), ctx.attachments, ctx.user_message)
    if not code.strip():
        return ToolOutcome(failed("there is no code to explain"))
    material = f"FILE: {label}\n\n{code}"
    if args.get("focus"):
        material = f"QUESTION: {args['focus']}\n\n{material}"
    result = await _specialist(ctx, prompts.EXPLAIN, material)
    if result is None:
        return ToolOutcome(failed("the explanation came back unreadable"))
    return ToolOutcome(ok("explained", target=label, **result))


async def _debug_error(args: dict[str, Any], ctx: ToolContext) -> ToolOutcome:
    error = str(args["error"]).strip()
    label, code = resolve(args.get("target"), ctx.attachments, ctx.user_message)
    material = f"ERROR:\n{error}"
    if code.strip() and code.strip() != error:
        material += f"\n\nCODE ({label}):\n{code}"
    result = await _specialist(ctx, prompts.DEBUG, material)
    if result is None:
        return ToolOutcome(failed("the diagnosis came back unreadable"))
    artifact = {"type": "debug", "target": label, **result}
    return ToolOutcome(ok("diagnosed", **result), artifacts=[artifact])


async def _analyze_project(args: dict[str, Any], ctx: ToolContext) -> ToolOutcome:
    if not ctx.tree and not ctx.attachments:
        return ToolOutcome(failed("no project is open — ask him to open a folder or attach files"))
    parts = []
    if ctx.tree:
        parts.append("TREE:\n" + "\n".join(ctx.tree[:1500]))
    for name, content in list(ctx.attachments.items())[:20]:
        parts.append(f"FILE: {name}\n{content[:20_000]}")
    if args.get("focus"):
        parts.insert(0, f"FOCUS: {args['focus']}")
    result = await _specialist(ctx, prompts.ANALYZE_PROJECT, "\n\n".join(parts))
    if result is None:
        return ToolOutcome(failed("the analysis came back unreadable"))
    artifact = {"type": "analysis", **result}
    return ToolOutcome(ok("analysed", **result), artifacts=[artifact])


async def _design_api(args: dict[str, Any], ctx: ToolContext) -> ToolOutcome:
    result = await _specialist(ctx, prompts.DESIGN_API, f"REQUIREMENTS:\n{args['requirements']}")
    if result is None:
        return ToolOutcome(failed("the design came back unreadable"))
    artifact = {"type": "api", **result}
    return ToolOutcome(ok("designed", **result), artifacts=[artifact])


async def _fetch_github(args: dict[str, Any], ctx: ToolContext) -> ToolOutcome:
    client = ctx.http or httpx.AsyncClient(timeout=20.0, follow_redirects=False)
    try:
        fetched = await github.fetch(client, str(args["url"]))
    except (github.GithubError, httpx.HTTPError) as exc:
        return ToolOutcome(failed(str(exc) or "could not fetch that link"))
    finally:
        if ctx.http is None:
            await client.aclose()

    if fetched["kind"] == "file":
        # Available to the other dev tools for the rest of this turn.
        ctx.attachments[fetched["name"]] = fetched["content"]
        return ToolOutcome(
            ok("fetched", file=fetched["name"], content=fetched["content"][:MAX_CODE_CHARS])
        )
    ctx.tree = fetched["paths"]
    return ToolOutcome(
        ok(
            "fetched tree",
            repo=fetched["repo"],
            files=len(fetched["paths"]),
            paths=fetched["paths"][:300],
        )
    )


_TARGET = string("An attached file's name, or omit to use the code pasted in his message")

DEV_TOOLS: list[Tool] = [
    Tool(
        "review_code",
        "Review code for bugs, security, performance and WordPress/WooCommerce/Laravel/NestJS "
        "best practice. Returns findings with suggested edits he can see as a diff.",
        schema({"target": _TARGET, "focus": string("Anything specific he wants checked")}),
        _review_code,
    ),
    Tool(
        "explain_code",
        "Explain what a piece of code does, step by step.",
        schema({"target": _TARGET, "focus": string("His specific question about it")}),
        _explain_code,
    ),
    Tool(
        "debug_error",
        "Diagnose an error message or stack trace, with the code if he gave it.",
        schema(
            {"error": string("The error or stack trace, verbatim"), "target": _TARGET}, ["error"]
        ),
        _debug_error,
    ),
    Tool(
        "analyze_project",
        "Get oriented in a whole project from its file tree and attached files.",
        schema({"focus": string("What he wants to understand about it")}),
        _analyze_project,
    ),
    Tool(
        "design_api",
        "Design a REST API — resources, endpoints, data model — from requirements.",
        schema({"requirements": string("What the API has to do")}, ["requirements"]),
        _design_api,
    ),
    Tool(
        "fetch_github",
        "Fetch a public GitHub file or repository tree from a github.com link he shared.",
        schema({"url": string("The github.com or raw.githubusercontent.com link")}, ["url"]),
        _fetch_github,
    ),
]