Skip to content

Latest commit

Β 

History

History
537 lines (412 loc) Β· 21.7 KB

essay-writer.md

File metadata and controls

537 lines (412 loc) Β· 21.7 KB

Essay Writer

Reflection을 μ΄μš©ν•œ Easy Writer의 κ΅¬ν˜„

essay-writer.ipynbμ—μ„œλŠ” Easy Writer을 싀행해보고 λ™μž‘μ„ 확인할 수 μžˆμŠ΅λ‹ˆλ‹€. Easy WriterλŠ” lambda_function.pyμ—μ„œ κ΅¬ν˜„λœ μ½”λ“œλ₯Ό 확인할 수 μžˆμŠ΅λ‹ˆλ‹€. deep learning.ai의 Essay WriterλŠ” LangGrap의 workflowλ₯Ό μ΄μš©ν•˜μ—¬ 주어진 μ£Όμ œμ— μ ν•©ν•œ Essayλ₯Ό μž‘μ„±ν•  수 μžˆλ„λ‘ λ„μ™€μ€λ‹ˆλ‹€. reflection-agent.mdμ™€μ˜ 차이점은 reflection agendμ—μ„œλŠ” μ™ΈλΆ€ 검색없이 reflection을 μ΄μš©ν•΄ LLM으둜 μƒμ„±λœ essayλ₯Ό μ—…λ°μ΄νŠΈ ν•˜λŠ”κ²ƒμ— λΉ„ν•΄ easy writerμ—μ„œλŠ” 인터넷 검색에 ν•„μš”ν•œ keywordλ₯Ό reflection으둜 μ—…λ°μ΄νŠΈν•˜κ³  μžˆμŠ΅λ‹ˆλ‹€. μ„±λŠ₯은 κ²€μƒ‰λœ λ°μ΄ν„°μ˜ 질과 양에 따라 λ‹¬λΌμ§€λ―€λ‘œ μ„±λŠ₯의 λΉ„κ΅λ³΄λ‹€λŠ” workflowλ₯Ό μ΄ν•΄ν•˜λŠ” μš©λ„λ‘œ ν™œμš©ν•©λ‹ˆλ‹€.

Easy writer의 activity diagram은 μ•„λž˜μ™€ κ°™μŠ΅λ‹ˆλ‹€.

image

상세 κ΅¬ν˜„

LangGraphλ₯Ό μœ„ν•œ State ClassλŠ” μ•„λž˜μ™€ κ°™μŠ΅λ‹ˆλ‹€.

class State(TypedDict):
    task: str
    plan: list[str]
    essay: str
    critique: str
    content: List[str]
    revision_number: int
    max_revisions: int

μ—μ„Έμ΄μ˜ 주제λ₯Ό μ„€λͺ…ν•˜λŠ” Plan을 μ•„λž˜μ™€ 같이 μ€€λΉ„ν•©λ‹ˆλ‹€. Plan ν΄λž˜μŠ€λŠ” with_structured_outputλ₯Ό μ΄μš©ν•΄ Plan을 μΆ”μΆœν•  λ•Œμ— μ‚¬μš©ν•©λ‹ˆλ‹€.

class Plan(BaseModel):
    """List of session topics and outline as a json format"""

    steps: List[str] = Field(
        description="different sessions to follow, should be in sorted order without numbers. Eash session has detailed description"
    )

def get_planner():
    system = """You are an expert writer tasked with writing a high level outline of an essay. \
Write such an outline for the user provided topic. Give an outline of the essay along with any relevant notes \
or instructions for the sections. \
Make sure that each session has all the information needed."""
            
    planner_prompt = ChatPromptTemplate.from_messages(
        [
            ("system", system),
            ("placeholder", "{messages}"),
        ]
    )
        
    chat = get_chat()   
        
    planner = planner_prompt | chat
    return planner

def plan(state: State):
    print("###### plan ######")
    print('task: ', state["task"])
        
    task = [HumanMessage(content=state["task"])]

    planner = get_planner()
    response = planner.invoke({"messages": task})
    print('response.content: ', response.content)
        
    chat = get_chat()
    structured_llm = chat.with_structured_output(Plan, include_raw=True)
    info = structured_llm.invoke(response.content)
    print('info: ', info)
        
    if not info['parsed'] == None:
        parsed_info = info['parsed']
        # print('parsed_info: ', parsed_info)        
        print('steps: ', parsed_info.steps)
            
        return {
            "task": state["task"],
            "plan": parsed_info.steps
        }
    else:
        print('parsing_error: ', info['parsing_error'])
            
        return {"plan": []}  

μ΄λ•Œ μ–»μ–΄μ§€λŠ” λ°μ΄ν„°μ˜ ν˜•νƒœλŠ” μ•„λž˜μ™€ κ°™μŠ΅λ‹ˆλ‹€.

{'task': '즐겁게 μ‚¬λŠ” 방법',
 'plan': ['ν–‰λ³΅μ˜ μ •μ˜μ™€ μ€‘μš”μ„±μ— λŒ€ν•œ κ°„λž΅ν•œ μ„€λͺ…κ³Ό 주제 μ œμ‹œ',
  '긍정적인 λ§ˆμŒκ°€μ§μ„ μœ μ§€ν•˜κΈ° μœ„ν•œ 방법듀 - 뢀정적 생각을 κΈμ •μ μœΌλ‘œ μ „ν™˜, κ°μ‚¬ν•˜λŠ” μŠ΅κ΄€, λ‚™κ΄€μ£Όμ˜μ™€ 희망적 μ‚¬κ³ μ˜ μ€‘μš”μ„±',
  'κ· ν˜• 작힌 μƒν™œμ„ μ˜μœ„ν•˜κΈ° μœ„ν•œ 방법듀 - 일과 νœ΄μ‹μ˜ κ· ν˜•, μš΄λ™κ³Ό κ±΄κ°•ν•œ μ‹μŠ΅κ΄€, μ·¨λ―Έμƒν™œκ³Ό μ—¬κ°€ν™œλ™',
  '인간관계λ₯Ό μ†Œμ€‘νžˆ μ—¬κΈ°λŠ” 방법듀 - κ°€μ‘±/친ꡬ/λ™λ£Œμ™€μ˜ 관계 λˆλ…νžˆ ν•˜κΈ°, 타인 배렀와 이해, μ‚¬νšŒμ  μœ λŒ€κ° ν˜•μ„±',
  'μžκΈ°κ³„λ°œκ³Ό μ„±μž₯을 μΆ”κ΅¬ν•˜λŠ” 방법듀 - μƒˆλ‘œμš΄ 것 λ°°μš°κΈ°μ™€ 도전, μž₯κΈ° λͺ©ν‘œ μ„€μ •κ³Ό μ„±μ·¨ λ…Έλ ₯, 영적/정신적 μ„±μž₯의 κ°€μΉ˜',
  'μ£Όμš” λ‚΄μš© μš”μ•½ 및 즐거운 삢을 μœ„ν•œ 톡합적 μ ‘κ·Όμ˜ ν•„μš”μ„± κ°•μ‘°']}

웹검색을 μœ„ν•œ keywordλ₯Ό μƒμ„±ν•˜κΈ° μœ„ν•˜μ—¬ Queries 클래슀λ₯Ό μ •μ˜ν•œ 후에 μ•„λž˜μ™€ 같이 Queryλ₯Ό μΆ”μΆœν•©λ‹ˆλ‹€.

from langchain_core.pydantic_v1 import BaseModel

class Queries(BaseModel):
    queries: List[str]

def research_plan(state: State):
    task = state['task']
    print('task: ', task)
    
    system = """You are a researcher charged with providing information that can \
be used when writing the following essay. Generate a list of search queries that will gather \
any relevant information. Only generate 3 queries max."""
        
    research_prompt = ChatPromptTemplate.from_messages(
        [
            ("system", system),
            ("human", "{task}"),
        ]
    )
        
    chat = get_chat()   
        
    research = research_prompt | chat
    
    response = research.invoke({"task": task})
    print('response.content: ', response.content)
    
    chat = get_chat()
    structured_llm = chat.with_structured_output(Queries, include_raw=True)
    info = structured_llm.invoke(response.content)
    # print('info: ', info)
    
    if not info['parsed'] == None:
        queries = info['parsed']
        print('queries: ', queries.queries)
        
    content = state["content"] if state.get("content") is not None else []
    search = TavilySearchResults(k=2)
    for q in queries.queries:
        response = search.invoke(q)     
        # print('response: ', response)        
        for r in response:
            content.append(r['content'])
    return {        
        "task": state['task'],
        "plan": state['plan'],
        "content": content,
    }

μ΄λ•Œ μ–»μ–΄μ§€λŠ” 쿼리λ₯Ό μœ„ν•œ keywordλŠ” μ•„λž˜μ™€ κ°™μŠ΅λ‹ˆλ‹€. μ›Ήκ²€μƒ‰μ˜ κ²°κ³ΌλŠ” content에 μ €μž₯λ˜μ–΄ generationμ—μ„œ ν™œμš©ν•©λ‹ˆλ‹€.

queries:  ['ν–‰λ³΅ν•œ μ‚Άμ˜ 방법', '긍정적 λ§ˆμΈλ“œ κΈ°λ₯΄κΈ°', 'μΌμƒμ—μ„œ 행볡 μ°ΎκΈ° 팁']

generation()μ—μ„œλŠ” Planκ³Ό μ›Ήκ²€μƒ‰μœΌλ‘œ 얻어진 정보인 contentλ₯Ό μ΄μš©ν•΄ 에세이λ₯Ό μƒμ„±ν•©λ‹ˆλ‹€.

def generation(state: State):    
    content = "\n\n".join(state['content'] or [])
    
    system = """You are an essay assistant tasked with writing excellent 5-paragraph essays.\
Generate the best essay possible for the user's request and the initial outline. \
If the user provides critique, respond with a revised version of your previous attempts. \
Utilize all the information below as needed: 

<content>
{content}
</content>
"""
    
    prompt = ChatPromptTemplate.from_messages(
        [
            ("system", system),
            ("human", "{task}\n\nHere is my plan:\n\n{plan}"),
        ]
    )
        
    chat = get_chat()
    chain = prompt | chat

    response = chain.invoke({
        "content": content,
        "task": state['task'],
        "plan": state['plan']
    })
        
    revision_number = state["revision_number"] if state.get("revision_number") is not None else 1
    return {
        "essay": response, 
        "revision_number": revision_number + 1
    }

μ΄λ•Œ μƒμ„±λœ μ—μ„Έμ΄λŠ” μ•„λž˜μ™€ κ°™μŠ΅λ‹ˆλ‹€.

{'essay': AIMessage(content='ν–‰λ³΅ν•œ 삢을 μ‚΄κΈ° μœ„ν•œ 길작이\n\nλͺ¨λ‘κ°€ ν–‰λ³΅ν•œ 삢을 κΏˆκΎΈμ§€λ§Œ μ‹€μ œλ‘œ 행볡을 λŠλΌλŠ” 것은 쉽지 μ•ŠμŠ΅λ‹ˆλ‹€. 행볡은 λ‹¨μˆœνžˆ 물질적 ν’μš”λ‚˜ μΌμ‹œμ μΈ 기쁨을 λ„˜μ–΄μ„œλŠ” μ‚Άμ˜ 만쑱과 μΆ©λ§Œν•¨μ„ μ˜λ―Έν•©λ‹ˆλ‹€. μ§„μ •ν•œ 행볡을 μœ„ν•΄μ„œλŠ” 우리의 λ§ˆμŒκ°€μ§κ³Ό μƒν™œ 방식에 μ£Όλͺ©ν•΄μ•Ό ν•©λ‹ˆλ‹€.\n\n첫째, 긍정적인 λ§ˆμŒκ°€μ§μ„ κΈ°λ₯΄λŠ” 것이 μ€‘μš”ν•©λ‹ˆλ‹€. 뢀정적인 생각과 감정에 μ‚¬λ‘œμž‘νžˆκΈ°λ³΄λ‹€λŠ” 낙관적이고 희망적인 관점을 κ°€μ Έμ•Ό ν•©λ‹ˆλ‹€. μž‘μ€ μΌμƒμ˜ 기쁨에 κ°μ‚¬ν•˜λŠ” μŠ΅κ΄€μ„ 듀이고, μ–΄λ €μš΄ μƒν™©μ—μ„œλ„ 긍정적인 면을 μ°Ύμ•„λ³΄μ„Έμš”. \n\nλ‘˜μ§Έ, κ· ν˜• 작힌 μƒν™œ 리듬을 μœ μ§€ν•˜λŠ” 것이 ν–‰λ³΅μ˜ μ—΄μ‡ μž…λ‹ˆλ‹€. 일과 νœ΄μ‹μ˜ μ μ ˆν•œ μ‘°ν™”λ₯Ό 이루고, μš΄λ™κ³Ό κ±΄κ°•ν•œ μ‹μŠ΅κ΄€μœΌλ‘œ λͺΈκ³Ό 마음의 건강을 λŒλ³΄μ„Έμš”. λ˜ν•œ μ·¨λ―Έμƒν™œμ΄λ‚˜ μ—¬κ°€ν™œλ™μ„ 톡해 μž¬λ―Έμ™€ ν™œλ ₯을 얻을 수 μžˆμŠ΅λ‹ˆλ‹€.\n\nμ…‹μ§Έ, 인간관계λ₯Ό μ†Œμ€‘νžˆ μ—¬κΈ°λŠ” μžμ„Έκ°€ ν•„μš”ν•©λ‹ˆλ‹€. κ°€μ‘±, 친ꡬ, λ™λ£Œλ“€κ³Ό μ •μ„œμ  μœ λŒ€λ₯Ό λˆλ…νžˆ ν•˜κ³  μ„œλ‘œλ₯Ό μ΄ν•΄ν•˜κ³  λ°°λ €ν•˜λŠ” λ§ˆμŒμ„ κ°€μ Έμ•Ό ν•©λ‹ˆλ‹€. νƒ€μΈκ³Όμ˜ μœ λŒ€κ°μ€ 행볡감을 λ†’μ΄λŠ” μ›μ²œμ΄ λ©λ‹ˆλ‹€.\n\nλ„·μ§Έ, μžκΈ°κ³„λ°œκ³Ό μ„±μž₯의 기회λ₯Ό λ†“μΉ˜μ§€ λ§ˆμ„Έμš”. μƒˆλ‘œμš΄ 것을 배우고 λ„μ „ν•˜λ©° μž₯기적인 λͺ©ν‘œλ₯Ό ν–₯ν•΄ λ‚˜μ•„κ°€λŠ” κ³Όμ •μ—μ„œ 성취감과 λ³΄λžŒμ„ λŠλ‚„ 수 μžˆμŠ΅λ‹ˆλ‹€. 영적, 정신적 μ„±μž₯ λ˜ν•œ 행볡에 이λ₯΄λŠ” μ€‘μš”ν•œ κΈΈμž‘μ΄κ°€ 될 κ²ƒμž…λ‹ˆλ‹€.\n\nλ§ˆμ§€λ§‰μœΌλ‘œ 행볡은 이 λͺ¨λ“  μš”μ†Œλ“€μ΄ μ‘°ν™”λ₯Ό 이룰 λ•Œ λΉ„λ‘œμ†Œ κ°€λŠ₯ν•΄μ§‘λ‹ˆλ‹€. 긍정적 λ§ˆμŒκ°€μ§, κ· ν˜• 작힌 μƒν™œ, 인간관계, μžκΈ°κ³„λ°œμ„ ν†΅ν•©μ μœΌλ‘œ 좔ꡬ할 λ•Œ μ§„μ •ν•œ 행볡이 우리 곁에 λ¨Έλ¬Ό κ²ƒμž…λ‹ˆλ‹€. μ˜€λŠ˜λΆ€ν„° μž‘μ€ μ‹€μ²œμœΌλ‘œ ν–‰λ³΅ν•œ μ‚Άμ˜ 여정을 μ‹œμž‘ν•΄λ³΄μ„Έμš”.', additional_kwargs={'usage': {'prompt_tokens': 2806, 'completion_tokens': 826, 'total_tokens': 3632}, 'stop_reason': 'end_turn', 'model_id': 'anthropic.claude-3-sonnet-20240229-v1:0'}, response_metadata={'usage': {'prompt_tokens': 2806, 'completion_tokens': 826, 'total_tokens': 3632}, 'stop_reason': 'end_turn', 'model_id': 'anthropic.claude-3-sonnet-20240229-v1:0'}, id='run-8490a66f-b9bc-4192-b6c6-7f0e34ebeb91-0', usage_metadata={'input_tokens': 2806, 'output_tokens': 826, 'total_tokens': 3632}),
 'revision_number': 2}

μƒμ„±λœ 에세이λ₯Ό μ΄μš©ν•˜μ—¬ 평가(critique)λ₯Ό μƒμ„±ν•©λ‹ˆλ‹€.

def reflection(state: State):    
    """You are a teacher grading an essay submission. \
Generate critique and recommendations for the user's submission. \
Provide detailed recommendations, including requests for length, depth, style, etc."""

    reflection_prompt = ChatPromptTemplate.from_messages(
        [
            (
                "system",
                "당신은 κ΅μ‚¬λ‘œμ„œ ν•™μ…μ˜ 에세이λ₯Ό ν‰κ°€ν•˜μ‚½λ‹ˆλ‹€. 비평과 κ°œμ„ μ‚¬ν•­μ„ μΉœμ ˆν•˜κ²Œ μ„€λͺ…ν•΄μ£Όμ„Έμš”."
                "μ΄λ•Œ μž₯점, 단점, 길이, 깊이, μŠ€νƒ€μΌλ“±μ— λŒ€ν•΄ μΆ©λΆ„ν•œ 정보λ₯Ό μ œκ³΅ν•©λ‹ˆλ‹€."
                "각 λ¬Έλ‹¨μ˜ κΈΈμ΄λŠ” μ΅œμ†Œ 200자 이상이 λ˜λ„λ‘ κ΄€λ ¨λœ 예제λ₯Ό μΆ©λΆ„νžˆ ν¬ν•¨ν•©λ‹ˆλ‹€.",
            ),
            ("human", "{essay}"),
        ]
    )
    
    chat = get_chat()
    reflect = reflection_prompt | chat
            
    res = reflect.invoke({"essay": state['essay'].content})    
    response = HumanMessage(content=res.content)    
    
    return {
        "critique": response,
        "revision_number": int(state['revision_number'])
    }

ν‰κ°€μ˜ κ²°κ³ΌλŠ” μ˜ˆλŠ” μ•„λž˜μ™€ κ°™μŠ΅λ‹ˆλ‹€.

'μ „λ°˜μ μœΌλ‘œ ν–‰λ³΅ν•œ 삢을 μ‚΄κΈ° μœ„ν•œ 쒋은 지침듀을 μ œμ‹œν•˜κ³  μžˆμŠ΅λ‹ˆλ‹€. λ‹€μŒμ€ λͺ‡ 가지 μž₯점과 κ°œμ„  사항에 λŒ€ν•œ ν”Όλ“œλ°±μž…λ‹ˆλ‹€.\n\nμž₯점:\n\n1. ν–‰λ³΅μ˜ 의미λ₯Ό 잘 μ •μ˜ν•˜κ³ , 물질적 ν’μš”λ‚˜ μΌμ‹œμ  기쁨을 λ„˜μ–΄μ„œλŠ” μ‚Άμ˜ 만쑱과 μΆ©λ§Œν•¨μ΄λΌκ³  μ„€λͺ…ν•œ 점이 μ’‹μŠ΅λ‹ˆλ‹€.\n\n2. 행볡을 μœ„ν•œ ꡬ체적인 μš”μ†Œλ“€(긍정적 λ§ˆμŒκ°€μ§, κ· ν˜• 작힌 μƒν™œ, 인간관계, μžκΈ°κ³„λ°œ)을 잘 μ œμ‹œν•˜κ³  μžˆμŠ΅λ‹ˆλ‹€. \n\n3. 각 μš”μ†Œμ— λŒ€ν•΄ ꡬ체적인 μ‹€μ²œ λ°©μ•ˆμ„ μ œμ•ˆν•˜κ³  μžˆμ–΄ μ‹€μš©μ μž…λ‹ˆλ‹€. 예λ₯Ό λ“€μ–΄ "μž‘μ€ μΌμƒμ˜ 기쁨에 κ°μ‚¬ν•˜λŠ” μŠ΅κ΄€"μ΄λ‚˜ "μš΄λ™κ³Ό κ±΄κ°•ν•œ μ‹μŠ΅κ΄€"κ³Ό 같은 ꡬ체적인 방법듀을 μ–ΈκΈ‰ν•˜κ³  μžˆμŠ΅λ‹ˆλ‹€.\n\n4. λ§ˆμ§€λ§‰ λΆ€λΆ„μ—μ„œ ν–‰λ³΅μ˜ μš”μ†Œλ“€μ΄ μ‘°ν™”λ₯Ό 이루어야 함을 κ°•μ‘°ν•˜κ³  μžˆμ–΄ κ· ν˜• 작힌 관점을 μ œμ‹œν•˜κ³  μžˆμŠ΅λ‹ˆλ‹€.\n\nκ°œμ„  사항:\n\n1. 각 λ¬Έλ‹¨μ˜ 길이가 λ‹€μ†Œ 짧은 νŽΈμž…λ‹ˆλ‹€. 각 μš”μ†Œμ— λŒ€ν•΄ μ’€ 더 ꡬ체적인 μ˜ˆμ‹œλ‚˜ μ„€λͺ…을 μΆ”κ°€ν•˜λ©΄ 이해도λ₯Ό 높일 수 μžˆμ„ κ²ƒμž…λ‹ˆλ‹€.\n\n2. 긍정적 λ§ˆμŒκ°€μ§μ„ κΈ°λ₯΄λŠ” ꡬ체적인 방법(예: λͺ…상, 긍정적 자기 λŒ€ν™” λ“±)에 λŒ€ν•œ μ„€λͺ…이 λΆ€μ‘±ν•©λ‹ˆλ‹€.\n\n3. 인간관계 λΆ€λΆ„μ—μ„œ κ°€μ‘±, 친ꡬ 외에도 μ§€μ—­μ‚¬νšŒλ‚˜ μ‚¬νšŒμ  μœ λŒ€κ°μ˜ μ€‘μš”μ„±μ— λŒ€ν•΄ μ–ΈκΈ‰ν•  수 μžˆμŠ΅λ‹ˆλ‹€.\n\n4. μžκΈ°κ³„λ°œκ³Ό μ„±μž₯의 ꡬ체적인 방법(예: μƒˆλ‘œμš΄ 기술 배우기, μžμ›λ΄‰μ‚¬ λ“±)에 λŒ€ν•œ μ„€λͺ…이 λΆ€μ‘±ν•©λ‹ˆλ‹€.\n\n5. 행볡을 λ°©ν•΄ν•˜λŠ” μš”μΈλ“€(예: 슀트레슀, 뢀정적 μŠ΅κ΄€ λ“±)에 λŒ€ν•œ μ–ΈκΈ‰κ³Ό 이λ₯Ό κ·Ήλ³΅ν•˜λŠ” 방법에 λŒ€ν•œ 쑰언이 μΆ”κ°€λ˜λ©΄ 쒋을 것 κ°™μŠ΅λ‹ˆλ‹€.\n\nμ „λ°˜μ μœΌλ‘œ ν–‰λ³΅ν•œ 삢을 μœ„ν•œ 쒋은 지침을 μ œμ‹œν•˜κ³  μžˆμ§€λ§Œ, 각 μš”μ†Œμ— λŒ€ν•œ ꡬ체적인 μ˜ˆμ‹œμ™€ μ„€λͺ…을 λ³΄μ™„ν•˜κ³  κ· ν˜• 작힌 관점을 더 κ°•μ‘°ν•œλ‹€λ©΄ λ”μš± 도움이 될 κ²ƒμž…λ‹ˆλ‹€.'

평가λ₯Ό μ΄μš©ν•˜μ—¬ μƒˆλ‘œμš΄ 검색 keywordλ₯Ό μƒμ„±ν•˜κ³  Tavilyλ₯Ό μ΄μš©ν•œ 웹검색을 톡해 contentλ₯Ό μ—…λ°μ΄νŠΈ ν•©λ‹ˆλ‹€.

def research_critique(state: State):
    system = """You are a researcher charged with providing information that can \
be used when making any requested revisions (as outlined below). \
Generate a list of search queries that will gather any relevant information. Only generate 3 queries max."""
    
    critique_prompt = ChatPromptTemplate.from_messages(
        [
            ("system", system),
            ("human", "{critique}"),
        ]
    )
    
    chat = get_chat()           
    critique = critique_prompt | chat    
    response = critique.invoke({"critique": state['critique']})
    print('response.content: ', response.content)
    
    chat = get_chat()
    structured_llm = chat.with_structured_output(Queries, include_raw=True)
    info = structured_llm.invoke(response.content)
    # print('info: ', info)
    
    content = ""
    if not info['parsed'] == None:
        queries = info['parsed']
        print('queries: ', queries.queries)
        
        content = state["content"] if state.get("content") is not None else []
        search = TavilySearchResults(k=2)
        for q in queries.queries:
            response = search.invoke(q)     
            # print('response: ', response)        
            for r in response:
                content.append(r['content'])
    return {
        "content": content,
        "revision_number": int(state['revision_number'])
    }

should_continue()μ—μ„œλŠ” max_revision λ°˜λ³΅ν•˜λ„λ‘ ν•©λ‹ˆλ‹€.

def should_continue(state, config):
    max_revisions = config.get("configurable", {}).get("max_revisions", MAX_REVISIONS)
    print("max_revisions: ", max_revisions)
        
    if state["revision_number"] > max_revisions:
        return "end"
    return "contine"

Workflow을 μœ„ν•œ Graphλ₯Ό μ€€λΉ„ν•©λ‹ˆλ‹€.

workflow = StateGraph(State)

workflow.add_node("planner", plan)
workflow.add_node("generation", generation)
workflow.add_node("reflection", reflection)
workflow.add_node("research_plan", research_plan)
workflow.add_node("research_critique", research_critique)

workflow.set_entry_point("planner")

workflow.add_conditional_edges(
    "generation", 
    should_continue, 
    {
        "end": END, 
        "contine": "reflection"}
)

workflow.add_edge("planner", "research_plan")
workflow.add_edge("research_plan", "generation")

workflow.add_edge("reflection", "research_critique")
workflow.add_edge("research_critique", "generation")

app = workflow.compile()

μ΄λ•Œ 얻어진 GraphλŠ” μ•„λž˜μ™€ κ°™μŠ΅λ‹ˆλ‹€.

from IPython.display import Image, display
display(Image(app.get_graph().draw_mermaid_png()))

image

μ•„λž˜μ™€ 같이 μ‹€ν–‰ν•©λ‹ˆλ‹€.

inputs = {"task": "즐겁게 μ‚¬λŠ” 방법"}
config = {
    "recursion_limit": 50,
    "max_revisions": 2,
}
for output in app.stream(inputs, config=config):
    for key, value in output.items():
        print(f"Finished: {key}")

print("Final: ", value["essay"])

μ‹€ν–‰ν•œ κ²°κ³ΌλŠ” μ•„λž˜μ™€ κ°™μŠ΅λ‹ˆλ‹€.

easy-writer

LangSmith둜 ν™•μΈν•œ λ™μž‘μ€ μ•„λž˜μ™€ κ°™μŠ΅λ‹ˆλ‹€. 전체 138μ΄ˆκ°€ μ†Œμš”λ˜μ—ˆμŠ΅λ‹ˆλ‹€.

image

Easy Writer

Essay Writer의 λ‚΄μš©μ„ μ •λ¦¬ν•©λ‹ˆλ‹€.

전체 Graph의 κ΅¬μ„±λ„λŠ” μ•„λž˜μ™€ κ°™μŠ΅λ‹ˆλ‹€.

λ¨Όμ € class와 ν”„λ‘¬ν”„νŠΈλ₯Ό μ •μ˜ν•©λ‹ˆλ‹€.

from langgraph.graph import StateGraph, END
from typing import TypedDict, Annotated, List
import operator
from langgraph.checkpoint.sqlite import SqliteSaver
from langchain_core.messages import AnyMessage, SystemMessage, HumanMessage, AIMessage, ChatMessage

memory = SqliteSaver.from_conn_string(":memory:")

class AgentState(TypedDict):
    task: str
    plan: str
    draft: str
    critique: str
    content: List[str]
    revision_number: int
    max_revisions: int

PLAN_PROMPT = """You are an expert writer tasked with writing a high level outline of an essay. \
Write such an outline for the user provided topic. Give an outline of the essay along with any relevant notes \
or instructions for the sections."""

WRITER_PROMPT = """You are an essay assistant tasked with writing excellent 5-paragraph essays.\
Generate the best essay possible for the user's request and the initial outline. \
If the user provides critique, respond with a revised version of your previous attempts. \
Utilize all the information below as needed: 

------

{content}"""

REFLECTION_PROMPT = """You are a teacher grading an essay submission. \
Generate critique and recommendations for the user's submission. \
Provide detailed recommendations, including requests for length, depth, style, etc."""

RESEARCH_PLAN_PROMPT = """You are a researcher charged with providing information that can \
be used when writing the following essay. Generate a list of search queries that will gather \
any relevant information. Only generate 3 queries max."""

RESEARCH_CRITIQUE_PROMPT = """You are a researcher charged with providing information that can \
be used when making any requested revisions (as outlined below). \
Generate a list of search queries that will gather any relevant information. Only generate 3 queries max."""

class와 ν•¨μˆ˜λ₯Ό κ΅¬μ„±ν•©λ‹ˆλ‹€.

from langchain_core.pydantic_v1 import BaseModel

class Queries(BaseModel):
    queries: List[str]

from tavily import TavilyClient
import os
tavily = TavilyClient(api_key=os.environ["TAVILY_API_KEY"])

def plan_node(state: AgentState):
    messages = [
        SystemMessage(content=PLAN_PROMPT), 
        HumanMessage(content=state['task'])
    ]
    response = model.invoke(messages)
    return {"plan": response.content}

def research_plan_node(state: AgentState):
    queries = model.with_structured_output(Queries).invoke([
        SystemMessage(content=RESEARCH_PLAN_PROMPT),
        HumanMessage(content=state['task'])
    ])
    content = state['content'] or []
    for q in queries.queries:
        response = tavily.search(query=q, max_results=2)
        for r in response['results']:
            content.append(r['content'])
    return {"content": content}

def generation_node(state: AgentState):
    content = "\n\n".join(state['content'] or [])
    user_message = HumanMessage(
        content=f"{state['task']}\n\nHere is my plan:\n\n{state['plan']}")
    messages = [
        SystemMessage(
            content=WRITER_PROMPT.format(content=content)
        ),
        user_message
        ]
    response = model.invoke(messages)
    return {
        "draft": response.content, 
        "revision_number": state.get("revision_number", 1) + 1
    }

def reflection_node(state: AgentState):
    messages = [
        SystemMessage(content=REFLECTION_PROMPT), 
        HumanMessage(content=state['draft'])
    ]
    response = model.invoke(messages)
    return {"critique": response.content}

def research_critique_node(state: AgentState):
    queries = model.with_structured_output(Queries).invoke([
        SystemMessage(content=RESEARCH_CRITIQUE_PROMPT),
        HumanMessage(content=state['critique'])
    ])
    content = state['content'] or []
    for q in queries.queries:
        response = tavily.search(query=q, max_results=2)
        for r in response['results']:
            content.append(r['content'])
    return {"content": content}

def should_continue(state):
    if state["revision_number"] > state["max_revisions"]:
        return END
    return "reflect"

μ•„λž˜μ™€ 같이 Graphλ₯Ό κ΅¬μ„±ν•©λ‹ˆλ‹€.

builder = StateGraph(AgentState)

builder.add_node("planner", plan_node)
builder.add_node("generate", generation_node)
builder.add_node("reflect", reflection_node)
builder.add_node("research_plan", research_plan_node)
builder.add_node("research_critique", research_critique_node)

builder.set_entry_point("planner")

builder.add_conditional_edges(
    "generate", 
    should_continue, 
    {END: END, "reflect": "reflect"}
)

builder.add_edge("planner", "research_plan")
builder.add_edge("research_plan", "generate")

builder.add_edge("reflect", "research_critique")
builder.add_edge("research_critique", "generate")

graph = builder.compile(checkpointer=memory)

μ•„λž˜μ™€ 같이 μ‹€ν–‰ν•©λ‹ˆλ‹€.

thread = {"configurable": {"thread_id": "1"}}
for s in graph.stream({
    'task': "what is the difference between langchain and langsmith",
    "max_revisions": 2,
    "revision_number": 1,
}, thread):
    print(s)

InterfaceλŠ” μ•„λž˜μ™€ κ°™μŠ΅λ‹ˆλ‹€.

import warnings
warnings.filterwarnings("ignore")

MultiAgent = ewriter()
app = writer_gui(MultiAgent.graph)
app.launch()