feat(m2): expose status and daily tools through jobbsok-tools
This commit is contained in:
parent
bdb6291042
commit
c7db9f16a0
4 changed files with 578 additions and 19 deletions
|
|
@ -13,10 +13,19 @@ SDK would add a PyPI dependency the Cowork sandbox cannot install, and would
|
|||
cost import time at every cold start against an MCP start-up timeout. Three
|
||||
JSON-RPC methods is less code than the dependency it would replace.
|
||||
|
||||
**Nothing heavy is imported at module load.** `kandidat_schema` and
|
||||
`vurdering` are imported inside the handlers, not at the top. The server has
|
||||
to answer `initialize` fast enough that Cowork does not time it out, and it
|
||||
answers that one without touching either module.
|
||||
**Nothing heavy is imported at module load.** `kandidat_schema`,
|
||||
`vurdering`, `sak_status`, `dagens` and `beslutninger` are imported inside the
|
||||
handlers, not at the top. The server has to answer `initialize` fast enough
|
||||
that Cowork does not time it out, and it answers that one without touching any
|
||||
of them.
|
||||
|
||||
**The M2 tools call the command line's own `main()`, with the streams
|
||||
redirected** (Step 24). Parity between the two paths is not a nicety here: every
|
||||
skill degrades to reading what the CLI produced when the server is absent, and
|
||||
that instruction is only true if the content is the same. A second renderer on
|
||||
this side would be a copy free to drift from the first, so what runs is the
|
||||
same entry point -- same argument parsing, same output, same refusals -- and
|
||||
the only thing this layer adds is turning an exit code into an MCP answer.
|
||||
|
||||
**Every tool takes an explicit `workspace`.** `jobbsok_lib.paths` refuses to
|
||||
guess at a home directory, and this process runs unsandboxed on the operator's
|
||||
|
|
@ -61,6 +70,23 @@ WORKSPACE_ARG = {
|
|||
),
|
||||
}
|
||||
|
||||
#: The injected clock. Every M2 tool takes it, and none of them reads the wall
|
||||
#: clock when it is absent only because a caller that cannot pin the date is
|
||||
#: still better served by today than by a refusal -- but a `Verifiser`-step
|
||||
#: that wants a reproducible answer passes it.
|
||||
TODAY_ARG = {
|
||||
"type": "string",
|
||||
"description": (
|
||||
"Datoen som skal brukes, YYYY-MM-DD. Utelates den, brukes dagens "
|
||||
"dato. Oppgi den i alt som skal kunne reproduseres."
|
||||
),
|
||||
}
|
||||
|
||||
SAK_ARG = {
|
||||
"type": "string",
|
||||
"description": "Begrens til én sak-id. Utelates den, tas hele arbeidsområdet.",
|
||||
}
|
||||
|
||||
TOOLS = [
|
||||
{
|
||||
"name": "kandidat_valider",
|
||||
|
|
@ -145,6 +171,95 @@ TOOLS = [
|
|||
"additionalProperties": False,
|
||||
},
|
||||
},
|
||||
{
|
||||
"name": "sak_status",
|
||||
"description": (
|
||||
"Utled status, ventende part, siste aktivitet, neste frist og "
|
||||
"stillhets-flagg for hver sak, fra logg.jsonl. sak.md er en "
|
||||
"hurtigbuffer og loggen vinner. Skriver ingenting."
|
||||
),
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"workspace": WORKSPACE_ARG,
|
||||
"today": TODAY_ARG,
|
||||
"sak": SAK_ARG,
|
||||
"format": {
|
||||
"type": "string",
|
||||
"enum": ["tekst", "json"],
|
||||
"description": "Utdataform. Standard: tekst.",
|
||||
},
|
||||
},
|
||||
"required": ["workspace"],
|
||||
"additionalProperties": False,
|
||||
},
|
||||
},
|
||||
{
|
||||
"name": "sak_status_check",
|
||||
"description": (
|
||||
"Samme utledning, men rapporterer om en hurtigbuffer i sak.md er "
|
||||
"uenig med loggen. Avviket staar i teksten, med saken navngitt; "
|
||||
"kommandolinja svarer i tillegg med exit-kode 1, som MCP ikke har. "
|
||||
"Retter ingenting og skriver ingenting."
|
||||
),
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"workspace": WORKSPACE_ARG,
|
||||
"today": TODAY_ARG,
|
||||
"sak": SAK_ARG,
|
||||
},
|
||||
"required": ["workspace"],
|
||||
"additionalProperties": False,
|
||||
},
|
||||
},
|
||||
{
|
||||
"name": "dagens",
|
||||
"description": (
|
||||
"Dagens arbeidsbilde som ren tekst: hva som krever handling, hva "
|
||||
"som har gaatt stille, kommende frister og saker per tilstand. "
|
||||
"Byte-stabil for samme arbeidsomraade og samme dato. Skriver "
|
||||
"ingenting."
|
||||
),
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {"workspace": WORKSPACE_ARG, "today": TODAY_ARG},
|
||||
"required": ["workspace"],
|
||||
"additionalProperties": False,
|
||||
},
|
||||
},
|
||||
{
|
||||
"name": "beslutning_append",
|
||||
"description": (
|
||||
"Legg til én beslutning i den append-only beslutningsloggen "
|
||||
"(build-brief 5.4), eller en korrigering som navngir en tidligere "
|
||||
"beslutning. En retting er alltid en ny linje, aldri en "
|
||||
"redigering. Returnerer posten som ble skrevet."
|
||||
),
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"workspace": WORKSPACE_ARG,
|
||||
"post": {
|
||||
"type": "object",
|
||||
"description": (
|
||||
"Posten. Feltene er build-brief 5.4: id, dato, kilde, "
|
||||
"url, tittel, arbeidsgiver, beslutning, arsak, notat, "
|
||||
"score_da, delscore, vekter, vekt_hash, korrigerer."
|
||||
),
|
||||
},
|
||||
"korriger": {
|
||||
"type": "boolean",
|
||||
"description": (
|
||||
"Posten er en korrigering av en tidligere beslutning. "
|
||||
"Standard: false."
|
||||
),
|
||||
},
|
||||
},
|
||||
"required": ["workspace", "post"],
|
||||
"additionalProperties": False,
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
|
|
@ -242,6 +357,95 @@ def tool_annonse_vurder(arguments):
|
|||
return til_json(resultat)
|
||||
|
||||
|
||||
def _streng(arguments, navn):
|
||||
"""An optional string argument, refused rather than coerced.
|
||||
|
||||
Types are checked before anything reaches `argparse`: a non-string would
|
||||
become a malformed argv, and argparse answers a malformed argv with
|
||||
SystemExit -- which is a BaseException and would take the server down
|
||||
rather than returning a tool error.
|
||||
"""
|
||||
verdi = arguments.get(navn)
|
||||
if verdi is None:
|
||||
return None
|
||||
if not isinstance(verdi, str) or not verdi.strip():
|
||||
raise ToolError("%s må være en ikke-tom tekst, fikk %r" % (navn, verdi))
|
||||
return verdi
|
||||
|
||||
|
||||
def _kjor_cli(modul, argv):
|
||||
"""Run a command-line entry point in-process and return its stdout.
|
||||
|
||||
The exit code is translated, not swallowed: a hard failure becomes a tool
|
||||
error carrying what the CLI wrote to stderr. `sak_status`'s exit code 1 --
|
||||
"a cache diverged" -- is deliberately not a failure; it is an answer, and
|
||||
it is in the text.
|
||||
"""
|
||||
import io as _io
|
||||
|
||||
ut, feil = _io.StringIO(), _io.StringIO()
|
||||
kode = modul.main(argv, stdout=ut, stderr=feil)
|
||||
if kode == modul.EXIT_FEIL:
|
||||
raise ToolError(feil.getvalue().strip() or "kommandoen feilet uten melding")
|
||||
return ut.getvalue()
|
||||
|
||||
|
||||
def _sak_status_argv(arguments):
|
||||
argv = ["--workspace", _workspace(arguments)]
|
||||
today = _streng(arguments, "today")
|
||||
if today:
|
||||
argv.extend(["--today", today])
|
||||
sak = _streng(arguments, "sak")
|
||||
if sak:
|
||||
argv.extend(["--sak", sak])
|
||||
return argv
|
||||
|
||||
|
||||
def tool_sak_status(arguments):
|
||||
import sak_status
|
||||
|
||||
argv = _sak_status_argv(arguments)
|
||||
form = _streng(arguments, "format") or "tekst"
|
||||
if form not in ("tekst", "json"):
|
||||
raise ToolError("format må være tekst eller json, fikk %r" % (form,))
|
||||
return _kjor_cli(sak_status, argv + ["--format", form])
|
||||
|
||||
|
||||
def tool_sak_status_check(arguments):
|
||||
import sak_status
|
||||
|
||||
# Text, not JSON: the CLI appends the divergence line to stdout after the
|
||||
# report, which is natural in the text form and would make the JSON form
|
||||
# unparseable. The answer is in that line, because MCP has no exit code.
|
||||
return _kjor_cli(sak_status, _sak_status_argv(arguments) + ["--check"])
|
||||
|
||||
|
||||
def tool_dagens(arguments):
|
||||
import dagens
|
||||
|
||||
argv = ["--workspace", _workspace(arguments)]
|
||||
today = _streng(arguments, "today")
|
||||
if today:
|
||||
argv.extend(["--today", today])
|
||||
return _kjor_cli(dagens, argv)
|
||||
|
||||
|
||||
def tool_beslutning_append(arguments):
|
||||
import beslutninger
|
||||
|
||||
root = _workspace(arguments)
|
||||
post = arguments.get("post")
|
||||
if not isinstance(post, dict):
|
||||
raise ToolError(
|
||||
"post mangler; forventet beslutningen som et kart med feltene fra "
|
||||
"build-brief 5.4"
|
||||
)
|
||||
argv = ["--workspace", root, "--json", json.dumps(post, ensure_ascii=False)]
|
||||
if arguments.get("korriger"):
|
||||
argv.append("--korriger")
|
||||
return _kjor_cli(beslutninger, argv)
|
||||
|
||||
|
||||
def tool_selvsjekk(arguments):
|
||||
root = _workspace(arguments)
|
||||
|
||||
|
|
@ -277,10 +481,16 @@ def tool_selvsjekk(arguments):
|
|||
)
|
||||
|
||||
|
||||
#: Insertion order is the order `tools/list` publishes, and the golden file
|
||||
#: pins it. Keep it the same as TOOLS above.
|
||||
HANDLERS = {
|
||||
"kandidat_valider": tool_kandidat_valider,
|
||||
"annonse_vurder": tool_annonse_vurder,
|
||||
"selvsjekk": tool_selvsjekk,
|
||||
"sak_status": tool_sak_status,
|
||||
"sak_status_check": tool_sak_status_check,
|
||||
"dagens": tool_dagens,
|
||||
"beslutning_append": tool_beslutning_append,
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue