Skip to content

Commit e7588e3

Browse files
authored
Merge pull request #226 from UiPath/feat/generate_agents_md
feat(AGENTS.md): generate the AGENTS.md file
2 parents afc482c + 8659340 commit e7588e3

File tree

6 files changed

+456
-9
lines changed

6 files changed

+456
-9
lines changed

pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
[project]
22
name = "uipath-langchain"
3-
version = "0.0.141"
3+
version = "0.0.142"
44
description = "UiPath Langchain"
55
readme = { file = "README.md", content-type = "text/markdown" }
66
requires-python = ">=3.10"
77
dependencies = [
8-
"uipath>=2.1.96, <2.2.0",
8+
"uipath>=2.1.101, <2.2.0",
99
"langgraph>=0.5.0, <0.7.0",
1010
"langchain-core>=0.3.34",
1111
"langgraph-checkpoint-sqlite>=2.0.3",

src/uipath_langchain/_cli/cli_init.py

Lines changed: 134 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
11
import asyncio
2+
import importlib.resources
23
import json
34
import os
5+
import shutil
46
import uuid
7+
from collections.abc import Generator
8+
from enum import Enum
59
from typing import Any, Callable, Dict, overload
610

11+
import click
712
from langgraph.graph.state import CompiledStateGraph
813
from uipath._cli._utils._console import ConsoleLogger
914
from uipath._cli._utils._parse_ast import generate_bindings_json # type: ignore
@@ -14,6 +19,14 @@
1419
console = ConsoleLogger()
1520

1621

22+
class FileOperationStatus(str, Enum):
23+
"""Status of a file operation."""
24+
25+
CREATED = "created"
26+
UPDATED = "updated"
27+
SKIPPED = "skipped"
28+
29+
1730
def resolve_refs(schema, root=None):
1831
"""Recursively resolves $ref references in a JSON schema."""
1932
if root is None:
@@ -96,6 +109,120 @@ def generate_schema_from_graph(
96109
return schema
97110

98111

112+
def generate_agent_md_file(
113+
target_directory: str,
114+
file_name: str,
115+
resource_name: str,
116+
no_agents_md_override: bool,
117+
) -> tuple[str, FileOperationStatus] | None:
118+
"""Generate an agent-specific file from the packaged resource.
119+
120+
Args:
121+
target_directory: The directory where the file should be created.
122+
file_name: The name of the file should be created.
123+
resource_name: The name of the resource folder where should be the file.
124+
no_agents_md_override: Whether to override existing files.
125+
126+
Returns:
127+
A tuple of (file_name, status) where status is a FileOperationStatus:
128+
- CREATED: File was created
129+
- UPDATED: File was overwritten
130+
- SKIPPED: File exists and no_agents_md_override is True
131+
Returns None if an error occurred.
132+
"""
133+
target_path = os.path.join(target_directory, file_name)
134+
will_override = os.path.exists(target_path)
135+
136+
if will_override and no_agents_md_override:
137+
return file_name, FileOperationStatus.SKIPPED
138+
try:
139+
source_path = importlib.resources.files(resource_name).joinpath(file_name)
140+
141+
with importlib.resources.as_file(source_path) as s_path:
142+
shutil.copy(s_path, target_path)
143+
144+
return (
145+
file_name,
146+
FileOperationStatus.UPDATED
147+
if will_override
148+
else FileOperationStatus.CREATED,
149+
)
150+
151+
except Exception as e:
152+
console.warning(f"Could not create {file_name}: {e}")
153+
return None
154+
155+
156+
def generate_specific_agents_md_files(
157+
target_directory: str, no_agents_md_override: bool
158+
) -> Generator[tuple[str, FileOperationStatus], None, None]:
159+
"""Generate agent-specific files from the packaged resource.
160+
161+
Args:
162+
target_directory: The directory where the files should be created.
163+
no_agents_md_override: Whether to override existing files.
164+
165+
Yields:
166+
Tuple of (file_name, status) for each file operation, where status is a FileOperationStatus:
167+
- CREATED: File was created
168+
- UPDATED: File was overwritten
169+
- SKIPPED: File exists and was not overwritten
170+
"""
171+
agent_dir = os.path.join(target_directory, ".agent")
172+
os.makedirs(agent_dir, exist_ok=True)
173+
174+
file_configs = [
175+
(target_directory, "CLAUDE.md", "uipath._resources"),
176+
(agent_dir, "CLI_REFERENCE.md", "uipath._resources"),
177+
(agent_dir, "SDK_REFERENCE.md", "uipath._resources"),
178+
(target_directory, "AGENTS.md", "uipath_langchain._resources"),
179+
(agent_dir, "REQUIRED_STRUCTURE.md", "uipath_langchain._resources"),
180+
]
181+
182+
for directory, file_name, resource_name in file_configs:
183+
result = generate_agent_md_file(
184+
directory, file_name, resource_name, no_agents_md_override
185+
)
186+
if result:
187+
yield result
188+
189+
190+
def generate_agents_md_files(options: dict[str, Any]) -> None:
191+
"""Generate agent MD files and log categorized summary.
192+
193+
Args:
194+
options: Options dictionary
195+
"""
196+
current_directory = os.getcwd()
197+
no_agents_md_override = options.get("no_agents_md_override", False)
198+
199+
created_files = []
200+
updated_files = []
201+
skipped_files = []
202+
203+
for file_name, status in generate_specific_agents_md_files(
204+
current_directory, no_agents_md_override
205+
):
206+
if status == FileOperationStatus.CREATED:
207+
created_files.append(file_name)
208+
elif status == FileOperationStatus.UPDATED:
209+
updated_files.append(file_name)
210+
elif status == FileOperationStatus.SKIPPED:
211+
skipped_files.append(file_name)
212+
213+
if created_files:
214+
files_str = ", ".join(click.style(f, fg="cyan") for f in created_files)
215+
console.success(f"Created: {files_str}")
216+
217+
if updated_files:
218+
files_str = ", ".join(click.style(f, fg="cyan") for f in updated_files)
219+
console.success(f"Updated: {files_str}")
220+
221+
if skipped_files:
222+
files_str = ", ".join(click.style(f, fg="yellow") for f in skipped_files)
223+
console.info(f"Skipped (already exist): {files_str}")
224+
225+
99226
async def langgraph_init_middleware_async(
100227
entrypoint: str,
101228
options: dict[str, Any] | None = None,
@@ -185,7 +312,9 @@ async def langgraph_init_middleware_async(
185312
try:
186313
with open(mermaid_file_path, "w") as f:
187314
f.write(mermaid_content)
188-
console.success(f" Created '{mermaid_file_path}' file.")
315+
console.success(
316+
f"Created {click.style(mermaid_file_path, fg='cyan')} file."
317+
)
189318
except Exception as write_error:
190319
console.error(
191320
f"Error writing mermaid file for '{graph_name}': {str(write_error)}"
@@ -194,7 +323,10 @@ async def langgraph_init_middleware_async(
194323
should_continue=False,
195324
should_include_stacktrace=True,
196325
)
197-
console.success(f" Created '{config_path}' file.")
326+
console.success(f"Created {click.style(config_path, fg='cyan')} file.")
327+
328+
generate_agents_md_files(options)
329+
198330
return MiddlewareResult(should_continue=False)
199331

200332
except Exception as e:
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# Agent Code Patterns Reference
2+
3+
This document provides practical code patterns for building UiPath coded agents using LangGraph and the UiPath Python SDK.
4+
5+
---
6+
7+
## Documentation Structure
8+
9+
This documentation is split into multiple files for efficient context loading. Load only the files you need:
10+
11+
1. **@.agent/REQUIRED_STRUCTURE.md** - Agent structure patterns and templates
12+
- **When to load:** Creating a new agent or understanding required patterns
13+
- **Contains:** Required Pydantic models (Input, State, Output), LLM initialization patterns, standard agent template
14+
15+
2. **@.agent/SDK_REFERENCE.md** - Complete SDK API reference
16+
- **When to load:** Calling UiPath SDK methods, working with services (actions, assets, jobs, etc.)
17+
- **Contains:** All SDK services and methods with full signatures and type annotations
18+
19+
3. **@.agent/CLI_REFERENCE.md** - CLI commands documentation
20+
- **When to load:** Working with `uipath init`, `uipath run`, or `uipath eval` commands
21+
- **Contains:** Command syntax, options, usage examples, and workflows
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
## Required Agent Structure
2+
3+
**IMPORTANT**: All UiPath coded agents MUST follow this standard structure unless explicitly specified otherwise by the user.
4+
5+
### Required Components
6+
7+
Every agent implementation MUST include these three Pydantic models:
8+
9+
```python
10+
from pydantic import BaseModel
11+
12+
class Input(BaseModel):
13+
"""Define input fields that the agent accepts"""
14+
# Add your input fields here
15+
pass
16+
17+
class State(BaseModel):
18+
"""Define the agent's internal state that flows between nodes"""
19+
# Add your state fields here
20+
pass
21+
22+
class Output(BaseModel):
23+
"""Define output fields that the agent returns"""
24+
# Add your output fields here
25+
pass
26+
```
27+
28+
### Required LLM Initialization
29+
30+
Unless the user explicitly requests a different LLM provider, always use `UiPathChat`:
31+
32+
```python
33+
from uipath_langchain.chat import UiPathChat
34+
35+
llm = UiPathChat(model="gpt-4o-2024-08-06", temperature=0.7)
36+
```
37+
38+
**Alternative LLMs** (only use if explicitly requested):
39+
- `ChatOpenAI` from `langchain_openai`
40+
- `ChatAnthropic` from `langchain_anthropic`
41+
- Other LangChain-compatible LLMs
42+
43+
### Standard Agent Template
44+
45+
Every agent should follow this basic structure:
46+
47+
```python
48+
from langchain_core.messages import SystemMessage, HumanMessage
49+
from langgraph.graph import START, StateGraph, END
50+
from uipath_langchain.chat import UiPathChat
51+
from pydantic import BaseModel
52+
53+
# 1. Define Input, State, and Output models
54+
class Input(BaseModel):
55+
field: str
56+
57+
class State(BaseModel):
58+
field: str
59+
result: str = ""
60+
61+
class Output(BaseModel):
62+
result: str
63+
64+
# 2. Initialize UiPathChat LLM
65+
llm = UiPathChat(model="gpt-4o-2024-08-06", temperature=0.7)
66+
67+
# 3. Define agent nodes (async functions)
68+
async def process_node(state: State) -> State:
69+
response = await llm.ainvoke([HumanMessage(state.field)])
70+
return State(field=state.field, result=response.content)
71+
72+
async def output_node(state: State) -> Output:
73+
return Output(result=state.result)
74+
75+
# 4. Build the graph
76+
builder = StateGraph(State, input=Input, output=Output)
77+
builder.add_node("process", process_node)
78+
builder.add_node("output", output_node)
79+
builder.add_edge(START, "process")
80+
builder.add_edge("process", "output")
81+
builder.add_edge("output", END)
82+
83+
# 5. Compile the graph
84+
graph = builder.compile()
85+
```
86+
87+
**Key Rules**:
88+
1. Always use async/await for all node functions
89+
2. All nodes (except output) must accept and return `State`
90+
3. The final output node must return `Output`
91+
4. Use `StateGraph(State, input=Input, output=Output)` for initialization
92+
5. Always compile with `graph = builder.compile()`

0 commit comments

Comments
 (0)