Aasher commited on
Commit
30844f5
·
1 Parent(s): c143df6

feat: Add cli for interacting with Axiom agent

Browse files
Files changed (5) hide show
  1. cli.py +131 -0
  2. pyproject.toml +2 -0
  3. src/axiom/agent.py +1 -0
  4. src/axiom/config.py +0 -3
  5. uv.lock +68 -0
cli.py ADDED
@@ -0,0 +1,131 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import asyncio
2
+ from typing import List, Dict, Optional
3
+
4
+ from src.axiom.agent import AxiomAgent
5
+ from src.axiom.config import settings, load_mcp_servers_from_config
6
+ from agents.mcp import MCPServer
7
+ import logging
8
+
9
+ # --- Configure Logging ---
10
+ httpx_logger = logging.getLogger("httpx")
11
+ httpx_logger.setLevel(logging.WARNING)
12
+
13
+ # Use Rich for pretty output
14
+ from rich.console import Console
15
+ from rich.markdown import Markdown
16
+ from rich.rule import Rule
17
+ from rich.text import Text
18
+
19
+ console = Console()
20
+
21
+ async def get_user_input(prompt_text: str) -> str:
22
+ """Gets user input asynchronously with a styled prompt."""
23
+ # Render the prompt text using Rich
24
+ prompt_markup = Text.from_markup(prompt_text, style="bold blue")
25
+ return await asyncio.to_thread(console.input, prompt_markup)
26
+
27
+
28
+ async def main():
29
+ console.print(Rule("[bold blue] Welcome to Axiom CLI [/bold blue]", style="blue"))
30
+ console.print("[dim]Type 'quit' or 'exit' to end the chat.[/dim]")
31
+ console.print("") # Blank line
32
+
33
+ loaded_mcp_servers: List[MCPServer] = []
34
+ started_mcp_servers: List[MCPServer] = []
35
+ agent: Optional[AxiomAgent] = None
36
+ chat_history: List[Dict[str, str]] = []
37
+
38
+ try:
39
+ # 1. Load MCP Servers
40
+ console.print("[bold]Loading MCP configurations...[/bold]")
41
+ try:
42
+ loaded_mcp_servers = load_mcp_servers_from_config()
43
+ if loaded_mcp_servers:
44
+ console.print(f"[green]Loaded {len(loaded_mcp_servers)} server(s) config.[/green] Attempting to start...")
45
+ else:
46
+ console.print("[yellow]No MCP server configurations found or loaded.[/yellow]")
47
+
48
+ except Exception as e:
49
+ console.print(f"[bold red]Error loading MCP config:[/bold red] [red]{e}[/red]")
50
+
51
+ # 2. Start MCP Servers
52
+ if loaded_mcp_servers:
53
+ for server in loaded_mcp_servers:
54
+ try:
55
+ await server.__aenter__()
56
+ started_mcp_servers.append(server)
57
+ console.print(f" [green]Started:[/green] {server.name}")
58
+ except Exception as e:
59
+ console.print(f" [yellow]Failed to start:[/yellow] {server.name} - [yellow]{e}[/yellow]")
60
+
61
+ if loaded_mcp_servers and not started_mcp_servers:
62
+ console.print("[yellow]Warning: All configured MCP servers failed to start. Agent will operate without MCP tools.[/yellow]")
63
+ elif started_mcp_servers:
64
+ console.print(f"[green]Successfully started {len(started_mcp_servers)} MCP server(s).[/green]")
65
+
66
+ # 3. Initialize the Agent
67
+ console.print("[bold]Initializing Agent...[/bold]")
68
+ try:
69
+ agent = AxiomAgent(mcp_servers=started_mcp_servers)
70
+ console.print(f"[bold green]{settings.AGENT_NAME} is ready![/bold green]")
71
+ except Exception as e:
72
+ console.print(f"[bold red]Fatal Error: Could not initialize Agent:[/bold red] [red]{e}[/red]")
73
+ raise
74
+
75
+ console.print(Rule(style="blue"))
76
+
77
+ # 4. Main chat loop
78
+ while True:
79
+ # Get user input with styled prompt
80
+ user_input = await get_user_input("You: ")
81
+
82
+ if user_input.lower() in ['quit', 'exit']:
83
+ break
84
+
85
+ chat_history.append({"role": "user", "content": user_input})
86
+
87
+ console.print("[bold green]Axiom:[/bold green]") # Print Agent prefix before response
88
+
89
+ full_response = ""
90
+ # Use Rich Status for the thinking indicator while streaming
91
+ with console.status("[bold green]Thinking...[/bold green]", spinner="dots", speed=0.5) as status:
92
+ try:
93
+ response_generator = agent.stream_agent(chat_history)
94
+ async for token in response_generator:
95
+ full_response += token
96
+ status.stop()
97
+
98
+ except Exception as e:
99
+ status.stop()
100
+ console.print(f"\n[bold red]Error during response:[/bold red] [red]{e}[/red]", style="red")
101
+ full_response = f"[Error: {e}]"
102
+
103
+
104
+ # Render the full response using Markdown
105
+ if full_response:
106
+ console.print(Markdown(full_response)) # Renders code blocks, lists, etc.
107
+ else:
108
+ console.print("[dim](No response generated)[/dim]")
109
+
110
+ chat_history.append({"role": "assistant", "content": full_response})
111
+ console.print(Rule(style="blue")) # Print a rule after each turn
112
+
113
+ except Exception as e:
114
+ # Catch any unexpected errors from Agent init or loop setup
115
+ console.print(f"\n[bold red]An unhandled error occurred:[/bold red] [red]{e}[/red]", style="red", highlight=True)
116
+
117
+ finally:
118
+ # 5. Cleanup: Stop MCP Servers
119
+ if started_mcp_servers:
120
+ console.print(Rule("[dim]Stopping MCP servers[/dim]", style="blue"))
121
+ for server in started_mcp_servers:
122
+ try:
123
+ await server.__aexit__(None, None, None)
124
+ console.print(f" [dim]Stopped:[/dim] {server.name}")
125
+ except Exception as e:
126
+ console.print(f" [red]Error stopping:[/red] {server.name} - [red]{e}[/red]")
127
+
128
+ console.print(Rule("[bold blue] Chat ended. Goodbye! [/bold blue]", style="blue"))
129
+
130
+ if __name__ == "__main__":
131
+ asyncio.run(main())
pyproject.toml CHANGED
@@ -7,7 +7,9 @@ requires-python = ">=3.11"
7
  dependencies = [
8
  "chainlit>=2.5.5",
9
  "openai-agents>=0.0.11",
 
10
  "python-dotenv>=1.1.0",
 
11
  ]
12
 
13
  [tool.hatch.build.targets.wheel]
 
7
  dependencies = [
8
  "chainlit>=2.5.5",
9
  "openai-agents>=0.0.11",
10
+ "prompt-toolkit>=3.0.51",
11
  "python-dotenv>=1.1.0",
12
+ "rich>=14.0.0",
13
  ]
14
 
15
  [tool.hatch.build.targets.wheel]
src/axiom/agent.py CHANGED
@@ -93,6 +93,7 @@ class AxiomAgent:
93
  result = Runner.run_streamed(
94
  starting_agent=self.agent,
95
  input=chat_history,
 
96
  run_config=config
97
  )
98
  async for event in result.stream_events():
 
93
  result = Runner.run_streamed(
94
  starting_agent=self.agent,
95
  input=chat_history,
96
+ max_turns=20,
97
  run_config=config
98
  )
99
  async for event in result.stream_events():
src/axiom/config.py CHANGED
@@ -80,8 +80,6 @@ def load_mcp_servers_from_config(config_path: Path = settings.MCP_CONFIG_PATH) -
80
  logger.error(f"MCP configuration file not found: {config_path}")
81
  raise FileNotFoundError(f"MCP configuration file not found: {config_path}")
82
 
83
- logger.info(f"Loading MCP servers from: {config_path}")
84
-
85
  # Allow json.JSONDecodeError to propagate if file is invalid JSON
86
  with open(config_path, 'r', encoding='utf-8') as f:
87
  data = json.load(f)
@@ -104,7 +102,6 @@ def load_mcp_servers_from_config(config_path: Path = settings.MCP_CONFIG_PATH) -
104
  }
105
  )
106
  servers.append(server_instance)
107
- logger.info(f"Prepared MCP Server: {name}")
108
 
109
  except (ValidationError, Exception) as e:
110
  logger.warning(f"Skipping MCP server '{name}' due to configuration error: {e}")
 
80
  logger.error(f"MCP configuration file not found: {config_path}")
81
  raise FileNotFoundError(f"MCP configuration file not found: {config_path}")
82
 
 
 
83
  # Allow json.JSONDecodeError to propagate if file is invalid JSON
84
  with open(config_path, 'r', encoding='utf-8') as f:
85
  data = json.load(f)
 
102
  }
103
  )
104
  servers.append(server_instance)
 
105
 
106
  except (ValidationError, Exception) as e:
107
  logger.warning(f"Skipping MCP server '{name}' due to configuration error: {e}")
uv.lock CHANGED
@@ -169,14 +169,18 @@ source = { editable = "." }
169
  dependencies = [
170
  { name = "chainlit" },
171
  { name = "openai-agents" },
 
172
  { name = "python-dotenv" },
 
173
  ]
174
 
175
  [package.metadata]
176
  requires-dist = [
177
  { name = "chainlit", specifier = ">=2.5.5" },
178
  { name = "openai-agents", specifier = ">=0.0.11" },
 
179
  { name = "python-dotenv", specifier = ">=1.1.0" },
 
180
  ]
181
 
182
  [[package]]
@@ -707,6 +711,18 @@ dependencies = [
707
  ]
708
  sdist = { url = "https://files.pythonhosted.org/packages/7e/c1/7bd34ad0ae6cfd99512f8a40b28b9624c3b1f4e1d40c9038eabc2f870b15/literalai-0.1.201.tar.gz", hash = "sha256:29e4ccadd9d68bfea319a7f0b4fc32611b081990d9195f98e5e97a14d24d3713", size = 67832 }
709
 
 
 
 
 
 
 
 
 
 
 
 
 
710
  [[package]]
711
  name = "markupsafe"
712
  version = "3.0.2"
@@ -786,6 +802,15 @@ wheels = [
786
  { url = "https://files.pythonhosted.org/packages/10/30/20a7f33b0b884a9d14dd3aa94ff1ac9da1479fe2ad66dd9e2736075d2506/mcp-1.6.0-py3-none-any.whl", hash = "sha256:7bd24c6ea042dbec44c754f100984d186620d8b841ec30f1b19eda9b93a634d0", size = 76077 },
787
  ]
788
 
 
 
 
 
 
 
 
 
 
789
  [[package]]
790
  name = "monotonic"
791
  version = "1.6"
@@ -1567,6 +1592,18 @@ wheels = [
1567
  { url = "https://files.pythonhosted.org/packages/54/e2/c158366e621562ef224f132e75c1d1c1fce6b078a19f7d8060451a12d4b9/posthog-3.25.0-py2.py3-none-any.whl", hash = "sha256:85db78c13d1ecb11aed06fad53759c4e8fb3633442c2f3d0336bc0ce8a585d30", size = 89115 },
1568
  ]
1569
 
 
 
 
 
 
 
 
 
 
 
 
 
1570
  [[package]]
1571
  name = "propcache"
1572
  version = "0.3.1"
@@ -1748,6 +1785,15 @@ wheels = [
1748
  { url = "https://files.pythonhosted.org/packages/b6/5f/d6d641b490fd3ec2c4c13b4244d68deea3a1b970a97be64f34fb5504ff72/pydantic_settings-2.9.1-py3-none-any.whl", hash = "sha256:59b4f431b1defb26fe620c71a7d3968a710d719f5f4cdbbdb7926edeb770f6ef", size = 44356 },
1749
  ]
1750
 
 
 
 
 
 
 
 
 
 
1751
  [[package]]
1752
  name = "pyjwt"
1753
  version = "2.10.1"
@@ -1915,6 +1961,19 @@ wheels = [
1915
  { url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928 },
1916
  ]
1917
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1918
  [[package]]
1919
  name = "simple-websocket"
1920
  version = "1.1.0"
@@ -2252,6 +2311,15 @@ wheels = [
2252
  { url = "https://files.pythonhosted.org/packages/3f/82/45dddf4f5bf8b73ba27382cebb2bb3c0ee922c7ef77d936b86276aa39dca/watchfiles-0.20.0-cp37-abi3-win_arm64.whl", hash = "sha256:b17d4176c49d207865630da5b59a91779468dd3e08692fe943064da260de2c7c", size = 265344 },
2253
  ]
2254
 
 
 
 
 
 
 
 
 
 
2255
  [[package]]
2256
  name = "wrapt"
2257
  version = "1.17.2"
 
169
  dependencies = [
170
  { name = "chainlit" },
171
  { name = "openai-agents" },
172
+ { name = "prompt-toolkit" },
173
  { name = "python-dotenv" },
174
+ { name = "rich" },
175
  ]
176
 
177
  [package.metadata]
178
  requires-dist = [
179
  { name = "chainlit", specifier = ">=2.5.5" },
180
  { name = "openai-agents", specifier = ">=0.0.11" },
181
+ { name = "prompt-toolkit", specifier = ">=3.0.51" },
182
  { name = "python-dotenv", specifier = ">=1.1.0" },
183
+ { name = "rich", specifier = ">=14.0.0" },
184
  ]
185
 
186
  [[package]]
 
711
  ]
712
  sdist = { url = "https://files.pythonhosted.org/packages/7e/c1/7bd34ad0ae6cfd99512f8a40b28b9624c3b1f4e1d40c9038eabc2f870b15/literalai-0.1.201.tar.gz", hash = "sha256:29e4ccadd9d68bfea319a7f0b4fc32611b081990d9195f98e5e97a14d24d3713", size = 67832 }
713
 
714
+ [[package]]
715
+ name = "markdown-it-py"
716
+ version = "3.0.0"
717
+ source = { registry = "https://pypi.org/simple" }
718
+ dependencies = [
719
+ { name = "mdurl" },
720
+ ]
721
+ sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596 }
722
+ wheels = [
723
+ { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528 },
724
+ ]
725
+
726
  [[package]]
727
  name = "markupsafe"
728
  version = "3.0.2"
 
802
  { url = "https://files.pythonhosted.org/packages/10/30/20a7f33b0b884a9d14dd3aa94ff1ac9da1479fe2ad66dd9e2736075d2506/mcp-1.6.0-py3-none-any.whl", hash = "sha256:7bd24c6ea042dbec44c754f100984d186620d8b841ec30f1b19eda9b93a634d0", size = 76077 },
803
  ]
804
 
805
+ [[package]]
806
+ name = "mdurl"
807
+ version = "0.1.2"
808
+ source = { registry = "https://pypi.org/simple" }
809
+ sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729 }
810
+ wheels = [
811
+ { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979 },
812
+ ]
813
+
814
  [[package]]
815
  name = "monotonic"
816
  version = "1.6"
 
1592
  { url = "https://files.pythonhosted.org/packages/54/e2/c158366e621562ef224f132e75c1d1c1fce6b078a19f7d8060451a12d4b9/posthog-3.25.0-py2.py3-none-any.whl", hash = "sha256:85db78c13d1ecb11aed06fad53759c4e8fb3633442c2f3d0336bc0ce8a585d30", size = 89115 },
1593
  ]
1594
 
1595
+ [[package]]
1596
+ name = "prompt-toolkit"
1597
+ version = "3.0.51"
1598
+ source = { registry = "https://pypi.org/simple" }
1599
+ dependencies = [
1600
+ { name = "wcwidth" },
1601
+ ]
1602
+ sdist = { url = "https://files.pythonhosted.org/packages/bb/6e/9d084c929dfe9e3bfe0c6a47e31f78a25c54627d64a66e884a8bf5474f1c/prompt_toolkit-3.0.51.tar.gz", hash = "sha256:931a162e3b27fc90c86f1b48bb1fb2c528c2761475e57c9c06de13311c7b54ed", size = 428940 }
1603
+ wheels = [
1604
+ { url = "https://files.pythonhosted.org/packages/ce/4f/5249960887b1fbe561d9ff265496d170b55a735b76724f10ef19f9e40716/prompt_toolkit-3.0.51-py3-none-any.whl", hash = "sha256:52742911fde84e2d423e2f9a4cf1de7d7ac4e51958f648d9540e0fb8db077b07", size = 387810 },
1605
+ ]
1606
+
1607
  [[package]]
1608
  name = "propcache"
1609
  version = "0.3.1"
 
1785
  { url = "https://files.pythonhosted.org/packages/b6/5f/d6d641b490fd3ec2c4c13b4244d68deea3a1b970a97be64f34fb5504ff72/pydantic_settings-2.9.1-py3-none-any.whl", hash = "sha256:59b4f431b1defb26fe620c71a7d3968a710d719f5f4cdbbdb7926edeb770f6ef", size = 44356 },
1786
  ]
1787
 
1788
+ [[package]]
1789
+ name = "pygments"
1790
+ version = "2.19.1"
1791
+ source = { registry = "https://pypi.org/simple" }
1792
+ sdist = { url = "https://files.pythonhosted.org/packages/7c/2d/c3338d48ea6cc0feb8446d8e6937e1408088a72a39937982cc6111d17f84/pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f", size = 4968581 }
1793
+ wheels = [
1794
+ { url = "https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c", size = 1225293 },
1795
+ ]
1796
+
1797
  [[package]]
1798
  name = "pyjwt"
1799
  version = "2.10.1"
 
1961
  { url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928 },
1962
  ]
1963
 
1964
+ [[package]]
1965
+ name = "rich"
1966
+ version = "14.0.0"
1967
+ source = { registry = "https://pypi.org/simple" }
1968
+ dependencies = [
1969
+ { name = "markdown-it-py" },
1970
+ { name = "pygments" },
1971
+ ]
1972
+ sdist = { url = "https://files.pythonhosted.org/packages/a1/53/830aa4c3066a8ab0ae9a9955976fb770fe9c6102117c8ec4ab3ea62d89e8/rich-14.0.0.tar.gz", hash = "sha256:82f1bc23a6a21ebca4ae0c45af9bdbc492ed20231dcb63f297d6d1021a9d5725", size = 224078 }
1973
+ wheels = [
1974
+ { url = "https://files.pythonhosted.org/packages/0d/9b/63f4c7ebc259242c89b3acafdb37b41d1185c07ff0011164674e9076b491/rich-14.0.0-py3-none-any.whl", hash = "sha256:1c9491e1951aac09caffd42f448ee3d04e58923ffe14993f6e83068dc395d7e0", size = 243229 },
1975
+ ]
1976
+
1977
  [[package]]
1978
  name = "simple-websocket"
1979
  version = "1.1.0"
 
2311
  { url = "https://files.pythonhosted.org/packages/3f/82/45dddf4f5bf8b73ba27382cebb2bb3c0ee922c7ef77d936b86276aa39dca/watchfiles-0.20.0-cp37-abi3-win_arm64.whl", hash = "sha256:b17d4176c49d207865630da5b59a91779468dd3e08692fe943064da260de2c7c", size = 265344 },
2312
  ]
2313
 
2314
+ [[package]]
2315
+ name = "wcwidth"
2316
+ version = "0.2.13"
2317
+ source = { registry = "https://pypi.org/simple" }
2318
+ sdist = { url = "https://files.pythonhosted.org/packages/6c/63/53559446a878410fc5a5974feb13d31d78d752eb18aeba59c7fef1af7598/wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5", size = 101301 }
2319
+ wheels = [
2320
+ { url = "https://files.pythonhosted.org/packages/fd/84/fd2ba7aafacbad3c4201d395674fc6348826569da3c0937e75505ead3528/wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859", size = 34166 },
2321
+ ]
2322
+
2323
  [[package]]
2324
  name = "wrapt"
2325
  version = "1.17.2"