local-agent: 파일 도구, 세션 지속성, 웹 UI 추가 및 에이전트 코어 리팩토링
이번 커밋에서는 `local-agent` 프로젝트에 파일 시스템 접근 도구, 세션 데이터 저장 및 복구 기능, 그리고 사용자 친화적인 웹 채팅 인터페이스를 도입했습니다. 또한, 에이전트의 핵심 로직을 리팩토링하여 터미널 REPL과 웹 요청/응답 사이클 간의 코드 중복을 제거하고, 보다 유연하고 재개 가능한 제너레이터 형태로 전환했습니다.
요약
이 커밋은 local-agent의 기능을 확장하고 핵심 로직을 개선하는 데 중점을 두었습니다. 새롭게 추가된 기능은 다음과 같습니다:
- 파일 시스템 도구:
read_file및write_file함수를 통해 파일 읽기 및 쓰기 기능을 에이전트가 사용할 수 있게 되었습니다. - 세션 지속성: 대화 기록을 JSON 파일로 저장하고 불러오는 기능을 구현하여
--session및--continue플래그를 통해 이전 세션을 이어갈 수 있도록 했습니다. - 웹 채팅 UI: Flask 기반의 간단한 단일 페이지 채팅 UI를
--web및--port플래그와 함께 제공합니다. 이 UI는 CLI와 동일한 에이전트 루프 및 확인 로직을 공유합니다. run_agent_turn리팩토링: 기존의 동기 함수를 재개 가능한 제너레이터로 변경하여, 대화 흐름 중에 일시 중지 및 재개를 지원합니다. 이 변경은 CLI와 웹 UI 모두에서 동일한 에이전트 로직을 공유할 수 있게 해줍니다.
총 532 라인이 추가되고 42 라인이 삭제되었습니다.
배경 및 목적
이전까지 local-agent는 주로 터미널 기반의 상호작용에 초점을 맞추고 있었습니다. 하지만 사용자의 편의성을 높이고 더 다양한 환경에서 에이전트를 활용하기 위해서는 다음과 같은 기능들이 필요했습니다.
- 파일 접근: 에이전트가 정보를 읽거나 작성하기 위해 외부 파일 시스템에 접근할 수 있어야 합니다. 이는 코드 생성, 설정 파일 수정, 데이터 분석 등 다양한 작업에 필수적입니다.
- 대화 기록 유지: 장시간의 대화나 중요한 정보를 잃지 않고 세션을 이어갈 수 있는 기능이 필요했습니다. 사용자가 언제든지 대화를 중단하고 다시 시작할 수 있어야 합니다.
- 사용자 인터페이스 다양화: 터미널뿐만 아니라 웹 브라우저를 통해서도 에이전트와 쉽게 상호작용할 수 있는 방법을 제공하여 접근성을 높이고자 했습니다.
- 코드 중복 제거 및 유연성 증대: 동일한 에이전트 로직이 터미널과 웹 UI에서 각각 구현되는 것은 비효율적이며 유지보수를 어렵게 만듭니다. 하나의 핵심 로직을 여러 환경에서 재사용할 수 있도록 개선이 필요했습니다.
이러한 문제들을 해결하기 위해 이번 커밋에서는 파일 시스템 도구, 세션 관리, 웹 UI 개발 및 에이전트 핵심 로직의 리팩토링을 진행했습니다.
구현 내용
이번 커밋의 핵심 변경 사항은 다음과 같습니다.
1. 파일 시스템 도구 추가 (local_agent/tools/files.py)
read_file 및 write_file 함수를 추가하여 에이전트가 파일 시스템과 상호작용할 수 있도록 했습니다.
read_file(path: str, max_chars: int = 20000): 지정된 경로의 텍스트 파일을 읽어옵니다.max_chars인자를 통해 최대 읽어올 문자 수를 제한할 수 있으며, 기본값은 20000자입니다. 파일 내용을 반환하며, 잘렸을 경우truncated플래그를True로 설정합니다.write_file(path: str, content: str, mode: str = "overwrite"): 지정된 경로에 텍스트 내용을 씁니다.mode인자는"overwrite"(기본값) 또는"append"를 지원합니다. 경로가 존재하지 않으면 부모 디렉토리를 생성합니다.
# local_agent/tools/files.py (일부 발췌)
from pathlib import Path
# ... (tool 데코레이터 및 import)
@tool(...)
def read_file(path: str, max_chars: int = 20000) -> dict:
content = Path(path).read_text(encoding="utf-8", errors="replace")
return {"content": content[:max_chars], "truncated": len(content) > max_chars}
@tool(...)
def write_file(path: str, content: str, mode: str = "overwrite") -> dict:
p = Path(path)
p.parent.mkdir(parents=True, exist_ok=True)
if mode == "append":
with p.open("a", encoding="utf-8") as f:
f.write(content)
else:
p.write_text(content, encoding="utf-8")
return {"path": str(p), "bytes_written": len(content.encode("utf-8"))}
2. 세션 지속성 구현 (local_agent/session_store.py 및 local_agent/cli.py 연동)
local_agent/session_store.py 파일에 세션 데이터를 JSON으로 저장하고 불러오는 함수들이 추가되었습니다. local_agent/cli.py에서는 이 기능을 활용하여 --session 및 --continue 플래그를 통해 이전 대화 기록을 로드하고, 현재 대화 기록을 세션 파일에 저장합니다.
load(session_path): 지정된 경로에서 세션 데이터를 불러옵니다.save(history_list, session_path): 현재 대화 기록을 JSON 형식으로 저장합니다.named_session_path(name): 이름으로 세션 파일 경로를 생성합니다.latest_session_path(): 가장 최근에 사용된 세션 파일 경로를 반환합니다.new_session_path(): 새로운 세션 파일 경로를 생성합니다.
CLI 실행 시, --session 또는 --continue 플래그가 제공되면 해당 세션 파일을 로드하고, 대화가 종료되면 finally 블록에서 현재 세션 상태를 저장합니다.
3. 웹 채팅 UI 추가 (local_agent/web.py)
Flask를 사용하여 간단한 웹 채팅 인터페이스를 구현했습니다.
PAGE변수에 HTML, CSS, JavaScript가 포함된 단일 페이지 애플리케이션 코드가 정의되어 있습니다./경로로 접근하면 채팅 UI가 표시됩니다./api/history엔드포인트는 이전 대화 기록을 반환합니다./api/chat엔드포인트는 사용자의 메시지를 받아 에이전트에게 전달하고 응답을 처리합니다./api/confirm엔드포인트는 도구 실행 확인에 대한 사용자 결정을 처리합니다.
CLI의 run_web 함수를 통해 --web 및 --port 플래그로 웹 UI를 실행할 수 있습니다.
# local_agent/web.py (일부 발췌)
from flask import Flask, jsonify, request
# ... (import)
PAGE = """<!doctype html>
<html lang="ko">
<head>
<meta charset="utf-8">
<title>local-agent</title>
<style>
body { font-family: system-ui, sans-serif; max-width: 720px; margin: 2rem auto; background: #0b0f14; color: #e6edf3; }
/* ... (CSS 스타일) ... */
</style>
</head>
<body>
<h2>local-agent</h2>
<div id="log"></div>
<div id="inputRow">
<input id="userInput" placeholder="메시지를 입력하세요" autofocus>
<button id="sendBtn" onclick="send()">전송</button>
</div>
<div id="confirmBox">
<strong>[확인 필요]</strong>
<pre id="confirmDetail"></pre>
<div class="confirmBtns">
<button onclick="confirmDecision('yes')">승인</button>
<button onclick="confirmDecision('no')">거부</button>
<button onclick="confirmDecision('always')">이번 세션 항상 승인</button>
</div>
</div>
<script>
// ... (JavaScript 코드) ...
</script>
</body>
</html>
"""
def create_app(settings: Settings) -> Flask:
app = Flask(__name__)
# ... (세션 설정 및 state 초기화) ...
@app.get("/")
def index():
return PAGE
# ... (API 엔드포인트 구현) ...
return app
def run_web(settings: Settings, port: int = 8765) -> None:
app = create_app(settings)
print(f"local-agent web UI: http://127.0.0.1:{port} (model={settings.model}, auto_approve={settings.auto_approve})")
app.run(host="127.0.0.1", port=port)
4. run_agent_turn 리팩토링 및 drive_turn 추가 (local_agent/agent.py)
가장 중요한 변경 사항 중 하나는 run_agent_turn 함수를 제너레이터로 리팩토링한 것입니다. 이제 이 함수는 대화의 각 단계를 yield하여 제어 흐름을 외부로 넘길 수 있습니다.
run_agent_turn(제너레이터):yield {"type": "message", "content": str}: 에이전트의 텍스트 응답을 전달합니다.yield {"type": "confirm", "tool_name": str, "args": dict}: 도구 호출에 대한 확인이 필요할 때 이 값을yield합니다. 호출자는.send(True|False|"always")를 통해 제너레이터를 재개해야 합니다.StopIteration.value를 통해 최종auto_approve플래그를 반환합니다.
drive_turn(gen, send_value=None):- 제너레이터
gen을 한 단계 진행시키거나,send_value를 전달하여 재개합니다. - 완료될 때까지
yield된 메시지를 모아서 반환합니다. - 확인 필요 시
{"status": "confirm", ...}을, 완료 시{"status": "done", ...}을 반환합니다.
- 제너레이터
이러한 변경으로 CLI와 웹 UI 모두 동일한 run_agent_turn 제너레이터를 사용하여 에이전트의 추론 과정을 제어할 수 있게 되었습니다. 또한, 도구 호출 시 _normalize_tool_call 함수를 통해 Pydantic 객체를 일반 딕셔너리로 변환하여 JSON 직렬화 문제를 방지했습니다.
# local_agent/agent.py (일부 발췌)
def run_agent_turn(history: History, client: OllamaClient, auto_approve: bool, max_tool_hops: int = 8):
"""Runs one user turn as a resumable generator.
Yields `{"type": "message", "content": str}` for assistant text, and
`{"type": "confirm", "tool_name": str, "args": dict}` when a dangerous tool
call needs a decision — the caller must `.send(True|False|"always")` to
resume. This lets both the blocking terminal CLI and the request/response
web UI drive the exact same loop without either one being baked in.
Returns (via StopIteration.value) the final auto_approve flag.
"""
# ... (기존 로직에서 yield로 변경) ...
def drive_turn(gen, send_value=None):
"""Advances `gen` until it needs a confirmation decision or finishes.
Returns either {"status": "confirm", "pending": {...}, "messages": [...]}
or {"status": "done", "auto_approve": bool, "messages": [...]}.
"""
messages = []
try:
item = gen.send(send_value)
while True:
if item["type"] == "message":
messages.append(item["content"])
item = gen.send(None)
else: # "confirm"
return {"status": "confirm", "pending": item, "messages": messages}
except StopIteration as stop:
return {"status": "done", "auto_approve": stop.value, "messages": messages}
5. 기타 변경 사항
local_agent/cli.py:--session,--continue,--web,--port옵션이 추가되었고, 세션 관리 로직이 통합되었습니다.tests/test_agent_loop.py: 리팩토링된run_agent_turn및drive_turn함수를 테스트하는 새로운 테스트 케이스들이 추가되었습니다.
기술적 의사결정
1. run_agent_turn을 제너레이터로 변경한 이유
선택: run_agent_turn 함수를 동기 함수에서 yield를 사용하는 제너레이터로 변경했습니다.
이유:
- 단일 로직 재사용: 에이전트의 핵심적인 대화 처리 로직이 터미널 CLI와 웹 UI에서 중복 구현되는 것을 방지하고 싶었습니다. 제너레이터를 사용하면, 대화의 중간 단계에서 제어 흐름을 외부로 넘기고, 외부에서 사용자 입력을 받아 다시 제너레이터를 재개하는 패턴을 구현할 수 있습니다. 이는 CLI에서는
input()함수를 통해, 웹 UI에서는 API 호출을 통해 이루어집니다. - 유연성 및 재개 가능성: 이전에는 에이전트의 한 턴(turn)이 완료될 때까지 블로킹되었지만, 제너레이터는 중간에 멈추고 다시 시작할 수 있는 능력이 있습니다. 이는 특히 도구 호출 시 사용자 확인이 필요한 경우에 유용합니다. 사용자의 결정이 있을 때까지 에이전트의 실행을 일시 중지했다가, 결정을 받으면 다시 이어서 실행할 수 있습니다.
- 명확한 상태 관리: 제너레이터의 상태는 내부적으로 관리되므로, 외부에서는 단순히
send()메소드를 호출하여 제어 흐름을 이어갈 수 있습니다. 이는 복잡한 상태 관리 코드를 줄여줍니다.
대안:
- 콜백 함수 사용:
run_agent_turn함수가 도구 호출 시 콜백 함수를 등록하도록 하는 방식도 고려할 수 있었습니다. - 이벤트 기반 아키텍처: 좀 더 복잡한 이벤트 버스를 사용하여 상태 변화를 관리하는 방식도 가능했습니다.
비교 및 장단점:
- 제너레이터:
- 장점: 코드 구조가 명확하고, Python의 내장 기능을 활용하여 간결하게 구현할 수 있습니다. CLI와 웹 UI 모두에서 동일한 로직을 공유하기에 최적입니다.
- 단점: 제너레이터의 동작 방식을 이해하는 데 약간의 학습이 필요할 수 있습니다. 디버깅 시 스택 추적이 동기 함수와 다르게 보일 수 있습니다.
- 콜백 함수:
- 장점: 특정 시점에 특정 함수를 실행한다는 점이 직관적일 수 있습니다.
- 단점: 여러 단계의 콜백이 중첩되면 '콜백 지옥(callback hell)'에 빠지기 쉬우며, 복잡한 상태 관리가 필요해집니다.
run_agent_turn과 같이 순차적인 로직에는 제너레이터가 더 적합하다고 판단했습니다.
- 이벤트 기반 아키텍처:
- 장점: 매우 복잡하고 분산된 시스템에서 유용할 수 있습니다.
- 단점: 이 프로젝트의 규모에는 과도한 복잡성을 초래할 수 있으며, 구현 및 유지보수 비용이 높습니다.
결론적으로, run_agent_turn을 제너레이터로 변경하는 것이 현재 프로젝트의 요구사항(단일 로직 재사용, 유연한 제어 흐름)에 가장 잘 부합하는 선택이라고 판단했습니다.
2. 파일 시스템 접근 도구 설계
선택: read_file과 write_file을 일반 Python 함수로 구현하고, @tool 데코레이터를 사용하여 에이전트가 호출할 수 있도록 등록했습니다. tool_defs 및 REGISTRY를 통해 도구 명세와 실제 구현을 연결했습니다.
이유:
- 표준적인 도구 통합 방식:
local-agent프레임워크에서 도구를 등록하고 사용하는 기존 방식을 따르는 것이 일관성을 유지하는 데 좋습니다. - 명확한 역할 분담:
read_file과write_file함수는 파일 시스템 접근이라는 명확한 역할만 수행하며, 에이전트의 복잡한 추론 로직과는 분리됩니다. - 안전성 고려:
read_file에는max_chars옵션을 두어 과도한 데이터 로드를 방지하고,write_file에는mode옵션을 두어 의도치 않은 데이터 덮어쓰기를 제어할 수 있게 했습니다.write_file함수는requires_confirmation=False로 설정하여, 도구 자체는 실행에 대한 별도의 확인이 필요하지 않음을 명시했습니다. (에이전트의 전반적인auto_approve설정에 따라 결정됩니다.)
대안:
@tool데코레이터에 직접 파일 경로를 인자로 전달:read_file이나write_file함수 자체에 파일 경로를 직접 인자로 넘기는 대신,@tool데코레이터 자체에서 파일 경로를 처리하도록 구현할 수도 있었습니다.
비교 및 장단점:
- 현재 방식:
- 장점: 도구의 API(함수 시그니처)와 에이전트가 사용하는 도구 명세(
tool_defs)가 일치하여 이해하기 쉽습니다. 파일 접근 로직이 별도의 함수로 캡슐화되어 있어 테스트 및 유지보수가 용이합니다. - 단점: 파일 경로와 같은 인자 처리를 함수 내부에서 해야 합니다.
- 장점: 도구의 API(함수 시그니처)와 에이전트가 사용하는 도구 명세(
- 대안 방식:
- 장점: 에이전트가 직접적으로 파일 경로를 인지하지 않아도 될 수 있습니다.
- 단점: 도구 데코레이터가 파일 시스템 접근이라는 부가적인 역할을 담당하게 되어, 도구의 명확성이 떨어질 수 있습니다. 또한, 파일 경로 처리 로직이 여러 곳에 분산될 가능성이 있습니다.
현재 방식이 도구의 역할과 구현을 명확하게 분리하고, local-agent의 기존 도구 통합 메커니즘과도 잘 맞기 때문에 이 방식을 선택했습니다.
배운 점 및 개선점
배운 점
- 제너레이터의 강력함: Python 제너레이터가 비동기 프로그래밍이나 복잡한 제어 흐름을 구현하는 데 얼마나 강력하고 유용한 도구인지 다시 한번 깨달았습니다. 코드 중복을 제거하고 유연성을 높이는 데 핵심적인 역할을 했습니다.
- CLI와 웹 UI 로직 통합의 중요성: 단일 로직을 여러 인터페이스에서 공유할 수 있도록 설계하는 것이 장기적인 유지보수성과 개발 효율성에 얼마나 큰 영향을 미치는지를 경험했습니다.
- 세션 관리의 필요성: 사용자 경험 측면에서 대화 기록을 유지하는 것이 얼마나 중요한지, 그리고 이를 구현하는 기본적인 방법(JSON 직렬화)을 익혔습니다.
개선점 및 다음 단계 계획
- 보안 강화: 현재
read_file과write_file은 임의의 경로에 접근할 수 있습니다. 이는 악의적인 사용자가 시스템 파일에 접근하거나 중요한 파일을 덮어쓰는 등의 보안 위험을 초래할 수 있습니다. 향후에는 파일 접근 경로를 제한하거나, 특정 디렉토리 내에서만 작동하도록 하는 등의 추가적인 보안 조치가 필요합니다. - 웹 UI 기능 확장: 현재 웹 UI는 매우 기본적인 형태입니다. 사용자에게 더 나은 경험을 제공하기 위해 실시간 업데이트, 로딩 인디케이터, 에러 메시지 표시 개선, 스타일링 개선 등이 필요합니다.
- 도구 오류 처리 개선:
run_agent_turn에서 도구 호출 실패 시 나오는 메시지가 좀 더 사용자 친화적이고 구체적이었으면 좋겠습니다. 예를 들어, 어떤 종류의 오류인지, 어떻게 해결할 수 있는지에 대한 힌트를 제공할 수 있습니다. - 세션 데이터 관리: 현재는 단순히 JSON 파일을 저장하는 방식이지만, 대화량이 많아질 경우 성능 문제가 발생할 수 있습니다. 향후에는 데이터베이스를 활용하거나, 압축 등의 방법을 사용하여 세션 데이터를 효율적으로 관리하는 방안을 고려할 수 있습니다.
- 테스트 커버리지 확대: 새로운 기능들이 추가된 만큼, 각 기능별로 더욱 꼼꼼한 테스트 케이스를 작성하여 안정성을 높여야 합니다. 특히 파일 시스템 접근이나 웹 API 호출에 대한 테스트를 강화할 필요가 있습니다.
이번 커밋은 local-agent를 더욱 강력하고 유연한 도구로 만드는 중요한 발걸음이었습니다. 앞으로 이러한 기능들을 바탕으로 더욱 발전시켜 나갈 계획입니다.
참고 자료
- Python Generator Documentation: https://docs.python.org/3/glossary.html#term-generator
- Flask Documentation: https://flask.palletsprojects.com/