Skip to content

Commit

Permalink
Feat/api refactor (#263)
Browse files Browse the repository at this point in the history
* feat: api refactor

* feat: separate agent mgmt apis, add a front-end ui service layer on
next.js

* feat: added list method & enhance process mgmt

* feat: finalize

* feat: add api docs

* feat: rename bot_uid and user_uid

* fix: fix new line after param table
  • Loading branch information
plutoless authored Sep 2, 2024
1 parent c2f9d26 commit 56d3b8e
Show file tree
Hide file tree
Showing 11 changed files with 292 additions and 22 deletions.
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { LanguageMap } from "./constant";
import { LanguageMap } from "@/common/constant";

export const voiceNameMap: LanguageMap = {
"zh-CN": {
Expand Down Expand Up @@ -43,6 +43,8 @@ export const voiceNameMap: LanguageMap = {
},
};

// Get the graph properties based on the graph name, language, and voice type
// This is the place where you can customize the properties for different graphs to override default property.json
export const getGraphProperties = (graphName: string, language: string, voiceType: string) => {
let localizationOptions = {
"greeting": "ASTRA agent connected. How can i help you today?",
Expand Down
50 changes: 50 additions & 0 deletions playground/src/app/api/agents/start/route.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { REQUEST_URL } from '@/common/constant';
import { NextRequest, NextResponse } from 'next/server';
import { getGraphProperties } from './graph';

/**
* Handles the POST request to start an agent.
*
* @param request - The NextRequest object representing the incoming request.
* @returns A NextResponse object representing the response to be sent back to the client.
*/
export async function POST(request: NextRequest) {
try {
const body = await request.json();
const {
request_id,
channel_name,
user_uid,
graph_name,
language,
voice_type,
} = body;

// Send a POST request to start the agent
const response = await fetch(`${REQUEST_URL}/start`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
request_id,
channel_name,
user_uid,
graph_name,
// Get the graph properties based on the graph name, language, and voice type
properties: getGraphProperties(graph_name, language, voice_type),
}),
});

const responseData = await response.json();

return NextResponse.json(responseData, { status: response.status });
} catch (error) {
if (error instanceof Response) {
const errorData = await error.json();
return NextResponse.json(errorData, { status: error.status });
} else {
return NextResponse.json({ code: "1", data: null, msg: "Internal Server Error" }, { status: 500 });
}
}
}
42 changes: 42 additions & 0 deletions playground/src/app/api/agents/stop/route.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { REQUEST_URL } from '@/common/constant';
import { NextRequest, NextResponse } from 'next/server';

/**
* Handles the POST request to stop an agent.
*
* @param request - The NextRequest object representing the incoming request.
* @returns A NextResponse object representing the response to be sent back to the client.
*/
export async function POST(request: NextRequest) {
try {
const body = await request.json();
const {
channel_name,
request_id,
} = body;

// Send a POST request to stop the agent
const response = await fetch(`${REQUEST_URL}/stop`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
request_id,
channel_name
}),
});

// Get the response data
const responseData = await response.json();

return NextResponse.json(responseData, { status: response.status });
} catch (error) {
if (error instanceof Response) {
const errorData = await error.json();
return NextResponse.json(errorData, { status: error.status });
} else {
return NextResponse.json({ code: "1", data: null, msg: "Internal Server Error" }, { status: 500 });
}
}
}
1 change: 0 additions & 1 deletion playground/src/common/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,3 @@ export * from "./utils"
export * from "./storage"
export * from "./request"
export * from "./mock"
export * from "./graph"
20 changes: 12 additions & 8 deletions playground/src/common/request.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import { AnyObject } from "antd/es/_util/type"
import { REQUEST_URL } from "./constant"
import { genUUID } from "./utils"
import { Language } from "@/types"

interface StartRequestConfig {
channel: string
userId: number,
graphName: string
properties: AnyObject
graphName: string,
language: Language,
voiceType: "male" | "female"
}

interface GenAgoraDataConfig {
Expand Down Expand Up @@ -34,15 +36,16 @@ export const apiGenAgoraData = async (config: GenAgoraDataConfig) => {
}

export const apiStartService = async (config: StartRequestConfig): Promise<any> => {
const url = `${REQUEST_URL}/start`
const { channel, userId, graphName, properties } = config
// look at app/api/agents/start/route.tsx for the server-side implementation
const url = `/api/agents/start`
const { channel, userId, graphName, language, voiceType } = config
const data = {
request_id: genUUID(),
channel_name: channel,
openai_proxy_url: "",
remote_stream_id: userId,
user_uid: userId,
graph_name: graphName,
properties,
language,
voice_type: voiceType
}
let resp: any = await fetch(url, {
method: "POST",
Expand All @@ -56,7 +59,8 @@ export const apiStartService = async (config: StartRequestConfig): Promise<any>
}

export const apiStopService = async (channel: string) => {
const url = `${REQUEST_URL}/stop`
// look at app/api/agents/stop/route.tsx for the server-side implementation
const url = `/api/agents/stop`
const data = {
request_id: genUUID(),
channel_name: channel
Expand Down
6 changes: 3 additions & 3 deletions playground/src/platform/mobile/description/index.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { setAgentConnected } from "@/store/reducers/global"
import {
DESCRIPTION, useAppDispatch, useAppSelector, apiPing, genUUID,
apiStartService, apiStopService,
getGraphProperties
apiStartService, apiStopService
} from "@/common"
import { message } from "antd"
import { useEffect, useState } from "react"
Expand Down Expand Up @@ -50,7 +49,8 @@ const Description = () => {
channel,
userId,
graphName,
properties: getGraphProperties(graphName, language, voiceType)
language,
voiceType
})
const { code, msg } = res || {}
if (code != 0) {
Expand Down
6 changes: 3 additions & 3 deletions playground/src/platform/pc/description/index.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { setAgentConnected } from "@/store/reducers/global"
import {
DESCRIPTION, useAppDispatch, useAppSelector, apiPing, genUUID,
apiStartService, apiStopService,
getGraphProperties
apiStartService, apiStopService
} from "@/common"
import { Select, Button, message, Upload } from "antd"
import { useEffect, useState, MouseEventHandler } from "react"
Expand Down Expand Up @@ -50,7 +49,8 @@ const Description = () => {
channel,
userId,
graphName,
properties: getGraphProperties(graphName, language, voiceType)
language,
voiceType
})
const { code, msg } = res || {}
if (code != 0) {
Expand Down
76 changes: 76 additions & 0 deletions server/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
## Request & Response Examples
The server provides a simple layer for managing agent processes.

### API Resources

- [POST /start](#get-magazines)
- [POST /stop](#get-magazinesid)
- [POST /ping](#post-magazinesidarticles)


### POST /start
This api starts an agent with given graph and override properties. The started agent will join into the specified channel, and subscribe to the uid which your browser/device's rtc use to join.

| Param | Description |
| -------- | ------- |
| request_id | any uuid for tracing purpose |
| channel_name | channel name, it needs to be the same with the one your browser/device joins, agent needs to stay with your browser/device in the same channel to communicate |
| user_uid | the uid which your browser/device's rtc use to join, agent needs to know your rtc uid to subscribe your audio |
| bot_uid | optional, the uid bot used to join rtc |
| graph_name | the graph to be used when starting agent, will find in property.json |
| properties | additional properties to override in property.json, the override will not change original property.json, only the one agent used to start |
| timeout | determines how long the agent will remain active without receiving any pings. If the timeout is set to `-1`, the agent will not terminate due to inactivity. By default, the timeout is set to 60 seconds, but this can be adjusted using the `WORKER_QUIT_TIMEOUT_SECONDS` variable in your `.env` file. |

Example:
```bash
curl 'http://localhost:8080/start' \
-H 'Content-Type: application/json' \
--data-raw '{
"request_id": "c1912182-924c-4d15-a8bb-85063343077c",
"channel_name": "test",
"user_uid": 176573,
"graph_name": "camera.va.openai.azure",
"properties": {
"openai_chatgpt": {
"model": "gpt-4o"
}
}
}'
```

### POST /stop
This api stops the agent you started

| Param | Description |
| -------- | ------- |
| request_id | any uuid for tracing purpose |
| channel_name | channel name, the one you used to start the agent |

Example:
```bash
curl 'http://localhost:8080/stop' \
-H 'Content-Type: application/json' \
--data-raw '{
"request_id": "c1912182-924c-4d15-a8bb-85063343077c",
"channel_name": "test"
}'
```


### POST /ping
This api sends a ping to the server to indicate connection is still alive. This is not needed if you specify `timeout:-1` when starting the agent, otherwise the agent will quit if not receiving ping after timeout in seconds.

| Param | Description |
| -------- | ------- |
| request_id | any uuid for tracing purpose |
| channel_name | channel name, the one you used to start the agent |

Example:
```bash
curl 'http://localhost:8080/ping' \
-H 'Content-Type: application/json' \
--data-raw '{
"request_id": "c1912182-924c-4d15-a8bb-85063343077c",
"channel_name": "test"
}'
```
5 changes: 5 additions & 0 deletions server/internal/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ const (
PropertyJsonFile = "./agents/property.json"
// Token expire time
tokenExpirationInSeconds = uint32(86400)

WORKER_TIMEOUT_INFINITY = -1
)

var (
Expand All @@ -31,6 +33,9 @@ var (
"RemoteStreamId": {
{ExtensionName: extensionNameAgoraRTC, Property: "remote_stream_id"},
},
"BotStreamId": {
{ExtensionName: extensionNameAgoraRTC, Property: "uid"},
},
"Token": {
{ExtensionName: extensionNameAgoraRTC, Property: "token"},
},
Expand Down
31 changes: 28 additions & 3 deletions server/internal/http_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,12 @@ type StartReq struct {
RequestId string `json:"request_id,omitempty"`
ChannelName string `json:"channel_name,omitempty"`
GraphName string `json:"graph_name,omitempty"`
RemoteStreamId uint32 `json:"remote_stream_id,omitempty"`
RemoteStreamId uint32 `json:"user_uid,omitempty"`
BotStreamId uint32 `json:"bot_uid,omitempty"`
Token string `json:"token,omitempty"`
WorkerHttpServerPort int32 `json:"worker_http_server_port,omitempty"`
Properties map[string]map[string]interface{} `json:"properties,omitempty"`
QuitTimeoutSeconds int `json:"timeout,omitempty"`
}

type StopReq struct {
Expand Down Expand Up @@ -92,6 +94,22 @@ func (s *HttpServer) handlerHealth(c *gin.Context) {
s.output(c, codeOk, nil)
}

func (s *HttpServer) handlerList(c *gin.Context) {
slog.Info("handlerList start", logTag)
// Create a slice of maps to hold the filtered data
filtered := make([]map[string]interface{}, len(workers.Keys()))
for _, channelName := range workers.Keys() {
worker := workers.Get(channelName).(*Worker)
workerJson := map[string]interface{}{
"channelName": worker.ChannelName,
"createTs": worker.CreateTs,
}
filtered = append(filtered, workerJson)
}
slog.Info("handlerList end", logTag)
s.output(c, codeSuccess, filtered)
}

func (s *HttpServer) handlerPing(c *gin.Context) {
var req PingReq

Expand Down Expand Up @@ -163,7 +181,13 @@ func (s *HttpServer) handlerStart(c *gin.Context) {

worker := newWorker(req.ChannelName, logFile, s.config.Log2Stdout, propertyJsonFile)
worker.HttpServerPort = req.WorkerHttpServerPort
worker.QuitTimeoutSeconds = s.config.WorkerQuitTimeoutSeconds

if req.QuitTimeoutSeconds > 0 {
worker.QuitTimeoutSeconds = req.QuitTimeoutSeconds
} else {
worker.QuitTimeoutSeconds = s.config.WorkerQuitTimeoutSeconds
}

if err := worker.start(&req); err != nil {
slog.Error("handlerStart start worker failed", "err", err, "requestId", req.RequestId, logTag)
s.output(c, codeErrStartWorkerFailed, http.StatusInternalServerError)
Expand Down Expand Up @@ -461,9 +485,10 @@ func (s *HttpServer) Start() {

r.GET("/", s.handlerHealth)
r.GET("/health", s.handlerHealth)
r.POST("/ping", s.handlerPing)
r.GET("/list", s.handlerList)
r.POST("/start", s.handlerStart)
r.POST("/stop", s.handlerStop)
r.POST("/ping", s.handlerPing)
r.POST("/token/generate", s.handlerGenerateToken)
r.GET("/vector/document/preset/list", s.handlerVectorDocumentPresetList)
r.POST("/vector/document/update", s.handlerVectorDocumentUpdate)
Expand Down
Loading

0 comments on commit 56d3b8e

Please sign in to comment.