Thanks to visit codestin.com
Credit goes to github.com

Skip to content

genonai/mocking-flowise

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

39 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Mocking Flowise

개요

'Mocking Flowise'는 GenOS에 탑재된 flowise로는 어렵거나 비효율적인 기능에 대해서 코드로 직접 구현하고, Docker 및 FastAPI를 이용하여 flowise와 유사한 역할을 하는 API를 제공한 뒤 GenOS의 워크플로우와 연결하는 예제 프로젝트입니다. 기본적인 LLM 스트리밍 호출(SSE)과 툴 호출 기능을 포함합니다.


기능 요약

  • LLM 스트리밍 응답(SSE) 지원: token, reasoning_token, result, error 이벤트 전송
  • 툴 호출 지원: 예시로 web_search(searchapi.io 기반) 포함
  • 툴 실행 내용을 GenOS 채팅 UI에 노출하기 위한 전용 이벤트(agentFlowExecutedData) 전송
  • Docker/Compose로 손쉽게 실행 가능 (API + Redis)
  • Redis 기반 멀티세션(chatId) 지원: chatId로 세션 저장/복구, 스트림 시작 시 session 이벤트로 chatId 알림
  • 로깅 지원: LOG_LEVEL, LOG_FORMAT=json|console 환경변수로 제어 (app/logger.py)

디렉터리 구조

mock_workflow/
  ├─ app/
  │  ├─ __init__.py
  │  ├─ main.py                 # FastAPI 엔트리 및 라우터 포함
  │  ├─ api/
  │  │  ├─ __init__.py
  │  │  ├─ chat.py              # /chat/stream (SSE)
  │  │  └─ health.py            # /health
  │  ├─ prompts/
  │  │  └─ system.txt           # 시스템 프롬프트
  │  ├─ stores/
  │  │  ├─ __init__.py
  │  │  └─ session_store.py     # Redis 기반 세션 저장/복구
  │  ├─ tools/
  │  │  ├─ __init__.py          # TOOL_MAP, VISIBLE_TOOL_MAP 등록
  │  │  └─ web_search.py        # 웹 검색 툴 및 공개 포맷 정의
  │  ├─ utils.py                # OpenRouter 클라이언트, 스트림 유틸 등
  │  ├─ logger.py               # 로거 설정/유틸
  │  └─ .env                    # 주요한 환경변수 관리
  ├─ tests/
  │  └─ test_chat.py            # SSE 수신 예제 스크립트
  ├─ docker-compose.yml         # API(5555) + Redis 구성
  ├─ Dockerfile
  ├─ requirements.txt
  └─ README.md

요구 사항

  • Python 3.11+
  • OpenRouter API Key (OPENROUTER_API_KEY)
  • searchapi.io API Key (SEARCHAPI_KEY) — 웹 검색 툴 사용 시 필요
  • Redis 7.x (docker compose 사용 시 자동 구성, 로컬 실행 시 REDIS_URL로 연결)

환경 변수 (.env 권장)

# .env 예시
OPENROUTER_API_KEY=<your_openrouter_api_key>
DEFAULT_MODEL=openai/gpt-4o-mini
SEARCHAPI_KEY=<your_searchapi_key>
# 선택: 로컬 Redis를 쓸 때
# REDIS_URL=redis://localhost:6379/0
# 선택: 로컬 실행 포트 (기본 6666)
# PORT=6666

# MCP 연동용
GENOS_ID=<genos_admin_id>
GENOS_PW=<genos_admin_password>
# 쉼표로 여러 서버 ID 등록 가능. 예: 12,34,56
MCP_SERVER_ID=<_comma_separated_server_ids_>

# 로깅 설정
# LOG_LEVEL=INFO            # DEBUG, INFO, WARNING, ERROR, CRITICAL
# LOG_FORMAT=console        # console 또는 json

실행 방법

1) 로컬 실행 (개발용)

pip install -r requirements.txt
export $(grep -v '^#' .env | xargs)  # 또는 수동으로 환경 변수 설정
# 방법 A: 모듈 실행 (reload 포함)
python -m app.main
# 방법 B: uvicorn 직접 실행
uvicorn app.main:app --host 0.0.0.0 --port ${PORT:-6666} --reload
  • 헬스체크: curl -s http://localhost:${PORT:-6666}/health | cat

2) Docker Compose (권장)

docker compose up -d --build
# 상태 확인
docker compose ps
# 헬스체크
curl -s http://localhost:5555/health | cat
  • 컨테이너 기본 포트는 5555입니다. (Dockerfile EXPOSE 5555 및 Compose 5555:5555)
  • 코드/의존성/Dockerfile 변경 후에는 --build를 붙여 최신 상태로 반영하세요.

엔드포인트 요약

  • GET /health — 단순 헬스체크
  • POST /chat/stream — SSE 스트리밍 응답
    • 헤더 예시: Accept: text/event-stream
    • 요청 바디 예시: { "question": "...", "chatId": "옵션" }

SSE 이벤트 규칙 (GenOS 채팅 앱 표현 규칙)

  • 본 프로젝트는 각 SSE 청크를 다음과 같은 JSON 한 줄로 보냅니다. 실제 SSE 라인은 아래 형식입니다.
    • data: {"event": string, "data": any} + 빈 줄
    • 주기적 하트비트: : keep-alive (클라이언트는 무시 가능)
  • 이벤트 타입과 data 규칙
    • token: 모델의 일반 답변 토큰. data는 텍스트 조각(string)
    • reasoning_token: 모델의 추론(리저닝) 토큰. data는 텍스트 조각(string)
    • agentFlowExecutedData: 툴 실행 결과를 채팅 UI(에이전트 플로우 노드)에 노출하기 위한 이벤트
      • payload 예시:
        {
          "event": "agentFlowExecutedData",
          "data": {
            "nodeLabel": "Visible Query Generator",
            "data": {
              "output": {
                "content": "{\"visible_web_search_query\":[\"python 3.12 change\"]}"
              }
            }
          }
        }
      • 중요: 이벤트가 agentFlowExecutedData인 경우에는 data.data.output.content에 GenOS 프론트와 약속된 규칙에 따른 내용이 들어있어야 GenOS 채팅 어플리케이션의 추론과정에 표현됩니다.
    • error: 오류 메시지(string)
    • result: 최종 완료 신호. datanull

툴 사용 및 노출 규칙

  • 툴 스키마: OpenAI 함수 호출 포맷을 따르며, 예시로 web_search가 등록되어 있습니다.
    • 등록 위치: app/tools/web_search.pyWEB_SEARCH 스키마, 실행 함수 web_search()
    • 서버 등록: app/tools/__init__.pyget_tool_map() (실행), get_visible_tool_map() (UI 노출)
    • 서버 사용: app/api/chat.py에서 states.tools = await get_tools_for_llm()로 사용
  • 사람이 볼 정보만 노출하기
    • 각 툴에 대응하는 Visible*Model을 정의하여 format(args)에서 공개 가능한 정보만 추립니다.
    • 서버는 format(args) 결과를 json.dumps(...)로 문자열화하여 agentFlowExecutedData.data.data.output.content에 넣어 보냅니다.
    • nodeLabel은 UI 노드명으로 사용됩니다.
  • 새 툴 추가 가이드(요약)
    1. app/tools/my_tool.py에 실행 함수, 스키마 WEB_MY_TOOL, 공개 모델 VisibleMyToolModel 구현
    2. app/tools/__init__.pyget_tool_map(), get_visible_tool_map()에 등록
    3. app/api/chat.pyget_tools_for_llm()에서 WEB_MY_TOOL 추가

MCP 툴 연동 규칙

  • 개요: GenOS MCP 서버에 등록된 툴을 불러와 OpenAI 규격의 툴 스키마로 변환합니다.
    • 구현 위치: app/mcp/mcp_tools.py
    • 공개 엔트리: MCP_TOOLS(LLM용 스키마 리스트), get_mcp_tool(name)(실행 함수 팩토리), get_mcp_tool_map()
  • 환경 변수
    • GENOS_ID, GENOS_PW: MCP API 토큰 발급용 자격 증명
    • MCP_SERVER_ID: 조회할 MCP 서버 ID(들), 쉼표구분 예: 12,34,56
  • 스키마 변환
    • MCP 툴의 input_schema를 OpenAI tools 포맷의 parameters로 매핑합니다.
    • 결과 형태 예시:
      {
        "type": "function",
        "function": {
          "name": "open_url",
          "description": "...",
          "parameters": {"type":"object", "properties": {"opens": {"type":"array"}}, "required": ["opens"]}
        }
      }
  • 실행 시그니처
    • MCP 툴 실행 함수는 async def(states, **tool_input) 서명을 따릅니다.
    • 내부적으로 json={"tool_name": name, "input_schema": tool_input} 페이로드로 MCP 서버에 호출합니다.
  • 서버 등록/사용
    • app/mcp/__init__.pyget_mcp_tool_map()으로 {tool_name: callable} 맵을 얻습니다.
    • app/tools/__init__.py
      • get_tool_map()에서 웹툴 + MCP 맵을 병합해 반환
      • get_tools_for_llm()에서 WEB_SEARCH + MCP_TOOLS를 반환
    • app/api/chat.py에서 요청 처리 중 states.tools = await get_tools_for_llm()로 주입합니다.
  • 주의사항
    • 네트워크 오류 또는 인증 오류에 대비해 .envGENOS_ID, GENOS_PW, MCP_SERVER_ID 설정을 확인하세요.
    • MCP 서버의 도구 스키마가 OpenAI 규격과 호환되지 않는 필드를 포함할 수 있으므로, 변환 로직이 parameters를 안전하게 구성하도록 되어 있습니다.

Workflow Python Step (예시)

GenOS 워크플로우의 Python Step을 생성한 후 아래 코드에서 endpoint를 여러분의 배포 주소로 바꾸어 사용하시면 됩니다.

from main_socketio import sio_server
import aiohttp, json

async def run(data: dict) -> dict:
    sid = data.get('socketIOClientId')  # 프론트 소켓 ID
    endpoint = "<your-end-point>"
    result = {}
    text_acc = ""

    BIG = 2**30
    async with aiohttp.ClientSession(
        timeout=aiohttp.ClientTimeout(total=None),
        read_bufsize=BIG, max_line_size=BIG
    ) as session:
        async with session.post(endpoint, json=data, headers={"x-request-from":"internal"}) as resp:
            prev_event = None
            reasoning = ""
            async for line in resp.content:
                if not line:
                    continue
                decoded = line.decode("utf-8").strip()
                if decoded.startswith("data:"):
                    decoded = decoded.removeprefix("data:")
                try:
                    payload = json.loads(decoded)
                except json.JSONDecodeError:
                    continue

                event = payload.get("event")
                ev_data = payload.get("data")

                if event == "reasoning_token":
                    reasoning += ev_data
                elif reasoning:
                    result.setdefault('agentFlowExecutedData', []).append({
                        "nodeLabel": "Visible Reasoner",
                        "data": {"output": {"content": json.dumps({"visible_rationale": reasoning}, ensure_ascii=False)}}
                    })
                    reasoning = ""
                    if sid:
                        await sio_server.emit("agentFlowExecutedData", result['agentFlowExecutedData'], room=sid)
                
                if event == "token":
                    if isinstance(ev_data, str):
                        text_acc += ev_data
                    if sid:
                        await sio_server.emit("token", ev_data, room=sid)
                elif event == "agentFlowExecutedData":
                    result.setdefault('agentFlowExecutedData', []).append(ev_data)
                    if sid:
                        await sio_server.emit("agentFlowExecutedData", result['agentFlowExecutedData'], room=sid)
                elif event == "error":
                    result["message"] = ev_data
                    result["success"] = False
                    result['statusCode'] = 500
                elif event == "result":
                    # 토큰만 왔다가 최종 result가 없으면 text로 보강
                    if text_acc and "text" not in result:
                        result["text"] = text_acc
                    if sid:
                        await sio_server.emit("result", result, room=sid)
                
                prev_event = event

    # 워크플로우 다음 스텝으로 넘길 데이터 머지 후 반환
    data.update(result)
    return data

주의사항

  • 멀티턴/멀티세션 지원: POST /chat/streamchatId를 주면 해당 세션을 이어서 대화합니다. chatId가 없으면 서버가 생성하며 스트림 초기에 session 이벤트로 전달합니다.
  • 세션 저장: Redis에 chat:{chatId} 키로 저장되며 기본 TTL은 7일입니다.
  • 동시성: 동일 chatId로 동시 요청 시 단순히 마지막 저장이 우선합니다(필요 시 분산 락/트랜잭션으로 강화 가능).

About

파이썬 코드로 구현한 플로우를 GenOS의 워크플로우와 연결 및 GenOS 채팅 어플리케이션에 연동하는 예제 프로젝트입니다.

Resources

Stars

4 stars

Watchers

0 watching

Forks

Contributors