netdocai 0.1.0__py3-none-any.whl

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.
netdocai/__init__.py ADDED
File without changes
netdocai/agent.py ADDED
@@ -0,0 +1,320 @@
1
+ import os
2
+ import sys
3
+ from typing import Any, TypedDict
4
+
5
+ from google.genai import types
6
+ from .gemini import generate_content
7
+ from langgraph.graph import END, START, StateGraph
8
+ from mcp import ClientSession, StdioServerParameters
9
+ from mcp.client.stdio import stdio_client
10
+
11
+ from .prompts import SYSTEM_PROMPT, build_user_prompt
12
+ from .report import build_report
13
+
14
+
15
+ MAX_STEPS = 8
16
+
17
+
18
+ class AgentState(TypedDict):
19
+ problem: str
20
+ history: list[dict[str, Any]]
21
+ tool_definitions: list[dict[str, Any]]
22
+ pending_calls: list[dict[str, Any]]
23
+ observations: list[dict[str, Any]]
24
+ hypothesis: str
25
+ response: str
26
+ steps: int
27
+
28
+
29
+ def build_mcp_tools(
30
+ tool_definitions: list[dict[str, Any]],
31
+ ) -> list[types.Tool]:
32
+ declarations = []
33
+
34
+ for tool in tool_definitions:
35
+ declarations.append(
36
+ types.FunctionDeclaration(
37
+ name=tool["name"],
38
+ description=tool["description"],
39
+ parameters_json_schema=tool["input_schema"],
40
+ )
41
+ )
42
+
43
+ return [types.Tool(function_declarations=declarations)]
44
+
45
+
46
+ async def get_tool_definitions() -> list[dict[str, Any]]:
47
+ server_params = StdioServerParameters(
48
+ command=sys.executable,
49
+ args=["-m", "netdocai.mcp_server"],
50
+ env=os.environ.copy(),
51
+ )
52
+
53
+ async with stdio_client(server_params) as (read, write):
54
+ async with ClientSession(read, write) as session:
55
+ await session.initialize()
56
+
57
+ result = await session.list_tools()
58
+
59
+ return [
60
+ {
61
+ "name": tool.name,
62
+ "description": tool.description or "",
63
+ "input_schema": tool.input_schema,
64
+ }
65
+ for tool in result.tools
66
+ ]
67
+
68
+
69
+ async def plan(state: AgentState) -> AgentState:
70
+ contents = [
71
+ types.Content.model_validate(message)
72
+ for message in state["history"]
73
+ ]
74
+
75
+ gemini_tools = build_mcp_tools(state["tool_definitions"])
76
+
77
+ response = await generate_content(
78
+ contents=contents,
79
+ config=types.GenerateContentConfig(
80
+ tools=gemini_tools,
81
+ automatic_function_calling=types.AutomaticFunctionCallingConfig(
82
+ disable=True
83
+ ),
84
+ ),
85
+ )
86
+
87
+ function_calls = response.function_calls
88
+
89
+ if not function_calls:
90
+ print("[Agent] Finalizing diagnosis...")
91
+
92
+ report = build_report(
93
+ problem=state["problem"],
94
+ observations=state["observations"],
95
+ hypothesis=state["hypothesis"],
96
+ conclusion=response.text or "",
97
+ )
98
+
99
+ return {
100
+ **state,
101
+ "response": report,
102
+ "pending_calls": [],
103
+ }
104
+
105
+ model_content = response.candidates[0].content
106
+
107
+ updated_history = [
108
+ *state["history"],
109
+ model_content.model_dump(exclude_none=True),
110
+ ]
111
+
112
+ pending_calls = [
113
+ {
114
+ "name": call.name,
115
+ "args": call.args or {},
116
+ }
117
+ for call in function_calls
118
+ ]
119
+
120
+ print(
121
+ "[Agent] "
122
+ + ", ".join(call["name"] for call in pending_calls)
123
+ )
124
+
125
+ return {
126
+ **state,
127
+ "history": updated_history,
128
+ "pending_calls": pending_calls,
129
+ "steps": state["steps"] + 1,
130
+ }
131
+
132
+
133
+ async def execute_tools(state: AgentState) -> AgentState:
134
+ server_params = StdioServerParameters(
135
+ command=sys.executable,
136
+ args=["-m", "netdocai.mcp_server"],
137
+ env=os.environ.copy(),
138
+ )
139
+
140
+ updated_history = list(state["history"])
141
+ observations = list(state["observations"])
142
+
143
+ async with stdio_client(server_params) as (read, write):
144
+ async with ClientSession(read, write) as session:
145
+ await session.initialize()
146
+
147
+ for call in state["pending_calls"]:
148
+ name = call["name"]
149
+ arguments = call["args"]
150
+
151
+ print(f"[Tool] Executing {name}...")
152
+
153
+ result = await session.call_tool(
154
+ name,
155
+ arguments=arguments,
156
+ )
157
+
158
+ result_text = "\n".join(
159
+ content.text
160
+ for content in result.content
161
+ if hasattr(content, "text") and content.text
162
+ )
163
+
164
+ if not result_text:
165
+ result_text = "Tool returned no text result."
166
+
167
+ print(f"[Tool] {name} completed.")
168
+
169
+ observations.append(
170
+ {
171
+ "tool": name,
172
+ "arguments": arguments,
173
+ "result": result_text,
174
+ }
175
+ )
176
+
177
+ function_response = types.Part.from_function_response(
178
+ name=name,
179
+ response={"result": result_text},
180
+ )
181
+
182
+ updated_history.append(
183
+ types.Content(
184
+ role="user",
185
+ parts=[function_response],
186
+ ).model_dump(exclude_none=True)
187
+ )
188
+
189
+ return {
190
+ **state,
191
+ "history": updated_history,
192
+ "observations": observations,
193
+ "pending_calls": [],
194
+ }
195
+
196
+
197
+ async def assess(state: AgentState) -> AgentState:
198
+ observation_text = "\n\n".join(
199
+ f"Tool: {item['tool']}\n"
200
+ f"Arguments: {item['arguments']}\n"
201
+ f"Result:\n{item['result']}"
202
+ for item in state["observations"]
203
+ )
204
+
205
+ prompt = f"""
206
+ You are assessing an ongoing network troubleshooting investigation.
207
+
208
+ Problem:
209
+ {state["problem"]}
210
+
211
+ Observed evidence:
212
+ {observation_text}
213
+
214
+ Current hypothesis:
215
+ {state["hypothesis"] or "None yet."}
216
+
217
+ Based only on the available evidence:
218
+
219
+ 1. State the most likely current hypothesis.
220
+ 2. Identify the strongest supporting evidence.
221
+ 3. Identify what remains uncertain.
222
+ 4. Do not claim that a test was performed unless it appears in the evidence.
223
+
224
+ Keep the assessment concise.
225
+ """
226
+
227
+ response = await generate_content(
228
+ contents=prompt,
229
+ config=types.GenerateContentConfig(),
230
+ )
231
+
232
+ return {
233
+ **state,
234
+ "hypothesis": response.text or "",
235
+ }
236
+
237
+
238
+ async def finalize(state: AgentState) -> dict[str, Any]:
239
+ report = build_report(
240
+ problem=state["problem"],
241
+ observations=state["observations"],
242
+ hypothesis=state["hypothesis"],
243
+ conclusion=(
244
+ "The investigation reached the maximum number of diagnostic "
245
+ "steps before a final diagnosis was produced."
246
+ ),
247
+ )
248
+
249
+ return {
250
+ "response": report,
251
+ }
252
+
253
+
254
+ def route_after_plan(state: AgentState) -> str:
255
+ if state["response"]:
256
+ return "done"
257
+
258
+ if state["steps"] > MAX_STEPS:
259
+ return "finalize"
260
+
261
+ return "execute"
262
+
263
+
264
+ graph_builder = StateGraph(AgentState)
265
+
266
+ graph_builder.add_node("plan", plan)
267
+ graph_builder.add_node("execute_tools", execute_tools)
268
+ graph_builder.add_node("assess", assess)
269
+ graph_builder.add_node("finalize", finalize)
270
+
271
+ graph_builder.add_edge(START, "plan")
272
+
273
+ graph_builder.add_conditional_edges(
274
+ "plan",
275
+ route_after_plan,
276
+ {
277
+ "execute": "execute_tools",
278
+ "finalize": "finalize",
279
+ "done": END,
280
+ },
281
+ )
282
+
283
+ graph_builder.add_edge("execute_tools", "assess")
284
+ graph_builder.add_edge("assess", "plan")
285
+ graph_builder.add_edge("finalize", END)
286
+
287
+ graph = graph_builder.compile()
288
+
289
+
290
+ async def run_agent(problem: str) -> str:
291
+ tool_definitions = await get_tool_definitions()
292
+
293
+ initial_history = [
294
+ {
295
+ "role": "user",
296
+ "parts": [
297
+ {
298
+ "text": (
299
+ f"{SYSTEM_PROMPT}\n\n"
300
+ f"{build_user_prompt(problem)}"
301
+ )
302
+ }
303
+ ],
304
+ }
305
+ ]
306
+
307
+ result = await graph.ainvoke(
308
+ {
309
+ "problem": problem,
310
+ "history": initial_history,
311
+ "tool_definitions": tool_definitions,
312
+ "pending_calls": [],
313
+ "observations": [],
314
+ "hypothesis": "",
315
+ "response": "",
316
+ "steps": 0,
317
+ }
318
+ )
319
+
320
+ return result["response"]
netdocai/config.py ADDED
@@ -0,0 +1,44 @@
1
+ import os
2
+ from dataclasses import dataclass
3
+
4
+ from dotenv import load_dotenv
5
+
6
+
7
+ DEFAULT_GEMINI_MODEL = "gemini-3.6-flash"
8
+
9
+
10
+ @dataclass(frozen=True)
11
+ class Settings:
12
+ gemini_api_key: str
13
+ gemini_model: str = DEFAULT_GEMINI_MODEL
14
+
15
+ @classmethod
16
+ def from_env(cls) -> "Settings":
17
+ load_dotenv()
18
+
19
+ api_key = os.getenv("GEMINI_API_KEY", "").strip()
20
+
21
+ if not api_key:
22
+ raise RuntimeError(
23
+ "Missing required environment variable: GEMINI_API_KEY"
24
+ )
25
+
26
+ model = os.getenv(
27
+ "GEMINI_MODEL",
28
+ DEFAULT_GEMINI_MODEL,
29
+ )
30
+
31
+ return cls(
32
+ gemini_api_key=api_key,
33
+ gemini_model=model,
34
+ )
35
+
36
+
37
+ class LazySettings:
38
+ def __getattr__(self, name: str):
39
+ return getattr(Settings.from_env(), name)
40
+
41
+
42
+ settings = LazySettings()
43
+
44
+ __all__ = ["Settings", "settings"]
netdocai/gemini.py ADDED
@@ -0,0 +1,44 @@
1
+ import asyncio
2
+ from typing import Any
3
+
4
+ from google import genai
5
+ from google.genai import types
6
+ from google.genai.errors import ClientError, ServerError
7
+
8
+ from .config import settings
9
+
10
+
11
+ async def generate_content(
12
+ contents: Any,
13
+ config: types.GenerateContentConfig,
14
+ retries: int = 3,
15
+ ) -> Any:
16
+ async with genai.Client(api_key=settings.gemini_api_key).aio as client:
17
+ for attempt in range(retries):
18
+ try:
19
+ return await client.models.generate_content(
20
+ model=settings.gemini_model,
21
+ contents=contents,
22
+ config=config,
23
+ )
24
+
25
+ except ServerError:
26
+ if attempt == retries - 1:
27
+ raise
28
+
29
+ except ClientError as exc:
30
+ status_code = getattr(exc, "code", None)
31
+
32
+ if status_code != 429 or attempt == retries - 1:
33
+ raise
34
+
35
+ delay = 2 ** attempt
36
+
37
+ print(
38
+ f"[Gemini] Temporary API failure. "
39
+ f"Retrying in {delay}s..."
40
+ )
41
+
42
+ await asyncio.sleep(delay)
43
+
44
+ raise RuntimeError("Gemini request failed after all retries.")
@@ -0,0 +1,55 @@
1
+ # DNS Troubleshooting
2
+
3
+ DNS translates domain names into IP addresses.
4
+
5
+ ## Common Outcomes
6
+
7
+ ### Successful Resolution
8
+
9
+ A hostname resolves when the DNS resolver returns one or more valid IP addresses.
10
+
11
+ Check both IPv4 and IPv6 when troubleshooting connectivity:
12
+
13
+ - A record -> IPv4 address
14
+ - AAAA record -> IPv6 address
15
+
16
+ ### NXDOMAIN
17
+
18
+ NXDOMAIN means the queried domain name does not exist according to the DNS server.
19
+
20
+ Possible causes:
21
+ - Typo in hostname
22
+ - Missing DNS record
23
+ - Incorrect domain configuration
24
+
25
+ ### SERVFAIL
26
+
27
+ SERVFAIL means the DNS server failed to provide a valid answer.
28
+
29
+ Possible causes:
30
+ - Upstream DNS failure
31
+ - DNSSEC validation failure
32
+ - Authoritative nameserver problems
33
+ - Temporary resolver problems
34
+
35
+ ### DNS Timeouts
36
+
37
+ A timeout means the DNS query did not receive a response within the expected period.
38
+
39
+ Possible causes:
40
+ - Network connectivity problems
41
+ - Unreachable resolver
42
+ - Firewall filtering
43
+ - Resolver overload
44
+
45
+ ### Troubleshooting
46
+
47
+ When investigating DNS problems:
48
+
49
+ 1. Check whether the hostname resolves.
50
+ 2. Check both A and AAAA records.
51
+ 3. Measure DNS response time.
52
+ 4. Compare results using another resolver.
53
+ 5. Check whether the problem is intermittent or persistent.
54
+
55
+ DNS problems can appear as application failures even when TCP and HTTP connectivity are otherwise healthy.
@@ -0,0 +1,46 @@
1
+ # HTTP Troubleshooting
2
+
3
+ HTTP operates above the transport layer and is used by web applications and APIs.
4
+
5
+ ## HTTP Status Codes
6
+
7
+ ### 2xx
8
+
9
+ Successful request.
10
+
11
+ ### 4xx
12
+
13
+ Client-side request error.
14
+
15
+ ### 5xx
16
+
17
+ Server-side error.
18
+
19
+ ## Response Time
20
+
21
+ HTTP response time measures how long the client waits for the server response.
22
+
23
+ High response time can result from:
24
+
25
+ - Slow application processing
26
+ - Slow upstream services
27
+ - Server overload
28
+ - Network latency
29
+ - Backend database delays
30
+
31
+ ## Time To First Byte
32
+
33
+ TTFB measures the time until the first byte of the response is received.
34
+
35
+ High TTFB can indicate that the server or an upstream dependency is taking a long time to begin generating the response.
36
+
37
+ ## Troubleshooting
38
+
39
+ When investigating HTTP slowness:
40
+
41
+ 1. Check the HTTP status code.
42
+ 2. Measure total response time.
43
+ 3. Measure TTFB when available.
44
+ 4. Compare multiple requests.
45
+ 5. Compare HTTP and HTTPS behavior.
46
+ 6. Determine whether the issue is persistent or intermittent.
@@ -0,0 +1,51 @@
1
+ # Linux Networking Troubleshooting
2
+
3
+ Linux provides several tools for investigating network connectivity.
4
+
5
+ ## Ping
6
+
7
+ Ping measures ICMP reachability and round-trip latency.
8
+
9
+ Useful metrics include:
10
+
11
+ - Packet loss
12
+ - Minimum latency
13
+ - Average latency
14
+ - Maximum latency
15
+ - Jitter
16
+
17
+ Ping does not prove that TCP or HTTP services are working.
18
+
19
+ ## DNS
20
+
21
+ Common commands include:
22
+
23
+ - getent hosts
24
+ - dig
25
+ - nslookup
26
+
27
+ Use DNS tools to determine whether hostname resolution is working correctly.
28
+
29
+ ## TCP
30
+
31
+ Useful tools include:
32
+
33
+ - nc
34
+ - curl
35
+ - ss
36
+
37
+ A TCP connection test checks whether a destination port can be reached.
38
+
39
+ ## HTTP
40
+
41
+ curl can be used to test HTTP and HTTPS endpoints and inspect:
42
+
43
+ - Status code
44
+ - Response headers
45
+ - Response timing
46
+
47
+ ## Traceroute
48
+
49
+ Traceroute can help identify routing paths and where latency or packet loss may occur.
50
+
51
+ A single failed or missing traceroute hop does not necessarily mean that the network path is broken because routers may filter or deprioritize diagnostic traffic.
@@ -0,0 +1,42 @@
1
+ # TCP Troubleshooting
2
+
3
+ TCP provides reliable, connection-oriented communication between applications.
4
+
5
+ ## TCP Connection
6
+
7
+ A successful TCP connection indicates that a client was able to establish a connection to the destination host and port.
8
+
9
+ For HTTPS, the usual destination port is 443.
10
+
11
+ ## Connection Failures
12
+
13
+ Common causes include:
14
+
15
+ - Destination service is not listening
16
+ - Firewall filtering
17
+ - Network routing problems
18
+ - Server unavailable
19
+ - Incorrect port
20
+
21
+ ## Connection Latency
22
+
23
+ Measure the time required to establish a TCP connection.
24
+
25
+ High TCP connection latency can indicate:
26
+
27
+ - Network congestion
28
+ - Routing problems
29
+ - High server load
30
+ - Packet loss and retransmissions
31
+
32
+ ## Troubleshooting
33
+
34
+ When investigating TCP problems:
35
+
36
+ 1. Confirm the destination hostname resolves.
37
+ 2. Attempt a TCP connection to the expected port.
38
+ 3. Measure connection latency.
39
+ 4. Compare results across repeated attempts.
40
+ 5. Check whether failures are intermittent or persistent.
41
+
42
+ A successful TCP connection does not prove that the application itself is healthy.
netdocai/main.py ADDED
@@ -0,0 +1,85 @@
1
+ import asyncio
2
+
3
+ from rich.console import Console
4
+ from rich.panel import Panel
5
+ from rich.prompt import Prompt
6
+
7
+ from .onboarding import get_api_key
8
+
9
+
10
+ console = Console()
11
+
12
+
13
+ def main() -> None:
14
+ console.print(
15
+ Panel(
16
+ "[bold]NetDocAI[/bold]\n"
17
+ "Autonomous Network Troubleshooter",
18
+ expand=False,
19
+ )
20
+ )
21
+
22
+ console.print()
23
+
24
+ try:
25
+ get_api_key()
26
+ except (KeyboardInterrupt, EOFError):
27
+ console.print("\n[yellow]Setup cancelled.[/yellow]")
28
+ return
29
+ except OSError as exc:
30
+ console.print(f"\n[red]Configuration error: {exc}[/red]")
31
+ return
32
+
33
+ from .agent import run_agent
34
+
35
+ console.print()
36
+
37
+ problem = Prompt.ask(
38
+ "[bold]Describe your network problem[/bold]"
39
+ ).strip()
40
+
41
+ if not problem:
42
+ console.print("[red]No problem provided.[/red]")
43
+ return
44
+
45
+ console.print()
46
+ console.print(
47
+ Panel(
48
+ problem,
49
+ title="Problem",
50
+ expand=False,
51
+ )
52
+ )
53
+
54
+ console.print()
55
+ console.print("[bold cyan]Investigating...[/bold cyan]")
56
+ console.print()
57
+
58
+ try:
59
+ with console.status(
60
+ "[bold cyan]NetDocAI is investigating...[/bold cyan]",
61
+ spinner="dots",
62
+ ):
63
+ response = asyncio.run(run_agent(problem))
64
+
65
+ console.print()
66
+ console.print(
67
+ Panel(
68
+ response,
69
+ title="NetDocAI Diagnostic Report",
70
+ border_style="green",
71
+ )
72
+ )
73
+ except Exception as exc:
74
+ console.print()
75
+ console.print(
76
+ Panel(
77
+ f"[red]{exc}[/red]",
78
+ title="Agent Error",
79
+ border_style="red",
80
+ )
81
+ )
82
+
83
+
84
+ if __name__ == "__main__":
85
+ main()