Skip to content

Commit 7b0042a

Browse files
michelle-hadfield-navaCopilotyoomlam
authored
feat: Add generate referrals endpoint (#21)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Yoom Lam <yoom@navapbc.com>
1 parent 919726b commit 7b0042a

5 files changed

Lines changed: 163 additions & 5 deletions

File tree

app/src/pipelines/generate_referrals/__init__.py

Whitespace-only changes.
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
import json
2+
import logging
3+
from pprint import pformat
4+
5+
import hayhooks
6+
from hayhooks import BasePipelineWrapper
7+
from haystack import Pipeline
8+
from haystack.components.builders import ChatPromptBuilder
9+
from haystack.dataclasses.chat_message import ChatMessage
10+
from haystack_integrations.components.generators.amazon_bedrock import AmazonBedrockChatGenerator
11+
from pydantic import BaseModel
12+
from sqlalchemy.inspection import inspect
13+
14+
from src.app_config import config
15+
from src.db.models.support_listing import Support
16+
17+
logger = logging.getLogger(__name__)
18+
19+
20+
class Resource(BaseModel):
21+
resource_name: str
22+
resource_addresses: list[str]
23+
resource_phones: list[str]
24+
description: str
25+
justification: str
26+
27+
28+
resource_as_json = json.dumps(Resource.model_json_schema(), indent=2)
29+
30+
system_prompt = """
31+
You are a supporting API for Goodwill Central Texas Referral. You are designed to help career case managers provide high-quality, local resource referrals to client's in Central Texas.
32+
Your role is to support Goodwill Central Texas career case managers working with low-income job seekers and learners in Austin and surrounding counties (Bastrop, Blanco, Burnet, Caldwell, DeWitt, Fayette, Gillespie, Gonzales, Hays, Lavaca, Lee, Llano, Mason, Travis, Williamson).
33+
34+
## Task Checklist
35+
- Evaluate the client needs and determine their eligibility (Factors to consider: age, income, disability, immigration/veteran status, number of dependents)
36+
- Prioritize Goodwill resources first (Basic Needs Resource packet, Goodwill websites)
37+
- Rank recommendations by proximity, eligibility fit, and other relevant factors
38+
39+
## Core Instructions
40+
- Use only trusted and up-to-date sources: Goodwill, government, vetted nonprofits, trusted news outlets (Findhelp, 211, Connect ATX permitted). Never use unreliable websites (e.g., shelterlistings.org, needhelppayingbills.com).
41+
- Never invent or fabricate resources. If none are available, state this clearly and suggest actionable, specific next steps
42+
43+
List of resources to choose from:
44+
{% for s in supports %}
45+
- {{ s.content }}
46+
{% endfor %}
47+
48+
## Response Constraints
49+
- Your response should ONLY include resources.
50+
- Do not summarize your assessment of the clients needs.
51+
- Limit the description for a resource to be less than 255 words.
52+
- Return a JSON list of resources in the following format:
53+
'''{{ resource_json }}'''
54+
"""
55+
model = "us.anthropic.claude-3-5-sonnet-20241022-v2:0"
56+
57+
prompt_template = [
58+
ChatMessage.from_system(system_prompt),
59+
ChatMessage.from_user("""User query: {{ query }}"""),
60+
]
61+
62+
63+
class PipelineWrapper(BasePipelineWrapper):
64+
name = "generate_referrals"
65+
66+
def setup(self) -> None:
67+
pipeline = Pipeline()
68+
pipeline.add_component("llm", AmazonBedrockChatGenerator(model=model))
69+
pipeline.add_component(
70+
instance=ChatPromptBuilder(
71+
template=prompt_template, required_variables=["query", "supports", "resource_json"]
72+
),
73+
name="prompt_builder",
74+
)
75+
pipeline.connect("prompt_builder", "llm.messages")
76+
77+
self.pipeline = pipeline
78+
79+
# Called for the `generate-referrals/run` endpoint
80+
def run_api(self, query: str) -> dict:
81+
supports_from_db = retrieve_supports_from_db()
82+
response = self.pipeline.run(
83+
{
84+
"prompt_builder": {
85+
"query": query,
86+
"supports": supports_from_db,
87+
"resource_json": resource_as_json,
88+
},
89+
}
90+
)
91+
logger.info("Results: %s", pformat(response))
92+
return response
93+
94+
# https://docs.haystack.deepset.ai/docs/hayhooks#openai-compatibility
95+
# Called for the `{pipeline_name}/chat`, `/chat/completions`, or `/v1/chat/completions` streaming endpoint using Server-Sent Events (SSE)
96+
def run_chat_completion(self, model: str, messages: list, body: dict) -> None:
97+
logger.info("Running chat completion with model: %s, messages: %s", model, messages)
98+
question = hayhooks.get_last_user_message(messages)
99+
logger.info("Question: %s", question)
100+
return hayhooks.streaming_generator(
101+
pipeline=self.pipeline,
102+
pipeline_run_args={
103+
"echo_component": {
104+
"prompt": [ChatMessage.from_user(question)],
105+
"history": messages[:-1],
106+
}
107+
},
108+
)
109+
110+
111+
def retrieve_supports_from_db() -> list[str]:
112+
all_supports: list[str] = []
113+
with config.db_session() as db_session, db_session.begin():
114+
all_db_supports = db_session.query(Support).all()
115+
116+
for support in all_db_supports:
117+
support_dict = {
118+
c.key: getattr(support, c.key) for c in inspect(Support).mapper.column_attrs
119+
}
120+
support_as_str = json.dumps(support_dict, default=str)
121+
all_supports.append(support_as_str)
122+
return all_supports

app/tests/src/db/models/factories.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -103,8 +103,9 @@ class Meta:
103103
support_listing = factory.SubFactory(SupportListingFactory)
104104

105105
name = factory.Faker("name")
106-
addresses = factory.Faker("address")
107-
phone_numbers = factory.Faker("phone_number")
108-
description = factory.Faker("sentence")
109-
website = factory.Faker("url")
110-
email_addresses = factory.Faker("email")
106+
addresses = factory.LazyFunction(lambda: [fake.address().replace("\n", ", ")])
107+
phone_numbers = factory.LazyFunction(lambda: [fake.phone_number()])
108+
email_addresses = factory.LazyFunction(lambda: [fake.email()])
109+
110+
description = factory.LazyFunction(lambda: fake.sentence())
111+
website = factory.LazyFunction(lambda: fake.url())

app/tests/src/pipelines/generate_referrals/__init__.py

Whitespace-only changes.
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import json
2+
3+
import pytest
4+
from sqlalchemy.inspection import inspect
5+
6+
from src.adapters import db
7+
from src.db.models.support_listing import Support
8+
from src.pipelines.generate_referrals.pipeline_wrapper import retrieve_supports_from_db
9+
from tests.src.db.models.factories import SupportFactory, SupportListingFactory
10+
11+
12+
@pytest.fixture
13+
def seed_supports(db_session: db.Session):
14+
# remove all pre-existing Support records
15+
db_session.query(Support).delete()
16+
17+
support_listing = SupportListingFactory.create()
18+
supports = []
19+
for i in range(0, 3):
20+
support = SupportFactory.create(support_listing=support_listing, name=f"support{i}")
21+
support_as_json_str = json.dumps(
22+
{c.key: getattr(support, c.key) for c in inspect(Support).mapper.column_attrs},
23+
default=str,
24+
)
25+
supports.append(support_as_json_str)
26+
return supports
27+
28+
29+
def test_retrieve_supports_from_db(enable_factory_create, seed_supports, db_session: db.Session):
30+
supports_from_db = retrieve_supports_from_db()
31+
32+
assert len(supports_from_db) == 3
33+
assert seed_supports[0] == supports_from_db[0]
34+
assert seed_supports[1] == supports_from_db[1]
35+
assert seed_supports[2] == supports_from_db[2]

0 commit comments

Comments
 (0)