idun-sdk 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,11 @@
1
+ Metadata-Version: 2.4
2
+ Name: idun-sdk
3
+ Version: 0.1.0
4
+ Summary: Thin client + CLI for Azure AI Foundry agent NatureLM-Idun-5-MoE
5
+ Classifier: Programming Language :: Python :: 3
6
+ Classifier: License :: OSI Approved :: MIT License
7
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
8
+ Requires-Python: >=3.8
9
+ Dynamic: classifier
10
+ Dynamic: requires-python
11
+ Dynamic: summary
@@ -0,0 +1,120 @@
1
+ # Idun SDK
2
+
3
+ Thin client + CLI for the **NatureLM-Idun-5-MoE** agent on **Azure AI Foundry**.
4
+ Stdlib-only (no httpx / azure.identity) so it runs headless on Termux/Android.
5
+
6
+ Idun is a **tool agent** (it calls `web_search`, `memory_search`). This SDK
7
+ surfaces the full agent trajectory — not just the final chat text — so you can
8
+ see every reasoning step and tool call instead of a black-box chatbot wheel.
9
+
10
+ ## Install
11
+
12
+ ```bash
13
+ cd idun_sdk
14
+ pip install -e . # provides the `idun` command
15
+ ```
16
+
17
+ ## Authenticate (device-code, Entra)
18
+
19
+ ```bash
20
+ idun login
21
+ # opens https://microsoft.com/devicelogin — enter the printed code,
22
+ # sign in with your QMFI-Research admin account.
23
+ # Token is saved to ~/foundry_token.txt (FOUNDRY_TOKEN).
24
+ ```
25
+
26
+ ## Use
27
+
28
+ ```bash
29
+ # final answer only
30
+ idun chat "Fasse in einem Satz zusammen, was Contoso im Bereich Nachhaltigkeit kommuniziert."
31
+
32
+ # full agent trajectory (reasoning + web_search tool steps)
33
+ idun trace "Use web_search to find the current CEO of Contoso and report the name."
34
+ ```
35
+
36
+ ### Python
37
+
38
+ ```python
39
+ from idun import IdunClient
40
+ res = IdunClient().complete("Your prompt here")
41
+ print(res.text) # final answer
42
+ for s in res.steps: # agent trajectory
43
+ if s.kind == "tool":
44
+ print("TOOL", s.tool, s.status, s.query)
45
+ else:
46
+ print("REASON", s.text[:80])
47
+ ```
48
+
49
+ ## Request shape (verified working)
50
+
51
+ ```
52
+ POST {base}/api/projects/{project}/agents/{agent}/endpoint/protocols/openai/responses?api-version=2025-05-15-preview
53
+ Authorization: Bearer <FOUNDRY_TOKEN>
54
+ Content-Type: application/json
55
+
56
+ {"model": "model-router", "input": "<prompt string>", "max_output_tokens": 4096}
57
+ ```
58
+
59
+ Notes:
60
+ - `model` MUST be `"model-router"` (the agent id is already in the URL).
61
+ - Do **not** send a `tools` key — the agent owns its capabilities; doing so
62
+ returns `400 invalid_payload`.
63
+ - The answer is in `output[].content[].text`; tool calls appear as
64
+ `web_search_call` items with `action.queries` and `status`.
65
+
66
+ ## Files
67
+
68
+ - `idun/client.py` — `IdunClient` (sync `complete()`) + `_normalize_output()`
69
+ - `idun/auth.py` — stdlib device-code `login()` + `load_token()`
70
+ - `idun_cli.py` — `idun login | chat | trace`
71
+
72
+ ## MCP — agent + docs
73
+
74
+ Idun is available as an MCP server **and** has a GitMCP docs mirror, so other
75
+ agents can both call Idun and read its documentation without hallucinating.
76
+
77
+ ### 1. Idun MCP server (stdlib-only, local)
78
+
79
+ `idun_mcp.py` is a zero-dependency stdio MCP server — no FastMCP / httpx
80
+ needed (runs on bare Python, ideal for Termux/Android).
81
+
82
+ ```bash
83
+ python3 idun_mcp.py # stdio MCP server
84
+ ```
85
+
86
+ Tools exposed:
87
+ - `idun_chat(prompt)` — final answer text
88
+ - `idun_trace(prompt)` — full agent trajectory (steps + text)
89
+
90
+ Add to any MCP client (e.g. Cursor `~/.cursor/mcp.json`):
91
+
92
+ ```json
93
+ {
94
+ "mcpServers": {
95
+ "idun": { "command": "python3", "args": ["/abs/path/idun-sdk/idun_mcp.py"] }
96
+ }
97
+ }
98
+ ```
99
+
100
+ ### 2. GitMCP docs mirror (remote, zero-setup)
101
+
102
+ Point an MCP client at the GitMCP URL to give it live access to this repo's
103
+ docs + code (prefers `llms.txt`):
104
+
105
+ ```
106
+ https://gitmcp.io/qapdex-maker/idun-sdk/sse
107
+ ```
108
+
109
+ For stdio-only clients (Claude Desktop, Cline, Msty):
110
+
111
+ ```json
112
+ { "mcpServers": { "idun-docs": { "command": "npx", "args": ["mcp-remote", "https://gitmcp.io/qapdex-maker/idun-sdk/sse"] } } }
113
+ ```
114
+
115
+ **Recommended combo for a foreign agent:** both `idun` (calls the agent) and
116
+ `idun-docs` (reads the SDK docs) — it can invoke Idun *and* look up the exact
117
+ `IdunClient` signature on its own.
118
+
119
+ [![GitMCP](https://img.shields.io/endpoint?url=https://gitmcp.io/badge/qapdex-maker/idun-sdk)](https://gitmcp.io/qapdex-maker/idun-sdk)
120
+
@@ -0,0 +1,6 @@
1
+ """Idun SDK public API."""
2
+ from .client import IdunClient, IdunResult, Step, _normalize_output
3
+ from .auth import login, load_token
4
+
5
+ __all__ = ["IdunClient", "IdunResult", "Step", "login", "load_token", "_normalize_output"]
6
+ __version__ = "0.1.0"
@@ -0,0 +1,75 @@
1
+ """Stdlib-only Entra device-code login for Idun (no azure.identity needed).
2
+
3
+ Flow: POST devicecode -> show user code -> poll token endpoint until granted
4
+ -> save FOUNDRY_TOKEN to ~/foundry_token.txt. Same endpoint/params the Azure
5
+ CLI uses, so it works headless on Termux.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import json
10
+ import os
11
+ import time
12
+ import urllib.parse
13
+ import urllib.request
14
+ from typing import Optional
15
+
16
+ TENANT = "885f01ab-7364-4484-be0a-231d541c9e7f"
17
+ SCOPE = "https://ai.azure.com/.default"
18
+ CLIENT_ID = "04b07795-8ddb-461a-bbee-02f9e1bf7b46" # Azure CLI first-party app
19
+ TOKEN_FILE = os.path.join(os.path.expanduser("~"), "foundry_token.txt")
20
+ CODE_FILE = os.path.join(os.path.expanduser("~"), "foundry_code.txt")
21
+ AUTH_ENDPOINT = f"https://login.microsoftonline.com/{TENANT}/oauth2/v2.0"
22
+
23
+
24
+ def _post(url: str, form: dict, headers=None) -> dict:
25
+ data = urllib.parse.urlencode(form).encode()
26
+ req = urllib.request.Request(url, data=data, headers=headers or {"Content-Type": "application/x-www-form-urlencoded"}, method="POST")
27
+ with urllib.request.urlopen(req, timeout=60) as r:
28
+ return json.loads(r.read())
29
+
30
+
31
+ def login() -> str:
32
+ # 1) request device code
33
+ dc = _post(f"{AUTH_ENDPOINT}/devicecode", {"client_id": CLIENT_ID, "scope": SCOPE})
34
+ msg = (f"To sign in, use a web browser to open {dc['verification_uri']} "
35
+ f"and enter the code {dc['user_code']} to authenticate.")
36
+ with open(CODE_FILE, "w") as f:
37
+ f.write(msg + "\n")
38
+ print(msg, flush=True)
39
+ print(f"(code also saved to {CODE_FILE})", flush=True)
40
+
41
+ # 2) poll for token
42
+ interval = int(dc.get("interval", 5))
43
+ expires = time.time() + float(dc.get("expires_in", 900))
44
+ form = {
45
+ "grant_type": "urn:ietf:params:oauth:grant-type:device_code",
46
+ "client_id": CLIENT_ID,
47
+ "device_code": dc["device_code"],
48
+ }
49
+ while time.time() < expires:
50
+ try:
51
+ tok = _post(f"{AUTH_ENDPOINT}/token", form)
52
+ if "access_token" in tok:
53
+ token = tok["access_token"]
54
+ with open(TOKEN_FILE, "w") as f:
55
+ f.write(token)
56
+ print(f"TOKEN_OK len={len(token)}")
57
+ print(f"saved to {TOKEN_FILE}")
58
+ return token
59
+ except urllib.error.HTTPError as e:
60
+ err = json.loads(e.read().decode("utf-8", "replace"))
61
+ if err.get("error") == "authorization_pending":
62
+ time.sleep(interval)
63
+ continue
64
+ if err.get("error") == "slow_down":
65
+ interval += 5
66
+ time.sleep(interval)
67
+ continue
68
+ raise RuntimeError(f"Login failed: {err.get('error_description', err)}")
69
+ raise RuntimeError("Login timed out. Re-run `idun login`.")
70
+
71
+
72
+ def load_token() -> Optional[str]:
73
+ if os.path.exists(TOKEN_FILE):
74
+ return open(TOKEN_FILE).read().strip()
75
+ return None
@@ -0,0 +1,130 @@
1
+ """Idun SDK — thin client + CLI for Azure AI Foundry agent NatureLM-Idun-5-MoE.
2
+
3
+ Stdlib-only (urllib, no httpx / azure.identity) so it runs on Termux/Android.
4
+ Mirrors the working request shape discovered during integration:
5
+ POST {base}/api/projects/{project}/agents/{agent}/endpoint/protocols/openai/responses?api-version={ver}
6
+ body: {"model": "model-router", "input": "<prompt string>", "max_output_tokens": N}
7
+ auth: Entra Bearer token (FOUNDRY_TOKEN), scope https://ai.azure.com/.default
8
+
9
+ Returns both the final text AND the agent trajectory ("steps"):
10
+ - kind=reasoning -> assistant plan / reasoning text
11
+ - kind=tool -> web_search call: {tool, query, status}
12
+ This is what makes Idun a visible tool-agent, not a chatbot wheel.
13
+ """
14
+ from __future__ import annotations
15
+
16
+ import json
17
+ import os
18
+ import time
19
+ import urllib.error
20
+ import urllib.request
21
+ from dataclasses import dataclass, field
22
+ from typing import List, Optional
23
+
24
+ # --- defaults (verified working for qmfi-research-project, 2026-07-25) ---
25
+ FOUNDRY_BASE_DEFAULT = "https://qmfi-research-project-resource.services.ai.azure.com"
26
+ FOUNDRY_PROJECT_DEFAULT = "qmfi-research-project"
27
+ FOUNDRY_AGENT_DEFAULT = "NatureLM-Idun-5-MoE"
28
+ FOUNDRY_API_VERSION_DEFAULT = "2025-05-15-preview"
29
+ FOUNDRY_SCOPE = "https://ai.azure.com/.default"
30
+ FOUNDRY_TENANT = "885f01ab-7364-4484-be0a-231d541c9e7f"
31
+ TOKEN_FILE = os.path.join(os.path.expanduser("~"), "foundry_token.txt")
32
+
33
+
34
+ @dataclass
35
+ class Step:
36
+ kind: str # "reasoning" | "tool" | "message"
37
+ text: str = ""
38
+ tool: str = ""
39
+ query: str = ""
40
+ status: str = ""
41
+ id: str = ""
42
+
43
+
44
+ @dataclass
45
+ class IdunResult:
46
+ text: str
47
+ steps: List[Step] = field(default_factory=list)
48
+ model: str = ""
49
+ raw: dict = field(default_factory=dict)
50
+
51
+
52
+ def _normalize_output(data: dict) -> IdunResult:
53
+ """Convert Foundry Responses output[] into (final_text, steps[])."""
54
+ text = ""
55
+ steps: List[Step] = []
56
+ for o in data.get("output", []):
57
+ otype = o.get("type")
58
+ if otype == "message" and o.get("role") == "assistant":
59
+ t = "".join(c.get("text", "") for c in o.get("content", []) if c.get("type") == "output_text")
60
+ if t:
61
+ text += t + "\n\n"
62
+ steps.append(Step(kind="reasoning", text=t))
63
+ elif otype == "web_search_call":
64
+ action = o.get("action") or {}
65
+ q = action.get("query") or ""
66
+ if action.get("queries"):
67
+ q = action["queries"][0]
68
+ steps.append(Step(kind="tool", tool="web_search", query=q,
69
+ status=o.get("status", "unknown"), id=o.get("id")))
70
+ elif otype == "message":
71
+ t = "".join(c.get("text", "") for c in o.get("content", []) if c.get("type") == "output_text")
72
+ if t:
73
+ text += t + "\n\n"
74
+ steps.append(Step(kind="message", text=t))
75
+ return IdunResult(text=text.strip(), steps=steps,
76
+ model=data.get("model", ""), raw=data)
77
+
78
+
79
+ class IdunClient:
80
+ def __init__(
81
+ self,
82
+ token: Optional[str] = None,
83
+ base: str = FOUNDRY_BASE_DEFAULT,
84
+ project: str = FOUNDRY_PROJECT_DEFAULT,
85
+ agent: str = FOUNDRY_AGENT_DEFAULT,
86
+ api_version: str = FOUNDRY_API_VERSION_DEFAULT,
87
+ timeout: int = 600,
88
+ ) -> None:
89
+ self.token = token or os.environ.get("FOUNDRY_TOKEN")
90
+ self.base = base.rstrip("/")
91
+ self.project = project
92
+ self.agent = agent
93
+ self.api_version = api_version
94
+ self.timeout = timeout
95
+
96
+ def _url(self) -> str:
97
+ return (f"{self.base}/api/projects/{self.project}/agents/{self.agent}"
98
+ f"/endpoint/protocols/openai/responses?api-version={self.api_version}")
99
+
100
+ def _headers(self) -> dict:
101
+ if not self.token:
102
+ raise RuntimeError("No FOUNDRY_TOKEN set. Run `idun login` or export FOUNDRY_TOKEN.")
103
+ return {"Authorization": f"Bearer {self.token}", "Content-Type": "application/json"}
104
+
105
+ def _build_payload(self, prompt: str, max_output_tokens: int = 4096) -> dict:
106
+ """Verified working request shape for the agent-in-URL endpoint.
107
+
108
+ Model MUST stay 'model-router' (agent name -> invalid_payload).
109
+ No 'tools' key (agent owns capabilities -> 400 invalid_payload).
110
+ """
111
+ return {
112
+ "model": "model-router",
113
+ "input": prompt,
114
+ "max_output_tokens": max_output_tokens,
115
+ }
116
+
117
+ def complete(self, prompt: str, max_output_tokens: int = 4096) -> IdunResult:
118
+ """Synchronous completion. Returns final text + agent trajectory."""
119
+ payload = self._build_payload(prompt, max_output_tokens)
120
+ req = urllib.request.Request(
121
+ self._url(), data=json.dumps(payload).encode("utf-8"),
122
+ headers=self._headers(), method="POST",
123
+ )
124
+ try:
125
+ with urllib.request.urlopen(req, timeout=self.timeout) as resp:
126
+ data = json.loads(resp.read())
127
+ except urllib.error.HTTPError as e:
128
+ body = e.read().decode("utf-8", "replace")[:400]
129
+ raise RuntimeError(f"Foundry HTTP {e.code}: {body}") from e
130
+ return _normalize_output(data)
@@ -0,0 +1,80 @@
1
+ #!/usr/bin/env python3
2
+ """Idun CLI — terminal client for NatureLM-Idun-5-MoE on Azure AI Foundry.
3
+
4
+ Usage:
5
+ idun login # device-code Entra login -> ~/foundry_token.txt
6
+ idun chat "your prompt" # print final answer
7
+ idun trace "your prompt" # print agent trajectory (reasoning + web_search steps)
8
+
9
+ Stdlib-only; needs FOUNDRY_TOKEN (from `idun login` or env).
10
+ """
11
+ from __future__ import annotations
12
+
13
+ import argparse
14
+ import os
15
+ import sys
16
+
17
+ from idun import IdunClient, login as do_login, load_token
18
+
19
+
20
+ def _client() -> IdunClient:
21
+ tok = load_token() or os.environ.get("FOUNDRY_TOKEN")
22
+ if not tok:
23
+ sys.exit("No token. Run `idun login` first (or export FOUNDRY_TOKEN).")
24
+ return IdunClient(token=tok)
25
+
26
+
27
+ def cmd_login(_args):
28
+ do_login()
29
+
30
+
31
+ def cmd_chat(args):
32
+ c = _client()
33
+ res = c.complete(args.prompt, max_output_tokens=args.max_tokens)
34
+ print(res.text)
35
+
36
+
37
+ def cmd_trace(args):
38
+ c = _client()
39
+ res = c.complete(args.prompt, max_output_tokens=args.max_tokens)
40
+ print(f"Model: {res.model}\n")
41
+ print("AGENT TRACE ({})".format(len(res.steps)))
42
+ print("=" * 60)
43
+ for i, s in enumerate(res.steps, 1):
44
+ if s.kind == "tool":
45
+ print(f" {i:>2}. TOOL web_search [{s.status}]")
46
+ print(f" query: {s.query}")
47
+ else:
48
+ head = s.text.replace("\n", " ").strip()[:90]
49
+ label = "REASON" if s.kind == "reasoning" else "MSG"
50
+ print(f" {i:>2}. {label} {head}{'…' if len(s.text) > 90 else ''}")
51
+ print("=" * 60)
52
+ print("\nFINAL ANSWER:")
53
+ print(res.text)
54
+
55
+
56
+ def build_parser() -> argparse.ArgumentParser:
57
+ p = argparse.ArgumentParser(prog="idun", description="NatureLM-Idun-5-MoE CLI")
58
+ sub = p.add_subparsers(dest="command", required=True)
59
+
60
+ sub.add_parser("login", help="device-code Entra login").set_defaults(func=cmd_login)
61
+
62
+ pc = sub.add_parser("chat", help="print final answer")
63
+ pc.add_argument("prompt")
64
+ pc.add_argument("--max-tokens", type=int, default=4096, dest="max_tokens")
65
+ pc.set_defaults(func=cmd_chat)
66
+
67
+ pt = sub.add_parser("trace", help="print agent trajectory (steps)")
68
+ pt.add_argument("prompt")
69
+ pt.add_argument("--max-tokens", type=int, default=4096, dest="max_tokens")
70
+ pt.set_defaults(func=cmd_trace)
71
+ return p
72
+
73
+
74
+ def main():
75
+ args = build_parser().parse_args()
76
+ args.func(args)
77
+
78
+
79
+ if __name__ == "__main__":
80
+ main()
@@ -0,0 +1,122 @@
1
+ #!/usr/bin/env python3
2
+ """Idun MCP server (stdio, stdlib-only).
3
+
4
+ Exposes the NatureLM-Idun-5-MoE agent as MCP tools so other agents/clients
5
+ can call it. No FastMCP / httpx / pydantic: implements the minimal MCP
6
+ JSON-RPC-over-stdio wire contract with only the standard library.
7
+
8
+ Tools:
9
+ idun_chat(prompt) -> final answer text
10
+ idun_trace(prompt) -> full agent trajectory (steps + text)
11
+
12
+ Auth: reads FOUNDRY_TOKEN / FOUNDRY_RESOURCE / FOUNDRY_AGENT from the
13
+ environment (same as the CLI). Entra device-code login is the caller's job
14
+ (`idun login`); this server only relays.
15
+
16
+ Run:
17
+ python3 idun_mcp.py # stdio MCP server
18
+ """
19
+ import sys, os, json
20
+
21
+ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
22
+ from idun.client import IdunClient
23
+
24
+
25
+ TOOLS = [
26
+ {
27
+ "name": "idun_chat",
28
+ "description": "Ask the NatureLM-Idun-5-MoE agent a question and return the final answer text.",
29
+ "inputSchema": {
30
+ "type": "object",
31
+ "properties": {"prompt": {"type": "string",
32
+ "description": "The user prompt for the Idun agent."}},
33
+ "required": ["prompt"],
34
+ },
35
+ },
36
+ {
37
+ "name": "idun_trace",
38
+ "description": ("Ask the Idun agent and return the FULL trajectory (reasoning + "
39
+ "tool calls) plus the final text — for auditable, visible tool-agent use."),
40
+ "inputSchema": {
41
+ "type": "object",
42
+ "properties": {"prompt": {"type": "string",
43
+ "description": "The user prompt for the Idun agent."}},
44
+ "required": ["prompt"],
45
+ },
46
+ },
47
+ ]
48
+
49
+
50
+ def _tool_chat(prompt):
51
+ cli = IdunClient()
52
+ res = dict(cli.complete(prompt))
53
+ return res.get("text", "")
54
+
55
+
56
+ def _tool_trace(prompt):
57
+ cli = IdunClient()
58
+ res = dict(cli.complete(prompt))
59
+ return {
60
+ "text": res.get("text", ""),
61
+ "steps": res.get("steps", []),
62
+ "model": res.get("model", ""),
63
+ }
64
+
65
+
66
+ def _dispatch(req):
67
+ method = req.get("method")
68
+ rid = req.get("id")
69
+ params = req.get("params") or {}
70
+
71
+ if method == "initialize":
72
+ return {
73
+ "jsonrpc": "2.0", "id": rid,
74
+ "result": {
75
+ "protocolVersion": "2024-11-05",
76
+ "capabilities": {"tools": {}},
77
+ "serverInfo": {"name": "idun-mcp", "version": "0.1.0"},
78
+ },
79
+ }
80
+ if method == "notifications/initialized":
81
+ return None # notification: no response
82
+ if method == "tools/list":
83
+ return {"jsonrpc": "2.0", "id": rid, "result": {"tools": TOOLS}}
84
+ if method == "tools/call":
85
+ name = params.get("name")
86
+ args = params.get("arguments") or {}
87
+ try:
88
+ if name == "idun_chat":
89
+ out = _tool_chat(args.get("prompt", ""))
90
+ content = [{"type": "text", "text": str(out)}]
91
+ elif name == "idun_trace":
92
+ out = _tool_trace(args.get("prompt", ""))
93
+ content = [{"type": "text", "text": json.dumps(out, ensure_ascii=False, indent=2)}]
94
+ else:
95
+ raise ValueError(f"unknown tool: {name}")
96
+ return {"jsonrpc": "2.0", "id": rid, "result": {"content": content}}
97
+ except Exception as e:
98
+ return {"jsonrpc": "2.0", "id": rid,
99
+ "error": {"code": -32603, "message": str(e)[:400]}}
100
+ if rid is not None:
101
+ return {"jsonrpc": "2.0", "id": rid,
102
+ "error": {"code": -32601, "message": f"method not found: {method}"}}
103
+ return None
104
+
105
+
106
+ def main():
107
+ for line in sys.stdin:
108
+ line = line.strip()
109
+ if not line:
110
+ continue
111
+ try:
112
+ req = json.loads(line)
113
+ except json.JSONDecodeError:
114
+ continue
115
+ resp = _dispatch(req)
116
+ if resp is not None:
117
+ sys.stdout.write(json.dumps(resp, ensure_ascii=False) + "\n")
118
+ sys.stdout.flush()
119
+
120
+
121
+ if __name__ == "__main__":
122
+ main()
@@ -0,0 +1,11 @@
1
+ Metadata-Version: 2.4
2
+ Name: idun-sdk
3
+ Version: 0.1.0
4
+ Summary: Thin client + CLI for Azure AI Foundry agent NatureLM-Idun-5-MoE
5
+ Classifier: Programming Language :: Python :: 3
6
+ Classifier: License :: OSI Approved :: MIT License
7
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
8
+ Requires-Python: >=3.8
9
+ Dynamic: classifier
10
+ Dynamic: requires-python
11
+ Dynamic: summary
@@ -0,0 +1,13 @@
1
+ README.md
2
+ idun_cli.py
3
+ idun_mcp.py
4
+ setup.py
5
+ idun/__init__.py
6
+ idun/auth.py
7
+ idun/client.py
8
+ idun_sdk.egg-info/PKG-INFO
9
+ idun_sdk.egg-info/SOURCES.txt
10
+ idun_sdk.egg-info/dependency_links.txt
11
+ idun_sdk.egg-info/entry_points.txt
12
+ idun_sdk.egg-info/top_level.txt
13
+ tests/test_sdk.py
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ idun = idun_cli:main
@@ -0,0 +1,3 @@
1
+ idun
2
+ idun_cli
3
+ idun_mcp
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,18 @@
1
+ from setuptools import setup, find_packages
2
+
3
+ setup(
4
+ name="idun-sdk",
5
+ version="0.1.0",
6
+ description="Thin client + CLI for Azure AI Foundry agent NatureLM-Idun-5-MoE",
7
+ packages=find_packages(),
8
+ py_modules=["idun_cli", "idun_mcp"],
9
+ python_requires=">=3.8",
10
+ # stdlib-only: no runtime dependencies. Works headless on Termux.
11
+ install_requires=[],
12
+ entry_points={"console_scripts": ["idun=idun_cli:main"]},
13
+ classifiers=[
14
+ "Programming Language :: Python :: 3",
15
+ "License :: OSI Approved :: MIT License",
16
+ "Topic :: Scientific/Engineering :: Artificial Intelligence",
17
+ ],
18
+ )
@@ -0,0 +1,65 @@
1
+ """pytest suite for idun-sdk (offline; no live Foundry call).
2
+
3
+ Covers:
4
+ - trajectory normalization (output array -> steps + text)
5
+ - request payload shape (model-router, no tools key)
6
+ - CLI entrypoint exposes login/chat/trace
7
+ - package importable as `idun`
8
+ """
9
+ import os
10
+ import sys
11
+
12
+ sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
13
+
14
+ from idun.client import IdunClient, _normalize_output
15
+
16
+
17
+ SAMPLE = {
18
+ "model": "gpt-5.4-2026-03-05",
19
+ "output": [
20
+ {"type": "message", "role": "assistant",
21
+ "content": [{"type": "output_text", "text": "Ich habe recherchiert: "}]},
22
+ {"type": "reasoning", "text": "Ich pruefe, ob Contoso eine reale Marke ist."},
23
+ {"type": "message", "role": "assistant",
24
+ "content": [{"type": "output_text", "text": "Contoso setzt auf Kreislauf."}]},
25
+ {"type": "web_search_call", "action": {"query": "Contoso Nachhaltigkeit"},
26
+ "status": "completed", "id": "call_1"},
27
+ ],
28
+ }
29
+
30
+
31
+ def test_normalize_trajectory():
32
+ out = _normalize_output(SAMPLE)
33
+ assert out.text.startswith("Ich habe recherchiert")
34
+ assert "Contoso setzt auf Kreislauf" in out.text
35
+ assert out.model == "gpt-5.4-2026-03-05"
36
+ steps = out.steps
37
+ kinds = [s.kind for s in steps]
38
+ assert "reasoning" in kinds
39
+ assert "tool" in kinds
40
+ tool = next(s for s in steps if s.kind == "tool")
41
+ assert tool.tool == "web_search"
42
+ assert tool.query == "Contoso Nachhaltigkeit"
43
+ assert tool.status == "completed"
44
+
45
+
46
+ def test_request_payload_shape():
47
+ cli = IdunClient.__new__(IdunClient)
48
+ payload = cli._build_payload("Hallo")
49
+ assert payload["model"] == "model-router"
50
+ assert payload["input"] == "Hallo"
51
+ assert payload["max_output_tokens"] == 4096
52
+ assert "tools" not in payload
53
+
54
+
55
+ def test_cli_entrypoint():
56
+ from idun_cli import build_parser
57
+ parser = build_parser()
58
+ args = parser.parse_args(["trace", "Was macht Contoso?"])
59
+ assert args.command == "trace"
60
+ assert args.prompt == "Was macht Contoso?"
61
+
62
+
63
+ def test_package_importable():
64
+ import idun
65
+ assert hasattr(idun, "__version__")